Compare commits
46 Commits
revert-706
...
v1.0.0
Author | SHA1 | Date | |
---|---|---|---|
35f5c62633 | |||
0f0fc39d07 | |||
a13b6b2b70 | |||
4212b95232 | |||
38a73a603b | |||
c48d9fd4d7 | |||
0753987b5a | |||
815ff7dc2b | |||
46684d420d | |||
eca09984a3 | |||
ce63c6423e | |||
09699afe82 | |||
36c8ad439d | |||
5dc77ceed5 | |||
c7baa26b2d | |||
4d0454abcd | |||
1dafbf105e | |||
773f013115 | |||
c5cd460595 | |||
845352046b | |||
597f1087f9 | |||
511334683a | |||
223a4ad45d | |||
edf31ec1d3 | |||
1539557005 | |||
1d3ba4e3ac | |||
4110aa00db | |||
7eb52cda36 | |||
7872fb9cbd | |||
651181e62c | |||
38a245f2fc | |||
1b4289f93f | |||
d0697c24fd | |||
8c24e29081 | |||
2b9d26e2ff | |||
ab148a7654 | |||
553e650fbe | |||
9690a24c68 | |||
978d5d44a2 | |||
9df476543a | |||
cf303ebe97 | |||
b1d1d89ca5 | |||
3a599d0a0a | |||
8340f6b906 | |||
ddb034b14d | |||
bfa2f67393 |
2
.github/ci-cd-scripts/upload-results.sh
vendored
@ -6,6 +6,7 @@ if [ -z "${TAB_API_URL:-}" ] || [ -z "${TAB_API_KEY:-}" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
project="https://github.com/KittyCAD/modeling-app"
|
project="https://github.com/KittyCAD/modeling-app"
|
||||||
|
suite="${CI_SUITE:-unit}"
|
||||||
branch="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME:-}}"
|
branch="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME:-}}"
|
||||||
commit="${CI_COMMIT_SHA:-${GITHUB_SHA:-}}"
|
commit="${CI_COMMIT_SHA:-${GITHUB_SHA:-}}"
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ echo "Uploading batch results"
|
|||||||
curl --silent --request POST \
|
curl --silent --request POST \
|
||||||
--header "X-API-Key: ${TAB_API_KEY}" \
|
--header "X-API-Key: ${TAB_API_KEY}" \
|
||||||
--form "project=${project}" \
|
--form "project=${project}" \
|
||||||
|
--form "suite=${suite}" \
|
||||||
--form "branch=${branch}" \
|
--form "branch=${branch}" \
|
||||||
--form "commit=${commit}" \
|
--form "commit=${commit}" \
|
||||||
--form "tests=@test-results/junit.xml" \
|
--form "tests=@test-results/junit.xml" \
|
||||||
|
5
.github/workflows/cargo-test.yml
vendored
@ -88,6 +88,7 @@ jobs:
|
|||||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
|
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
|
||||||
ZOO_HOST: https://api.dev.zoo.dev
|
ZOO_HOST: https://api.dev.zoo.dev
|
||||||
RUST_BACKTRACE: full
|
RUST_BACKTRACE: full
|
||||||
|
RUST_MIN_STACK: 10485760000
|
||||||
- name: Commit differences
|
- name: Commit differences
|
||||||
if: steps.path-changes.outputs.outside-kcl-samples == 'false' && steps.cargo-test-kcl-samples.outcome == 'failure'
|
if: steps.path-changes.outputs.outside-kcl-samples == 'false' && steps.cargo-test-kcl-samples.outcome == 'failure'
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -119,6 +120,7 @@ jobs:
|
|||||||
# Configure nextest when it's run by insta (via just).
|
# Configure nextest when it's run by insta (via just).
|
||||||
NEXTEST_PROFILE: ci
|
NEXTEST_PROFILE: ci
|
||||||
RUST_BACKTRACE: full
|
RUST_BACKTRACE: full
|
||||||
|
RUST_MIN_STACK: 10485760000
|
||||||
- name: Build and archive tests
|
- name: Build and archive tests
|
||||||
run: |
|
run: |
|
||||||
cd rust
|
cd rust
|
||||||
@ -182,6 +184,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
|
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
|
||||||
ZOO_HOST: https://api.dev.zoo.dev
|
ZOO_HOST: https://api.dev.zoo.dev
|
||||||
|
RUST_MIN_STACK: 10485760000
|
||||||
- name: Upload results
|
- name: Upload results
|
||||||
if: always()
|
if: always()
|
||||||
run: .github/ci-cd-scripts/upload-results.sh
|
run: .github/ci-cd-scripts/upload-results.sh
|
||||||
@ -190,6 +193,7 @@ jobs:
|
|||||||
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
|
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
|
||||||
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
|
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
CI_SUITE: unit:kcl
|
||||||
run-internal-kcl-samples:
|
run-internal-kcl-samples:
|
||||||
name: cargo test (internal-kcl-samples)
|
name: cargo test (internal-kcl-samples)
|
||||||
runs-on:
|
runs-on:
|
||||||
@ -238,6 +242,7 @@ jobs:
|
|||||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
|
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
|
||||||
ZOO_HOST: https://api.dev.zoo.dev
|
ZOO_HOST: https://api.dev.zoo.dev
|
||||||
MODELING_APP_INTERNAL_SAMPLES_SECRET: ${{secrets.MODELING_APP_INTERNAL_SAMPLES_SECRET}}
|
MODELING_APP_INTERNAL_SAMPLES_SECRET: ${{secrets.MODELING_APP_INTERNAL_SAMPLES_SECRET}}
|
||||||
|
RUST_MIN_STACK: 10485760000
|
||||||
run-wasm-tests:
|
run-wasm-tests:
|
||||||
name: Run wasm tests
|
name: Run wasm tests
|
||||||
strategy:
|
strategy:
|
||||||
|
17
.github/workflows/e2e-tests.yml
vendored
@ -143,7 +143,7 @@ jobs:
|
|||||||
- name: Install browsers
|
- name: Install browsers
|
||||||
run: npm run playwright install --with-deps
|
run: npm run playwright install --with-deps
|
||||||
|
|
||||||
- name: Capture snapshots
|
- name: Test snapshots
|
||||||
uses: nick-fields/retry@v3.0.2
|
uses: nick-fields/retry@v3.0.2
|
||||||
with:
|
with:
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -156,6 +156,19 @@ jobs:
|
|||||||
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
|
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
|
||||||
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
|
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
CI_SUITE: snapshots
|
||||||
|
TARGET: web
|
||||||
|
|
||||||
|
- name: Update snapshots
|
||||||
|
if: always()
|
||||||
|
run: npm run test:snapshots -- --last-failed --update-snapshots
|
||||||
|
env:
|
||||||
|
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
|
TAB_API_URL: ${{ secrets.TAB_API_URL }}
|
||||||
|
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
|
||||||
|
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
CI_SUITE: snapshots
|
||||||
TARGET: web
|
TARGET: web
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
@ -173,7 +186,7 @@ jobs:
|
|||||||
id: git-check
|
id: git-check
|
||||||
run: |
|
run: |
|
||||||
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
|
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
|
||||||
if git status | grep -q "Changes to be committed"
|
if git status | grep --quiet "Changes to be committed"
|
||||||
then echo "modified=true" >> $GITHUB_OUTPUT
|
then echo "modified=true" >> $GITHUB_OUTPUT
|
||||||
else echo "modified=false" >> $GITHUB_OUTPUT
|
else echo "modified=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
@ -4,7 +4,7 @@ excerpt: "Documentation of the KCL language for the Zoo Design Studio."
|
|||||||
layout: manual
|
layout: manual
|
||||||
---
|
---
|
||||||
|
|
||||||
This is a reference for KCL. If you are learning KCL, you may prefer the [guide]() which explains
|
This is a reference for KCL. If you are learning KCL, you may prefer the [guide](https://zoo.dev/docs/kcl-book/intro.html) which explains
|
||||||
things in a more tutorial fashion. See also our documentation of the [standard library](/docs/kcl-std).
|
things in a more tutorial fashion. See also our documentation of the [standard library](/docs/kcl-std).
|
||||||
|
|
||||||
## Topics
|
## Topics
|
||||||
|
@ -27,9 +27,6 @@ import increment from "util.kcl"
|
|||||||
answer = increment(41)
|
answer = increment(41)
|
||||||
```
|
```
|
||||||
|
|
||||||
Imported files _must_ be in the same project so that units are uniform across
|
|
||||||
modules. This means that it must be in the same directory.
|
|
||||||
|
|
||||||
Import statements must be at the top-level of a file. It is not allowed to have
|
Import statements must be at the top-level of a file. It is not allowed to have
|
||||||
an `import` statement inside a function or in the body of an if‑else.
|
an `import` statement inside a function or in the body of an if‑else.
|
||||||
|
|
||||||
@ -58,6 +55,9 @@ Imported symbols can be renamed for convenience or to avoid name collisions.
|
|||||||
import increment as inc, decrement as dec from "util.kcl"
|
import increment as inc, decrement as dec from "util.kcl"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can import files from the current directory or from subdirectories, but if importing from a
|
||||||
|
subdirectory you can only import `main.kcl`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Functions vs `clone`
|
## Functions vs `clone`
|
||||||
@ -229,6 +229,19 @@ The final statement is what's important because it's the return value of the
|
|||||||
entire module. The module is expected to return a single object that can be used
|
entire module. The module is expected to return a single object that can be used
|
||||||
as a variable by the file that imports it.
|
as a variable by the file that imports it.
|
||||||
|
|
||||||
|
The name of the file or subdirectory is used as the name of the variable within the importing program.
|
||||||
|
If you want to use a different name, you can do so by using the `as` keyword:
|
||||||
|
|
||||||
|
```kcl,norun
|
||||||
|
import "cube.kcl" // Introduces a new variable called `cube`.
|
||||||
|
import "cube.kcl" as block // Introduces a new variable called `block`.
|
||||||
|
import "cube/main.kcl" // Introduces a new variable called `cube`.
|
||||||
|
import "cube/main.kcl" as block // Introduces a new variable called `block`.
|
||||||
|
```
|
||||||
|
|
||||||
|
If the filename includes hyphens (`-`) or starts with an underscore (`_`), then you must specify a
|
||||||
|
variable name.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Multiple instances of the same import
|
## Multiple instances of the same import
|
||||||
|
@ -11,7 +11,8 @@ layout: manual
|
|||||||
circle(
|
circle(
|
||||||
@sketch_or_surface: Sketch | Plane | Face,
|
@sketch_or_surface: Sketch | Plane | Face,
|
||||||
center: Point2d,
|
center: Point2d,
|
||||||
radius: number(Length),
|
radius?: number(Length),
|
||||||
|
diameter?: number(Length),
|
||||||
tag?: tag,
|
tag?: tag,
|
||||||
): Sketch
|
): Sketch
|
||||||
```
|
```
|
||||||
@ -25,7 +26,8 @@ the provided (x, y) origin point.
|
|||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `sketch_or_surface` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) or [`Plane`](/docs/kcl-std/types/std-types-Plane) or [`Face`](/docs/kcl-std/types/std-types-Face) | Sketch to extend, or plane or surface to sketch on. | Yes |
|
| `sketch_or_surface` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) or [`Plane`](/docs/kcl-std/types/std-types-Plane) or [`Face`](/docs/kcl-std/types/std-types-Face) | Sketch to extend, or plane or surface to sketch on. | Yes |
|
||||||
| `center` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | The center of the circle. | Yes |
|
| `center` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | The center of the circle. | Yes |
|
||||||
| `radius` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The radius of the circle. | Yes |
|
| `radius` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The radius of the circle. Incompatible with `diameter`. | No |
|
||||||
|
| `diameter` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The diameter of the circle. Incompatible with `radius`. | No |
|
||||||
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this circle. | No |
|
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this circle. | No |
|
||||||
|
|
||||||
### Returns
|
### Returns
|
||||||
@ -51,7 +53,7 @@ exampleSketch = startSketchOn(XZ)
|
|||||||
|> line(end = [0, 30])
|
|> line(end = [0, 30])
|
||||||
|> line(end = [-30, 0])
|
|> line(end = [-30, 0])
|
||||||
|> close()
|
|> close()
|
||||||
|> subtract2d(tool = circle(center = [0, 15], radius = 5))
|
|> subtract2d(tool = circle(center = [0, 15], diameter = 10))
|
||||||
|
|
||||||
example = extrude(exampleSketch, length = 5)
|
example = extrude(exampleSketch, length = 5)
|
||||||
```
|
```
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
---
|
---
|
||||||
title: "patternTransform2d"
|
title: "patternTransform2d"
|
||||||
subtitle: "Function in std::sketch"
|
subtitle: "Function in std::sketch"
|
||||||
excerpt: "Just like patternTransform, but works on 2D sketches not 3D solids."
|
excerpt: "Just like `patternTransform`, but works on 2D sketches not 3D solids."
|
||||||
layout: manual
|
layout: manual
|
||||||
---
|
---
|
||||||
|
|
||||||
Just like patternTransform, but works on 2D sketches not 3D solids.
|
Just like `patternTransform`, but works on 2D sketches not 3D solids.
|
||||||
|
|
||||||
```kcl
|
```kcl
|
||||||
patternTransform2d(
|
patternTransform2d(
|
||||||
@sketches: [Sketch],
|
@sketches: [Sketch; 1+],
|
||||||
instances: number,
|
instances: number(_),
|
||||||
transform: FunctionSource,
|
transform: fn(number(_)): { },
|
||||||
useOriginal?: bool,
|
useOriginal?: boolean,
|
||||||
): [Sketch]
|
): [Sketch; 1+]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -22,14 +22,14 @@ patternTransform2d(
|
|||||||
|
|
||||||
| Name | Type | Description | Required |
|
| Name | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `sketches` | [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) | The sketch(es) to duplicate | Yes |
|
| `sketches` | [`[Sketch; 1+]`](/docs/kcl-std/types/std-types-Sketch) | The sketch(es) to duplicate. | Yes |
|
||||||
| `instances` | [`number`](/docs/kcl-std/types/std-types-number) | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
|
| `instances` | [`number(_)`](/docs/kcl-std/types/std-types-number) | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
|
||||||
| `transform` | `FunctionSource` | How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples. | Yes |
|
| `transform` | [`fn(number(_)): { }`](/docs/kcl-std/types/std-types-fn) | How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples. | Yes |
|
||||||
| `useOriginal` | [`bool`](/docs/kcl-std/types/std-types-bool) | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
| `useOriginal` | `boolean` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. | No |
|
||||||
|
|
||||||
### Returns
|
### Returns
|
||||||
|
|
||||||
[`[Sketch]`](/docs/kcl-std/types/std-types-Sketch)
|
[`[Sketch; 1+]`](/docs/kcl-std/types/std-types-Sketch)
|
||||||
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
@ -65,7 +65,7 @@ layout: manual
|
|||||||
* [`line`](/docs/kcl-std/line)
|
* [`line`](/docs/kcl-std/line)
|
||||||
* [`loft`](/docs/kcl-std/loft)
|
* [`loft`](/docs/kcl-std/loft)
|
||||||
* [`patternCircular2d`](/docs/kcl-std/patternCircular2d)
|
* [`patternCircular2d`](/docs/kcl-std/patternCircular2d)
|
||||||
* [`patternTransform2d`](/docs/kcl-std/patternTransform2d)
|
* [`patternTransform2d`](/docs/kcl-std/functions/std-sketch-patternTransform2d)
|
||||||
* [`polygon`](/docs/kcl-std/polygon)
|
* [`polygon`](/docs/kcl-std/polygon)
|
||||||
* [`profileStart`](/docs/kcl-std/profileStart)
|
* [`profileStart`](/docs/kcl-std/profileStart)
|
||||||
* [`profileStartX`](/docs/kcl-std/profileStartX)
|
* [`profileStartX`](/docs/kcl-std/profileStartX)
|
||||||
@ -94,7 +94,7 @@ layout: manual
|
|||||||
* [`intersect`](/docs/kcl-std/intersect)
|
* [`intersect`](/docs/kcl-std/intersect)
|
||||||
* [`patternCircular3d`](/docs/kcl-std/patternCircular3d)
|
* [`patternCircular3d`](/docs/kcl-std/patternCircular3d)
|
||||||
* [`patternLinear3d`](/docs/kcl-std/patternLinear3d)
|
* [`patternLinear3d`](/docs/kcl-std/patternLinear3d)
|
||||||
* [`patternTransform`](/docs/kcl-std/patternTransform)
|
* [`patternTransform`](/docs/kcl-std/functions/std-solid-patternTransform)
|
||||||
* [`shell`](/docs/kcl-std/functions/std-solid-shell)
|
* [`shell`](/docs/kcl-std/functions/std-solid-shell)
|
||||||
* [`subtract`](/docs/kcl-std/subtract)
|
* [`subtract`](/docs/kcl-std/subtract)
|
||||||
* [`union`](/docs/kcl-std/union)
|
* [`union`](/docs/kcl-std/union)
|
||||||
|
@ -30,7 +30,7 @@ This module contains functions for creating and manipulating sketches, and makin
|
|||||||
* [`line`](/docs/kcl-std/line)
|
* [`line`](/docs/kcl-std/line)
|
||||||
* [`loft`](/docs/kcl-std/loft)
|
* [`loft`](/docs/kcl-std/loft)
|
||||||
* [`patternCircular2d`](/docs/kcl-std/patternCircular2d)
|
* [`patternCircular2d`](/docs/kcl-std/patternCircular2d)
|
||||||
* [`patternTransform2d`](/docs/kcl-std/patternTransform2d)
|
* [`patternTransform2d`](/docs/kcl-std/functions/std-sketch-patternTransform2d)
|
||||||
* [`polygon`](/docs/kcl-std/polygon)
|
* [`polygon`](/docs/kcl-std/polygon)
|
||||||
* [`profileStart`](/docs/kcl-std/profileStart)
|
* [`profileStart`](/docs/kcl-std/profileStart)
|
||||||
* [`profileStartX`](/docs/kcl-std/profileStartX)
|
* [`profileStartX`](/docs/kcl-std/profileStartX)
|
||||||
|
@ -18,7 +18,7 @@ This module contains functions for modifying solids, e.g., by adding a fillet or
|
|||||||
* [`intersect`](/docs/kcl-std/intersect)
|
* [`intersect`](/docs/kcl-std/intersect)
|
||||||
* [`patternCircular3d`](/docs/kcl-std/patternCircular3d)
|
* [`patternCircular3d`](/docs/kcl-std/patternCircular3d)
|
||||||
* [`patternLinear3d`](/docs/kcl-std/patternLinear3d)
|
* [`patternLinear3d`](/docs/kcl-std/patternLinear3d)
|
||||||
* [`patternTransform`](/docs/kcl-std/patternTransform)
|
* [`patternTransform`](/docs/kcl-std/functions/std-solid-patternTransform)
|
||||||
* [`shell`](/docs/kcl-std/functions/std-solid-shell)
|
* [`shell`](/docs/kcl-std/functions/std-solid-shell)
|
||||||
* [`subtract`](/docs/kcl-std/subtract)
|
* [`subtract`](/docs/kcl-std/subtract)
|
||||||
* [`union`](/docs/kcl-std/union)
|
* [`union`](/docs/kcl-std/union)
|
||||||
|
@ -11,7 +11,7 @@ Contains frequently used constants, functions for interacting with the KittyCAD
|
|||||||
|
|
||||||
The standard library is organised into modules (listed below), but most things are always available in KCL programs.
|
The standard library is organised into modules (listed below), but most things are always available in KCL programs.
|
||||||
|
|
||||||
You might also want the [KCL language reference](/docs/kcl-lang) or the [KCL guide]().
|
You might also want the [KCL language reference](/docs/kcl-lang) or the [KCL guide](https://zoo.dev/docs/kcl-book/intro.html).
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
|
16124
docs/kcl-std/std.json
@ -235,6 +235,48 @@ extrude001 = extrude(sketch001, length = 5)`
|
|||||||
.first()
|
.first()
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('KCL errors with functions show hints for the entire backtrace', async ({
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
cmdBar,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
}) => {
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
const code = `fn check(@x) {
|
||||||
|
return assert(x, isGreaterThan = 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn middle(@x) {
|
||||||
|
return check(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
middle(1)
|
||||||
|
middle(0)
|
||||||
|
`
|
||||||
|
await test.step('Set the code with a KCL error', async () => {
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.replaceCode('', code)
|
||||||
|
})
|
||||||
|
// This shows all the diagnostics in a way that doesn't require the mouse
|
||||||
|
// pointer hovering over a coordinate, which would be brittle.
|
||||||
|
await test.step('Open CodeMirror diagnostics list', async () => {
|
||||||
|
// Ensure keyboard focus is in the editor.
|
||||||
|
await page.getByText('fn check(').click()
|
||||||
|
await page.keyboard.press('ControlOrMeta+Shift+M')
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
page.getByText(`assert failed: Expected 0 to be greater than 0 but it wasn't
|
||||||
|
check()
|
||||||
|
middle()`)
|
||||||
|
).toBeVisible()
|
||||||
|
// There should be one hint inside middle() and one at the top level.
|
||||||
|
await expect(page.getByText('Part of the error backtrace')).toHaveCount(2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
@ -45,15 +45,16 @@ test.describe('Command bar tests', () => {
|
|||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
commandName: 'Extrude',
|
commandName: 'Extrude',
|
||||||
currentArgKey: 'length',
|
currentArgKey: 'sketches',
|
||||||
currentArgValue: '5',
|
currentArgValue: '',
|
||||||
headerArguments: {
|
headerArguments: {
|
||||||
Profiles: '1 profile',
|
Profiles: '',
|
||||||
Length: '',
|
Length: '',
|
||||||
},
|
},
|
||||||
highlightedHeaderArg: 'length',
|
highlightedHeaderArg: 'Profiles',
|
||||||
})
|
})
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'review',
|
stage: 'review',
|
||||||
commandName: 'Extrude',
|
commandName: 'Extrude',
|
||||||
@ -684,4 +685,33 @@ c = 3 + a`
|
|||||||
highlightedHeaderArg: 'value',
|
highlightedHeaderArg: 'value',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Text-to-CAD command can be closed with escape while in prompt', async ({
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
await homePage.expectState({
|
||||||
|
projectCards: [],
|
||||||
|
sortBy: 'last-modified-desc',
|
||||||
|
})
|
||||||
|
await homePage.textToCadBtn.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
commandName: 'Text-to-CAD Create',
|
||||||
|
currentArgKey: 'prompt',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Method: 'New project',
|
||||||
|
NewProjectName: 'untitled',
|
||||||
|
Prompt: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'prompt',
|
||||||
|
})
|
||||||
|
await page.keyboard.press('Escape')
|
||||||
|
await cmdBar.toBeClosed()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'commandBarClosed',
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1134,6 +1134,7 @@ sketch001 = startSketchOn(XZ)
|
|||||||
// Wait for the selection to register (TODO: we need a definitive way to wait for this)
|
// Wait for the selection to register (TODO: we need a definitive way to wait for this)
|
||||||
await page.waitForTimeout(200)
|
await page.waitForTimeout(200)
|
||||||
await toolbar.extrudeButton.click()
|
await toolbar.extrudeButton.click()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
currentArgKey: 'length',
|
currentArgKey: 'length',
|
||||||
@ -1355,9 +1356,7 @@ sketch001 = startSketchOn(XZ)
|
|||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const projectLink = page.getByRole('link', { name: 'cube' })
|
const projectLink = page.getByRole('link', { name: 'cube' })
|
||||||
const gizmo = page.locator('[aria-label*=gizmo]')
|
const gizmo = page.locator('[aria-label*=gizmo]')
|
||||||
const resetCameraButton = page.getByRole('button', {
|
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
|
||||||
name: 'Reset view',
|
|
||||||
})
|
|
||||||
const locationToHaveColor = async (
|
const locationToHaveColor = async (
|
||||||
position: { x: number; y: number },
|
position: { x: number; y: number },
|
||||||
color: [number, number, number]
|
color: [number, number, number]
|
||||||
|
@ -238,6 +238,26 @@ test.describe('when using the file tree to', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
`create new folders and that doesn't trigger a navigation`,
|
||||||
|
{ tag: ['@electron', '@macos', '@windows'] },
|
||||||
|
async ({ page, homePage, scene, toolbar, cmdBar }) => {
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
await toolbar.openPane('files')
|
||||||
|
const { createNewFolder } = await getUtils(page, test)
|
||||||
|
|
||||||
|
await createNewFolder('folder')
|
||||||
|
|
||||||
|
await createNewFolder('folder.kcl')
|
||||||
|
|
||||||
|
await test.step(`Postcondition: folders are created and we didn't navigate`, async () => {
|
||||||
|
await toolbar.expectFileTreeState(['folder', 'folder.kcl', 'main.kcl'])
|
||||||
|
await expect(toolbar.fileName).toHaveText('main.kcl')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'deleting all files recreates a default main.kcl with no code',
|
'deleting all files recreates a default main.kcl with no code',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
|
@ -105,14 +105,19 @@ export class CmdBarFixture {
|
|||||||
expectState = async (expected: CmdBarSerialised) => {
|
expectState = async (expected: CmdBarSerialised) => {
|
||||||
return expect.poll(() => this._serialiseCmdBar()).toEqual(expected)
|
return expect.poll(() => this._serialiseCmdBar()).toEqual(expected)
|
||||||
}
|
}
|
||||||
/** The method will use buttons OR press enter randomly to progress the cmdbar,
|
/**
|
||||||
* this could have unexpected results depending on what's focused
|
* This method is used to progress the command bar to the next step, defaulting to clicking the next button.
|
||||||
*
|
* Optionally, with the `shouldUseKeyboard` parameter, it will hit `Enter` to progress.
|
||||||
* TODO: This method assumes the user has a valid input to the current stage,
|
* * TODO: This method assumes the user has a valid input to the current stage,
|
||||||
* and assumes we are past the `pickCommand` step.
|
* and assumes we are past the `pickCommand` step.
|
||||||
*/
|
*/
|
||||||
progressCmdBar = async (shouldFuzzProgressMethod = true) => {
|
progressCmdBar = async (shouldUseKeyboard = false) => {
|
||||||
await this.page.waitForTimeout(2000)
|
await this.page.waitForTimeout(2000)
|
||||||
|
if (shouldUseKeyboard) {
|
||||||
|
await this.page.keyboard.press('Enter')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const arrowButton = this.page.getByRole('button', {
|
const arrowButton = this.page.getByRole('button', {
|
||||||
name: 'arrow right Continue',
|
name: 'arrow right Continue',
|
||||||
})
|
})
|
||||||
@ -308,6 +313,11 @@ export class CmdBarFixture {
|
|||||||
await expect(this.cmdBarElement).toBeVisible({ timeout: 10_000 })
|
await expect(this.cmdBarElement).toBeVisible({ timeout: 10_000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async toBeClosed() {
|
||||||
|
// Check that the command bar is closed
|
||||||
|
await expect(this.cmdBarElement).not.toBeVisible({ timeout: 10_000 })
|
||||||
|
}
|
||||||
|
|
||||||
async expectArgValue(value: string) {
|
async expectArgValue(value: string) {
|
||||||
// Check the placeholder project name exists
|
// Check the placeholder project name exists
|
||||||
const actualArgument = await this.cmdBarElement
|
const actualArgument = await this.cmdBarElement
|
||||||
|
@ -26,6 +26,7 @@ export class HomePageFixture {
|
|||||||
sortByNameBtn!: Locator
|
sortByNameBtn!: Locator
|
||||||
appHeader!: Locator
|
appHeader!: Locator
|
||||||
tutorialBtn!: Locator
|
tutorialBtn!: Locator
|
||||||
|
textToCadBtn!: Locator
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
@ -47,6 +48,7 @@ export class HomePageFixture {
|
|||||||
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
|
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
|
||||||
this.appHeader = this.page.getByTestId('app-header')
|
this.appHeader = this.page.getByTestId('app-header')
|
||||||
this.tutorialBtn = this.page.getByTestId('home-tutorial-button')
|
this.tutorialBtn = this.page.getByTestId('home-tutorial-button')
|
||||||
|
this.textToCadBtn = this.page.getByTestId('home-text-to-cad')
|
||||||
}
|
}
|
||||||
|
|
||||||
private _serialiseSortBy = async (): Promise<
|
private _serialiseSortBy = async (): Promise<
|
||||||
|
@ -61,6 +61,7 @@ class MyAPIReporter implements Reporter {
|
|||||||
const payload = {
|
const payload = {
|
||||||
// Required information
|
// Required information
|
||||||
project: 'https://github.com/KittyCAD/modeling-app',
|
project: 'https://github.com/KittyCAD/modeling-app',
|
||||||
|
suite: process.env.CI_SUITE || 'e2e',
|
||||||
branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || '',
|
branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || '',
|
||||||
commit: process.env.CI_COMMIT_SHA || process.env.GITHUB_SHA || '',
|
commit: process.env.CI_COMMIT_SHA || process.env.GITHUB_SHA || '',
|
||||||
test: test.titlePath().slice(2).join(' › '),
|
test: test.titlePath().slice(2).join(' › '),
|
||||||
|
@ -70,22 +70,28 @@ test.describe('Point-and-click assemblies tests', () => {
|
|||||||
await test.step('Setup parts and expect empty assembly scene', async () => {
|
await test.step('Setup parts and expect empty assembly scene', async () => {
|
||||||
const projectName = 'assembly'
|
const projectName = 'assembly'
|
||||||
await context.folderSetupFn(async (dir) => {
|
await context.folderSetupFn(async (dir) => {
|
||||||
const bracketDir = path.join(dir, projectName)
|
const projDir = path.join(dir, projectName)
|
||||||
await fsp.mkdir(bracketDir, { recursive: true })
|
const nestedProjDir = path.join(dir, projectName, 'nested', 'twice')
|
||||||
|
await fsp.mkdir(projDir, { recursive: true })
|
||||||
|
await fsp.mkdir(nestedProjDir, { recursive: true })
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fsp.copyFile(
|
fsp.copyFile(
|
||||||
executorInputPath('cylinder.kcl'),
|
executorInputPath('cylinder.kcl'),
|
||||||
path.join(bracketDir, 'cylinder.kcl')
|
path.join(projDir, 'cylinder.kcl')
|
||||||
|
),
|
||||||
|
fsp.copyFile(
|
||||||
|
executorInputPath('cylinder.kcl'),
|
||||||
|
path.join(nestedProjDir, 'main.kcl')
|
||||||
),
|
),
|
||||||
fsp.copyFile(
|
fsp.copyFile(
|
||||||
executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
|
executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
|
||||||
path.join(bracketDir, 'bracket.kcl')
|
path.join(projDir, 'bracket.kcl')
|
||||||
),
|
),
|
||||||
fsp.copyFile(
|
fsp.copyFile(
|
||||||
testsInputPath('cube.step'),
|
testsInputPath('cube.step'),
|
||||||
path.join(bracketDir, 'cube.step')
|
path.join(projDir, 'cube.step')
|
||||||
),
|
),
|
||||||
fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''),
|
fsp.writeFile(path.join(projDir, 'main.kcl'), ''),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
@ -167,6 +173,25 @@ test.describe('Point-and-click assemblies tests', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByText('This file is already imported')
|
page.getByText('This file is already imported')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
await cmdBar.closeCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Insert a nested kcl part', async () => {
|
||||||
|
await insertPartIntoAssembly(
|
||||||
|
'nested/twice/main.kcl',
|
||||||
|
'main',
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
page
|
||||||
|
)
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await page.waitForTimeout(10000)
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
import "nested/twice/main.kcl" as main
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -74,6 +74,15 @@ test.describe('Point-and-click tests', () => {
|
|||||||
|
|
||||||
await test.step('do extrude flow and check extrude code is added to editor', async () => {
|
await test.step('do extrude flow and check extrude code is added to editor', async () => {
|
||||||
await toolbar.extrudeButton.click()
|
await toolbar.extrudeButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'sketches',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: { Profiles: '', Length: '' },
|
||||||
|
highlightedHeaderArg: 'Profiles',
|
||||||
|
commandName: 'Extrude',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
currentArgKey: 'length',
|
currentArgKey: 'length',
|
||||||
@ -1645,6 +1654,15 @@ sketch002 = startSketchOn(plane001)
|
|||||||
|
|
||||||
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
|
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
|
||||||
await toolbar.loftButton.click()
|
await toolbar.loftButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'sketches',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: { Profiles: '' },
|
||||||
|
highlightedHeaderArg: 'Profiles',
|
||||||
|
commandName: 'Loft',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'review',
|
stage: 'review',
|
||||||
headerArguments: { Profiles: '2 profiles' },
|
headerArguments: { Profiles: '2 profiles' },
|
||||||
@ -1855,7 +1873,11 @@ sketch002 = startSketchOn(XZ)
|
|||||||
},
|
},
|
||||||
stage: 'review',
|
stage: 'review',
|
||||||
})
|
})
|
||||||
await cmdBar.progressCmdBar()
|
// Confirm we can submit from the review step with just `Enter`
|
||||||
|
await cmdBar.progressCmdBar(true)
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'commandBarClosed',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
@ -1995,7 +2017,7 @@ profile001 = ${circleCode}`
|
|||||||
},
|
},
|
||||||
stage: 'review',
|
stage: 'review',
|
||||||
})
|
})
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar(true)
|
||||||
await editor.expectEditor.toContain(sweepDeclaration)
|
await editor.expectEditor.toContain(sweepDeclaration)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -2088,6 +2110,18 @@ extrude001 = extrude(sketch001, length = -12)
|
|||||||
await test.step(`Apply fillet to the preselected edge`, async () => {
|
await test.step(`Apply fillet to the preselected edge`, async () => {
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await toolbar.filletButton.click()
|
await toolbar.filletButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Fillet',
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Radius: '',
|
||||||
|
},
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
commandName: 'Fillet',
|
commandName: 'Fillet',
|
||||||
highlightedHeaderArg: 'radius',
|
highlightedHeaderArg: 'radius',
|
||||||
@ -2617,6 +2651,18 @@ extrude001 = extrude(profile001, length = 5)
|
|||||||
await test.step(`Apply fillet`, async () => {
|
await test.step(`Apply fillet`, async () => {
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await toolbar.filletButton.click()
|
await toolbar.filletButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Fillet',
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Radius: '',
|
||||||
|
},
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
commandName: 'Fillet',
|
commandName: 'Fillet',
|
||||||
highlightedHeaderArg: 'radius',
|
highlightedHeaderArg: 'radius',
|
||||||
@ -2722,6 +2768,19 @@ extrude001 = extrude(sketch001, length = -12)
|
|||||||
await test.step(`Apply chamfer to the preselected edge`, async () => {
|
await test.step(`Apply chamfer to the preselected edge`, async () => {
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await toolbar.chamferButton.click()
|
await toolbar.chamferButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Chamfer',
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Length: '',
|
||||||
|
},
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
commandName: 'Chamfer',
|
commandName: 'Chamfer',
|
||||||
highlightedHeaderArg: 'length',
|
highlightedHeaderArg: 'length',
|
||||||
@ -3205,6 +3264,8 @@ extrude001 = extrude(sketch001, length = 30)
|
|||||||
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
|
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
|
||||||
await toolbar.shellButton.click()
|
await toolbar.shellButton.click()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'review',
|
stage: 'review',
|
||||||
headerArguments: {
|
headerArguments: {
|
||||||
@ -3638,13 +3699,12 @@ tag=$rectangleSegmentC002,
|
|||||||
// revolve
|
// revolve
|
||||||
await editor.scrollToText(codeToSelection)
|
await editor.scrollToText(codeToSelection)
|
||||||
await page.getByText(codeToSelection).click()
|
await page.getByText(codeToSelection).click()
|
||||||
// Wait for the selection to register (TODO: we need a definitive way to wait for this)
|
|
||||||
await page.waitForTimeout(200)
|
|
||||||
await toolbar.revolveButton.click()
|
await toolbar.revolveButton.click()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
|
||||||
const newCodeToFind = `revolve001 = revolve(sketch002, angle = 360, axis = X)`
|
const newCodeToFind = `revolve001 = revolve(sketch002, angle = 360, axis = X)`
|
||||||
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
|
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
|
||||||
@ -4573,6 +4633,18 @@ path001 = startProfile(sketch001, at = [0, 0])
|
|||||||
|
|
||||||
await test.step('Go through command bar flow', async () => {
|
await test.step('Go through command bar flow', async () => {
|
||||||
await toolbar.extrudeButton.click()
|
await toolbar.extrudeButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'sketches',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Profiles: '',
|
||||||
|
Length: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'Profiles',
|
||||||
|
commandName: 'Extrude',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
currentArgKey: 'length',
|
currentArgKey: 'length',
|
||||||
@ -4655,6 +4727,19 @@ path001 = startProfile(sketch001, at = [0, 0])
|
|||||||
|
|
||||||
await test.step('Go through command bar flow', async () => {
|
await test.step('Go through command bar flow', async () => {
|
||||||
await toolbar.sweepButton.click()
|
await toolbar.sweepButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'sketches',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Profiles: '',
|
||||||
|
Path: '',
|
||||||
|
Sectional: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'Profiles',
|
||||||
|
commandName: 'Sweep',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
currentArgKey: 'path',
|
currentArgKey: 'path',
|
||||||
@ -4739,6 +4824,19 @@ path001 = startProfile(sketch001, at = [0, 0])
|
|||||||
await test.step('Go through command bar flow', async () => {
|
await test.step('Go through command bar flow', async () => {
|
||||||
await toolbar.closePane('code')
|
await toolbar.closePane('code')
|
||||||
await toolbar.revolveButton.click()
|
await toolbar.revolveButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'sketches',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Profiles: '',
|
||||||
|
AxisOrEdge: '',
|
||||||
|
Angle: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'Profiles',
|
||||||
|
commandName: 'Revolve',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
currentArgKey: 'axisOrEdge',
|
currentArgKey: 'axisOrEdge',
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
getPlaywrightDownloadDir,
|
getPlaywrightDownloadDir,
|
||||||
getUtils,
|
getUtils,
|
||||||
isOutOfViewInScrollContainer,
|
isOutOfViewInScrollContainer,
|
||||||
|
runningOnWindows,
|
||||||
} from '@e2e/playwright/test-utils'
|
} from '@e2e/playwright/test-utils'
|
||||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||||
|
|
||||||
@ -1979,7 +1980,6 @@ test(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Flaky
|
|
||||||
test(
|
test(
|
||||||
'Original project name persist after onboarding',
|
'Original project name persist after onboarding',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
@ -2064,3 +2064,55 @@ test(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'import from nested directory',
|
||||||
|
{ tag: ['@electron', '@windows', '@macos'] },
|
||||||
|
async ({ scene, cmdBar, context, page }) => {
|
||||||
|
await context.folderSetupFn(async (dir) => {
|
||||||
|
const bracketDir = path.join(dir, 'bracket')
|
||||||
|
await fsp.mkdir(bracketDir, { recursive: true })
|
||||||
|
const nestedDir = path.join(bracketDir, 'nested')
|
||||||
|
await fsp.mkdir(nestedDir, { recursive: true })
|
||||||
|
|
||||||
|
await fsp.copyFile(
|
||||||
|
executorInputPath('cylinder-inches.kcl'),
|
||||||
|
path.join(nestedDir, 'main.kcl')
|
||||||
|
)
|
||||||
|
await fsp.writeFile(
|
||||||
|
path.join(bracketDir, 'main.kcl'),
|
||||||
|
runningOnWindows()
|
||||||
|
? `import 'nested\\main.kcl' as thing\n\nthing`
|
||||||
|
: `import 'nested/main.kcl' as thing\n\nthing`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
const u = await getUtils(page)
|
||||||
|
|
||||||
|
const pointOnModel = { x: 630, y: 280 }
|
||||||
|
|
||||||
|
await test.step('Opening the bracket project should load the stream', async () => {
|
||||||
|
// expect to see the text bracket
|
||||||
|
await expect(page.getByText('bracket')).toBeVisible()
|
||||||
|
|
||||||
|
await page.getByText('bracket').click()
|
||||||
|
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).toBeEnabled({
|
||||||
|
timeout: 20_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// gray at this pixel means the stream has loaded in the most
|
||||||
|
// user way we can verify it (pixel color)
|
||||||
|
await expect
|
||||||
|
.poll(() => u.getGreatestPixDiff(pointOnModel, [125, 125, 125]), {
|
||||||
|
timeout: 10_000,
|
||||||
|
})
|
||||||
|
.toBeLessThan(15)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -1016,6 +1016,7 @@ profile001 = startProfile(sketch001, at = [${roundOff(scale * 69.6)}, ${roundOff
|
|||||||
|
|
||||||
// sketch selection should already have been made.
|
// sketch selection should already have been made.
|
||||||
// otherwise the cmdbar would be waiting for a selection.
|
// otherwise the cmdbar would be waiting for a selection.
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
currentArgKey: 'length',
|
currentArgKey: 'length',
|
||||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
@ -557,6 +557,14 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createNewFolder: async (name: string) => {
|
||||||
|
return test?.step(`Create a folder named ${name}`, async () => {
|
||||||
|
await page.getByTestId('create-folder-button').click()
|
||||||
|
await page.getByTestId('tree-input-field').fill(name)
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
cloneFile: async (name: string) => {
|
cloneFile: async (name: string) => {
|
||||||
return test?.step(`Cloning file '${name}'`, async () => {
|
return test?.step(`Cloning file '${name}'`, async () => {
|
||||||
await page
|
await page
|
||||||
|
@ -103,6 +103,8 @@ test.describe('Testing loading external models', () => {
|
|||||||
file: 'ball-bearing' + FILE_EXT,
|
file: 'ball-bearing' + FILE_EXT,
|
||||||
title: 'Ball Bearing',
|
title: 'Ball Bearing',
|
||||||
file1: 'ball-bearing-1' + FILE_EXT,
|
file1: 'ball-bearing-1' + FILE_EXT,
|
||||||
|
folderName: 'ball-bearing',
|
||||||
|
folderName1: 'ball-bearing-1',
|
||||||
}
|
}
|
||||||
const projectCard = page.getByRole('link', { name: 'bracket' })
|
const projectCard = page.getByRole('link', { name: 'bracket' })
|
||||||
const overwriteWarning = page.getByText(
|
const overwriteWarning = page.getByText(
|
||||||
@ -154,8 +156,10 @@ test.describe('Testing loading external models', () => {
|
|||||||
|
|
||||||
await test.step(`Ensure we made and opened a new file`, async () => {
|
await test.step(`Ensure we made and opened a new file`, async () => {
|
||||||
await editor.expectEditor.toContain('// ' + sampleOne.title)
|
await editor.expectEditor.toContain('// ' + sampleOne.title)
|
||||||
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
|
await expect(
|
||||||
await expect(projectMenuButton).toContainText(sampleOne.file)
|
page.getByTestId('file-tree-item').getByText(sampleOne.folderName)
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(projectMenuButton).toContainText('main.kcl')
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Load a KCL sample with the command palette`, async () => {
|
await test.step(`Load a KCL sample with the command palette`, async () => {
|
||||||
@ -169,8 +173,10 @@ test.describe('Testing loading external models', () => {
|
|||||||
|
|
||||||
await test.step(`Ensure we made and opened a new file with a unique name`, async () => {
|
await test.step(`Ensure we made and opened a new file with a unique name`, async () => {
|
||||||
await editor.expectEditor.toContain('// ' + sampleOne.title)
|
await editor.expectEditor.toContain('// ' + sampleOne.title)
|
||||||
await expect(newlyCreatedFile(sampleOne.file1)).toBeVisible()
|
await expect(
|
||||||
await expect(projectMenuButton).toContainText(sampleOne.file1)
|
page.getByTestId('file-tree-item').getByText(sampleOne.folderName1)
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(projectMenuButton).toContainText('main.kcl')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -984,12 +984,12 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
)
|
)
|
||||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||||
'2x2x2-cube.kcl'
|
'main.kcl'
|
||||||
)
|
)
|
||||||
|
|
||||||
await u.openFilePanel()
|
await u.openFilePanel()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
|
page.getByTestId('file-tree-item').getByText('2x2x2-cube')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -1184,13 +1184,13 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
)
|
)
|
||||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||||
'2x2x2-cube.kcl'
|
'main.kcl'
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check file is created
|
// Check file is created
|
||||||
await u.openFilePanel()
|
await u.openFilePanel()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
|
page.getByTestId('file-tree-item').getByText('2x2x2-cube')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -1476,13 +1476,13 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
)
|
)
|
||||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||||
'2x2x2-cube.kcl'
|
'main.kcl'
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check file is created
|
// Check file is created
|
||||||
await u.openFilePanel()
|
await u.openFilePanel()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
|
page.getByTestId('file-tree-item').getByText('2x2x2-cube')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId('file-tree-item').getByText('main.kcl')
|
page.getByTestId('file-tree-item').getByText('main.kcl')
|
||||||
|
@ -573,6 +573,7 @@ profile001 = startProfile(sketch002, at = [-12.34, 12.34])
|
|||||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
|
@ -37,6 +37,8 @@ When you submit a PR to add or modify KCL samples, images will be generated and
|
|||||||
[](bottle/main.kcl)
|
[](bottle/main.kcl)
|
||||||
#### [bracket](bracket/main.kcl) ([screenshot](screenshots/bracket.png))
|
#### [bracket](bracket/main.kcl) ([screenshot](screenshots/bracket.png))
|
||||||
[](bracket/main.kcl)
|
[](bracket/main.kcl)
|
||||||
|
#### [brake-rotor](brake-rotor/main.kcl) ([screenshot](screenshots/brake-rotor.png))
|
||||||
|
[](brake-rotor/main.kcl)
|
||||||
#### [car-wheel-assembly](car-wheel-assembly/main.kcl) ([screenshot](screenshots/car-wheel-assembly.png))
|
#### [car-wheel-assembly](car-wheel-assembly/main.kcl) ([screenshot](screenshots/car-wheel-assembly.png))
|
||||||
[](car-wheel-assembly/main.kcl)
|
[](car-wheel-assembly/main.kcl)
|
||||||
#### [cold-plate](cold-plate/main.kcl) ([screenshot](screenshots/cold-plate.png))
|
#### [cold-plate](cold-plate/main.kcl) ([screenshot](screenshots/cold-plate.png))
|
||||||
|
179
public/kcl-samples/brake-rotor/main.kcl
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
// A 320mm vented brake disc (rotor), with straight vanes, 30mm thick. The disc bell should accommodate 5 M12 wheel studs on a 114.3mm pitch circle diameter.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@settings(defaultLengthUnit = mm)
|
||||||
|
|
||||||
|
// Define parameters.
|
||||||
|
dDisc = 320
|
||||||
|
dPitchCircle = 114.3
|
||||||
|
dBore = 64
|
||||||
|
nStuds = 5
|
||||||
|
dStudDrilling = 12.5 // M12
|
||||||
|
hFrictionSurface = 60
|
||||||
|
tDiscHalf = 10
|
||||||
|
|
||||||
|
// Vent parameters.
|
||||||
|
tVent = 10
|
||||||
|
wVent = 6
|
||||||
|
rVentFillet = 2
|
||||||
|
nVentBosses = 36
|
||||||
|
|
||||||
|
// Drilling parameters.
|
||||||
|
dDrillDia = 6
|
||||||
|
aBase = 90
|
||||||
|
aSweep = 30
|
||||||
|
nArcs = 12
|
||||||
|
|
||||||
|
// Bell parameters.
|
||||||
|
aDraftBell = 5
|
||||||
|
tBell = 5 // Wall thickness.
|
||||||
|
hBellAboveDiscFace = 40
|
||||||
|
hBellSubflush = 4
|
||||||
|
wUndercut = 8
|
||||||
|
|
||||||
|
fn drillHole(activeSketch, t) {
|
||||||
|
// Sketch a vent hole at line parameter value t on an arc drawn across the disc surface.
|
||||||
|
rInner = dDisc / 2 - hFrictionSurface
|
||||||
|
rOuter = dDisc / 2
|
||||||
|
|
||||||
|
aStart = aBase
|
||||||
|
aEnd = aBase - aSweep
|
||||||
|
|
||||||
|
// Linear interpolation of radius.
|
||||||
|
rCurrent = rInner + t * (rOuter - rInner)
|
||||||
|
|
||||||
|
// Linear interpolation of angle.
|
||||||
|
aCurrent = aStart + t * (aEnd - aStart)
|
||||||
|
|
||||||
|
// Calculate position.
|
||||||
|
xCenter = rCurrent * cos(aCurrent)
|
||||||
|
yCenter = rCurrent * sin(aCurrent)
|
||||||
|
|
||||||
|
// Draw.
|
||||||
|
drillCircle = circle(activeSketch, center = [xCenter, yCenter], radius = dDrillDia / 2)
|
||||||
|
return drillCircle
|
||||||
|
}
|
||||||
|
|
||||||
|
fn createDiscHalf(plane, dDiscParam, hFrictionSurfaceParam, tDiscHalfParam) {
|
||||||
|
// Create a disc half with a vent hole pattern.
|
||||||
|
sketchFace = startSketchOn(plane)
|
||||||
|
profileFace = circle(sketchFace, center = [0, 0], radius = dDiscParam / 2)
|
||||||
|
|> subtract2d(tool = circle(sketchFace, center = [0, 0], radius = dDiscParam / 2 - hFrictionSurfaceParam))
|
||||||
|
|
||||||
|
// Create three circles at t = 0, 0.5, and 1
|
||||||
|
hole1 = drillHole(activeSketch = sketchFace, t = 0.2)
|
||||||
|
hole2 = drillHole(activeSketch = sketchFace, t = 0.5)
|
||||||
|
hole3 = drillHole(activeSketch = sketchFace, t = 0.8)
|
||||||
|
|
||||||
|
// Pattern and cut.
|
||||||
|
holes = patternCircular2d(
|
||||||
|
[hole1, hole2, hole3],
|
||||||
|
instances = nArcs,
|
||||||
|
center = [0, 0],
|
||||||
|
arcDegrees = 360,
|
||||||
|
rotateDuplicates = true,
|
||||||
|
)
|
||||||
|
profileDrilled = subtract2d(profileFace, tool = holes)
|
||||||
|
|
||||||
|
// Extrude.
|
||||||
|
discHalf = extrude(profileFace, length = tDiscHalfParam)
|
||||||
|
return discHalf
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Create inboard half.
|
||||||
|
discInboard = createDiscHalf(
|
||||||
|
plane = XY,
|
||||||
|
dDiscParam = dDisc,
|
||||||
|
hFrictionSurfaceParam = hFrictionSurface,
|
||||||
|
tDiscHalfParam = tDiscHalf,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create vents.
|
||||||
|
planeVent = offsetPlane(XY, offset = tDiscHalf)
|
||||||
|
sketchVent = startSketchOn(planeVent)
|
||||||
|
profileVent = startProfile(sketchVent, at = [-wVent, dDisc / 2])
|
||||||
|
|> angledLine(angle = 0, length = wVent, tag = $rectangleSegmentA001)
|
||||||
|
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = hFrictionSurface, tag = $seg02)
|
||||||
|
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg03)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|
||||||
|
|> close()
|
||||||
|
|
||||||
|
ventPad = extrude(profileVent, length = tVent)
|
||||||
|
|> fillet(
|
||||||
|
radius = rVentFillet,
|
||||||
|
tags = [
|
||||||
|
getCommonEdge(faces = [seg01, rectangleSegmentA001]),
|
||||||
|
getCommonEdge(faces = [seg02, rectangleSegmentA001]),
|
||||||
|
getCommonEdge(faces = [seg01, seg03]),
|
||||||
|
getCommonEdge(faces = [seg03, seg02])
|
||||||
|
],
|
||||||
|
)
|
||||||
|
ventSet = patternCircular3d(
|
||||||
|
ventPad,
|
||||||
|
instances = nVentBosses,
|
||||||
|
axis = [0, 0, 1],
|
||||||
|
center = [0, 0, tDiscHalf],
|
||||||
|
arcDegrees = 360,
|
||||||
|
rotateDuplicates = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create outboard half.
|
||||||
|
planeOutboard = offsetPlane(XY, offset = tDiscHalf + tVent)
|
||||||
|
discOutboard = createDiscHalf(
|
||||||
|
plane = planeOutboard,
|
||||||
|
dDiscParam = dDisc,
|
||||||
|
hFrictionSurfaceParam = hFrictionSurface,
|
||||||
|
tDiscHalfParam = tDiscHalf,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Now create bell.
|
||||||
|
rCenter = dDisc / 2 - hFrictionSurface - wUndercut
|
||||||
|
rBore = dBore / 2
|
||||||
|
lDraftExterior = hBellAboveDiscFace / tan(90 - aDraftBell)
|
||||||
|
lDraftInterior = (hBellAboveDiscFace - tBell) / tan(90 - aDraftBell)
|
||||||
|
|
||||||
|
// Inner and outer radius of outboard face of disc bell.
|
||||||
|
rOuter = rCenter - lDraftExterior - rBore
|
||||||
|
rInner = rOuter + lDraftExterior - (tBell + lDraftInterior)
|
||||||
|
|
||||||
|
sketchDiscBell = startSketchOn(-YZ)
|
||||||
|
bodyDiscBell = startProfile(
|
||||||
|
sketchDiscBell,
|
||||||
|
at = [
|
||||||
|
-dDisc / 2 + hFrictionSurface,
|
||||||
|
tDiscHalf * 2 + tVent
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|> arc(
|
||||||
|
%,
|
||||||
|
angleStart = -180,
|
||||||
|
angleEnd = 0,
|
||||||
|
radius = wUndercut / 2,
|
||||||
|
)
|
||||||
|
|> line(end = [lDraftExterior, hBellAboveDiscFace])
|
||||||
|
|> xLine(length = rOuter, tag = $seg04)
|
||||||
|
|> yLine(length = -tBell)
|
||||||
|
|> xLine(length = -rInner)
|
||||||
|
|> line(end = [-lDraftInterior, -hBellAboveDiscFace])
|
||||||
|
|> line(end = [0, -2]) // Wall thickness.
|
||||||
|
|> xLine(length = -1 * (tBell + wUndercut))
|
||||||
|
|> close(%)
|
||||||
|
|> revolve(axis = Y)
|
||||||
|
|
||||||
|
// Drill lug holes.
|
||||||
|
sketchLugs = startSketchOn(bodyDiscBell, face = seg04)
|
||||||
|
profileStud = circle(sketchLugs, center = [0, dPitchCircle / 2], radius = dStudDrilling / 2)
|
||||||
|
|> patternCircular2d(
|
||||||
|
%,
|
||||||
|
instances = nStuds,
|
||||||
|
center = [0, 0],
|
||||||
|
arcDegrees = 360,
|
||||||
|
rotateDuplicates = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
clearance = 2 // Some margin on negative extrude.
|
||||||
|
lugs = extrude(profileStud, length = -1 * (tBell + clearance))
|
@ -74,6 +74,16 @@
|
|||||||
"main.kcl"
|
"main.kcl"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"file": "main.kcl",
|
||||||
|
"pathFromProjectDirectoryToFirstFile": "brake-rotor/main.kcl",
|
||||||
|
"multipleFiles": false,
|
||||||
|
"title": "A 320mm vented brake disc (rotor), with straight vanes, 30mm thick. The disc bell should accommodate 5 M12 wheel studs on a 114.3mm pitch circle diameter.",
|
||||||
|
"description": "",
|
||||||
|
"files": [
|
||||||
|
"main.kcl"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"file": "main.kcl",
|
"file": "main.kcl",
|
||||||
"pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl",
|
"pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl",
|
||||||
|
@ -72,25 +72,25 @@ leftSpacerShape = boxModuleFn(width = leftSpacerWidth)
|
|||||||
|
|
||||||
// Module for power switch including front plate and red rocker button
|
// Module for power switch including front plate and red rocker button
|
||||||
switchPosition = leftSpacerPosition + leftSpacerWidth / 2 + moduleWidth / 2
|
switchPosition = leftSpacerPosition + leftSpacerWidth / 2 + moduleWidth / 2
|
||||||
swtichWidth = moduleWidth
|
switchWidth = moduleWidth
|
||||||
|
|
||||||
// Switch Body
|
// Switch Body
|
||||||
switchBody = boxModuleFn(width = moduleWidth)
|
switchBody = boxModuleFn(width = moduleWidth)
|
||||||
|
|
||||||
// Switch Plate
|
// Switch Plate
|
||||||
swtichPlateWidth = 20
|
switchPlateWidth = 20
|
||||||
switchPlateHeight = 30
|
switchPlateHeight = 30
|
||||||
switchPlateThickness = 3
|
switchPlateThickness = 3
|
||||||
switchPlateShape = startSketchOn(switchBody, face = END)
|
switchPlateShape = startSketchOn(switchBody, face = END)
|
||||||
|> startProfile(
|
|> startProfile(
|
||||||
%,
|
%,
|
||||||
at = [
|
at = [
|
||||||
-swtichPlateWidth / 2,
|
-switchPlateWidth / 2,
|
||||||
-switchPlateHeight / 2
|
-switchPlateHeight / 2
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|> yLine(length = switchPlateHeight)
|
|> yLine(length = switchPlateHeight)
|
||||||
|> xLine(length = swtichPlateWidth)
|
|> xLine(length = switchPlateWidth)
|
||||||
|> yLine(length = -switchPlateHeight)
|
|> yLine(length = -switchPlateHeight)
|
||||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
@ -104,8 +104,8 @@ switchPlateBody = extrude(switchPlateShape, length = switchPlateThickness)
|
|||||||
|
|
||||||
// Switch Button
|
// Switch Button
|
||||||
switchButtonHeight = 26
|
switchButtonHeight = 26
|
||||||
swtichButtonWidth = 15
|
switchButtonWidth = 15
|
||||||
switchButtonShape = startSketchOn(offsetPlane(-YZ, offset = -swtichButtonWidth / 2))
|
switchButtonShape = startSketchOn(offsetPlane(-YZ, offset = -switchButtonWidth / 2))
|
||||||
|> startProfile(
|
|> startProfile(
|
||||||
%,
|
%,
|
||||||
at = [
|
at = [
|
||||||
@ -121,7 +121,7 @@ switchButtonShape = startSketchOn(offsetPlane(-YZ, offset = -swtichButtonWidth /
|
|||||||
])
|
])
|
||||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
switchButtonBody = extrude(switchButtonShape, length = swtichButtonWidth)
|
switchButtonBody = extrude(switchButtonShape, length = switchButtonWidth)
|
||||||
|> translate(
|
|> translate(
|
||||||
%,
|
%,
|
||||||
x = switchPosition,
|
x = switchPosition,
|
||||||
@ -132,7 +132,7 @@ switchButtonBody = extrude(switchButtonShape, length = swtichButtonWidth)
|
|||||||
|
|
||||||
// Spacer between switch and plug modules for layout alignment
|
// Spacer between switch and plug modules for layout alignment
|
||||||
secondSpacerWidth = moduleWidth / 2
|
secondSpacerWidth = moduleWidth / 2
|
||||||
secondSpacerPosition = switchPosition + swtichWidth / 2 + secondSpacerWidth / 2
|
secondSpacerPosition = switchPosition + switchWidth / 2 + secondSpacerWidth / 2
|
||||||
secondSpacerBody = boxModuleFn(width = secondSpacerWidth)
|
secondSpacerBody = boxModuleFn(width = secondSpacerWidth)
|
||||||
|> translate(
|
|> translate(
|
||||||
%,
|
%,
|
||||||
|
@ -33,14 +33,9 @@ stemLoftProfile2 = startSketchOn(offsetPlane(XY, offset = 75))
|
|||||||
// Draw the third profile for the lofted femur
|
// Draw the third profile for the lofted femur
|
||||||
p3Z = 110
|
p3Z = 110
|
||||||
p3A = 25
|
p3A = 25
|
||||||
plane003 = {
|
|
||||||
origin = [0, 0.0, p3Z],
|
|
||||||
xAxis = [cos(p3A), 0, sin(p3A)],
|
|
||||||
yAxis = [0.0, 1.0, 0.0]
|
|
||||||
}
|
|
||||||
l3 = 32
|
l3 = 32
|
||||||
r3 = 4
|
r3 = 4
|
||||||
stemLoftProfile3 = startSketchOn(plane003)
|
stemLoftProfile3 = startSketchOn(XY)
|
||||||
|> startProfile(at = [-15.5, -l3 / 2])
|
|> startProfile(at = [-15.5, -l3 / 2])
|
||||||
|> yLine(length = l3, tag = $seg03)
|
|> yLine(length = l3, tag = $seg03)
|
||||||
|> tangentialArc(angle = -120, radius = r3)
|
|> tangentialArc(angle = -120, radius = r3)
|
||||||
@ -49,18 +44,14 @@ stemLoftProfile3 = startSketchOn(plane003)
|
|||||||
|> angledLine(angle = 30, length = -segLen(seg03))
|
|> angledLine(angle = 30, length = -segLen(seg03))
|
||||||
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
|
|> translate(z = p3Z)
|
||||||
|
|> rotate(pitch = -p3A)
|
||||||
// Draw the fourth profile for the lofted femur
|
// Draw the fourth profile for the lofted femur
|
||||||
p4Z = 130
|
p4Z = 130
|
||||||
p4A = 36.5
|
p4A = 36.5
|
||||||
plane004 = {
|
|
||||||
origin = [0, 0.0, p4Z],
|
|
||||||
xAxis = [cos(p4A), 0, sin(p4A)],
|
|
||||||
yAxis = [0.0, 1.0, 0.0]
|
|
||||||
}
|
|
||||||
l4 = 16
|
l4 = 16
|
||||||
r4 = 5
|
r4 = 5
|
||||||
stemLoftProfile4 = startSketchOn(plane004)
|
stemLoftProfile4 = startSketchOn(XY)
|
||||||
|> startProfile(at = [-23, -l4 / 2])
|
|> startProfile(at = [-23, -l4 / 2])
|
||||||
|> yLine(length = l4, tag = $seg04)
|
|> yLine(length = l4, tag = $seg04)
|
||||||
|> tangentialArc(angle = -120, radius = r4)
|
|> tangentialArc(angle = -120, radius = r4)
|
||||||
@ -69,18 +60,14 @@ stemLoftProfile4 = startSketchOn(plane004)
|
|||||||
|> angledLine(angle = 30, length = -segLen(seg04))
|
|> angledLine(angle = 30, length = -segLen(seg04))
|
||||||
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
|
|> translate(z = p4Z)
|
||||||
|
|> rotate(pitch = -p4A)
|
||||||
// Draw the first profile for the femoral stem
|
// Draw the first profile for the femoral stem
|
||||||
p5Z = 140
|
p5Z = 140
|
||||||
p5A = 36.5
|
p5A = 36.5
|
||||||
plane005 = {
|
|
||||||
origin = [0, 0.0, p5Z],
|
|
||||||
xAxis = [cos(p5A), 0, sin(p5A)],
|
|
||||||
yAxis = [0.0, 1.0, 0.0]
|
|
||||||
}
|
|
||||||
l5 = 1.6
|
l5 = 1.6
|
||||||
r5 = 1.6
|
r5 = 1.6
|
||||||
stemLoftProfile5 = startSketchOn(plane005)
|
stemLoftProfile5 = startSketchOn(XY)
|
||||||
|> startProfile(at = [-19.5, -l5 / 2])
|
|> startProfile(at = [-19.5, -l5 / 2])
|
||||||
|> yLine(length = l5, tag = $seg05)
|
|> yLine(length = l5, tag = $seg05)
|
||||||
|> tangentialArc(angle = -120, radius = r5)
|
|> tangentialArc(angle = -120, radius = r5)
|
||||||
@ -89,18 +76,14 @@ stemLoftProfile5 = startSketchOn(plane005)
|
|||||||
|> angledLine(angle = 30, length = -segLen(seg05))
|
|> angledLine(angle = 30, length = -segLen(seg05))
|
||||||
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
|
|> translate(z = p5Z)
|
||||||
|
|> rotate(pitch = -p5A)
|
||||||
// Draw the second profile for the femoral stem
|
// Draw the second profile for the femoral stem
|
||||||
p6Z = 145
|
p6Z = 145
|
||||||
p6A = 36.5
|
p6A = 36.5
|
||||||
plane006 = {
|
|
||||||
origin = [0, 0.0, p6Z],
|
|
||||||
xAxis = [cos(p6A), 0, sin(p6A)],
|
|
||||||
yAxis = [0.0, 1.0, 0.0]
|
|
||||||
}
|
|
||||||
l6 = 1
|
l6 = 1
|
||||||
r6 = 3
|
r6 = 3
|
||||||
stemLoftProfile6 = startSketchOn(plane006)
|
stemLoftProfile6 = startSketchOn(XY)
|
||||||
|> startProfile(at = [-23.4, -l6 / 2])
|
|> startProfile(at = [-23.4, -l6 / 2])
|
||||||
|> yLine(length = l6, tag = $seg06)
|
|> yLine(length = l6, tag = $seg06)
|
||||||
|> tangentialArc(angle = -120, radius = r6)
|
|> tangentialArc(angle = -120, radius = r6)
|
||||||
@ -109,27 +92,24 @@ stemLoftProfile6 = startSketchOn(plane006)
|
|||||||
|> angledLine(angle = 30, length = -segLen(seg06))
|
|> angledLine(angle = 30, length = -segLen(seg06))
|
||||||
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
|
|> translate(z = p6Z)
|
||||||
// Draw the third profile for the femoral stem
|
|> rotate(pitch = -p6A)
|
||||||
stemTab = clone(stemLoftProfile6)
|
|
||||||
|> extrude(%, length = 6)
|
|
||||||
|
|
||||||
// Loft the femur using all profiles in sequence
|
// Loft the femur using all profiles in sequence
|
||||||
|
|
||||||
femur = loft([
|
femur = loft([
|
||||||
stemLoftProfile1,
|
stemLoftProfile1,
|
||||||
stemLoftProfile2,
|
stemLoftProfile2,
|
||||||
stemLoftProfile3,
|
stemLoftProfile3,
|
||||||
stemLoftProfile4
|
stemLoftProfile4
|
||||||
])
|
])
|
||||||
|
|
||||||
// Loft the femoral stem
|
// Loft the femoral stem
|
||||||
femoralStem = loft([
|
femoralStem = loft([
|
||||||
clone(stemLoftProfile4),
|
clone(stemLoftProfile4),
|
||||||
stemLoftProfile5,
|
stemLoftProfile5,
|
||||||
stemLoftProfile6
|
clone(stemLoftProfile6)
|
||||||
])
|
])
|
||||||
|
// Draw the third profile for the femoral stem
|
||||||
|
stemTab = stemLoftProfile6
|
||||||
|
|> extrude(length = 6)
|
||||||
// Revolve a hollow socket to represent the femoral head
|
// Revolve a hollow socket to represent the femoral head
|
||||||
femoralHead = startSketchOn(XZ)
|
femoralHead = startSketchOn(XZ)
|
||||||
|> startProfile(at = [4, 0])
|
|> startProfile(at = [4, 0])
|
||||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
BIN
public/kcl-samples/screenshots/brake-rotor.png
Normal file
After Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
@ -53,8 +53,8 @@ baseSlab = boxFn(plane = XY, width = slabWidth, height = -baseThickness)
|
|||||||
|> appearance(%, color = "#dbd7d2")
|
|> appearance(%, color = "#dbd7d2")
|
||||||
|
|
||||||
// Create ground platform beneath the base
|
// Create ground platform beneath the base
|
||||||
goundSize = 50
|
groundSize = 50
|
||||||
groundBody = boxFn(plane = offsetPlane(XY, offset = -baseThickness), width = goundSize, height = -5)
|
groundBody = boxFn(plane = offsetPlane(XY, offset = -baseThickness), width = groundSize, height = -5)
|
||||||
|> appearance(%, color = "#3a3631")
|
|> appearance(%, color = "#3a3631")
|
||||||
|
|
||||||
// Create a single slab with handrail height to be reused with pattern
|
// Create a single slab with handrail height to be reused with pattern
|
||||||
|
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB |
20
rust/Cargo.lock
generated
@ -1815,7 +1815,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-bumper"
|
name = "kcl-bumper"
|
||||||
version = "0.1.74"
|
version = "0.1.75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@ -1826,7 +1826,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-derive-docs"
|
name = "kcl-derive-docs"
|
||||||
version = "0.1.74"
|
version = "0.1.75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Inflector",
|
"Inflector",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -1845,7 +1845,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-directory-test-macro"
|
name = "kcl-directory-test-macro"
|
||||||
version = "0.1.74"
|
version = "0.1.75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -1855,7 +1855,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-language-server"
|
name = "kcl-language-server"
|
||||||
version = "0.2.74"
|
version = "0.2.75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@ -1876,7 +1876,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-language-server-release"
|
name = "kcl-language-server-release"
|
||||||
version = "0.1.74"
|
version = "0.1.75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@ -1896,7 +1896,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
version = "0.2.74"
|
version = "0.2.75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"approx 0.5.1",
|
"approx 0.5.1",
|
||||||
@ -1973,7 +1973,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-python-bindings"
|
name = "kcl-python-bindings"
|
||||||
version = "0.3.74"
|
version = "0.3.75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"kcl-lib",
|
"kcl-lib",
|
||||||
@ -1988,7 +1988,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-test-server"
|
name = "kcl-test-server"
|
||||||
version = "0.1.74"
|
version = "0.1.75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"hyper 0.14.32",
|
"hyper 0.14.32",
|
||||||
@ -2001,7 +2001,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-to-core"
|
name = "kcl-to-core"
|
||||||
version = "0.1.74"
|
version = "0.1.75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -2015,7 +2015,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-wasm-lib"
|
name = "kcl-wasm-lib"
|
||||||
version = "0.1.74"
|
version = "0.1.75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bson",
|
"bson",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "kcl-bumper"
|
name = "kcl-bumper"
|
||||||
version = "0.1.74"
|
version = "0.1.75"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://github.com/KittyCAD/modeling-api"
|
repository = "https://github.com/KittyCAD/modeling-api"
|
||||||
rust-version = "1.76"
|
rust-version = "1.76"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-derive-docs"
|
name = "kcl-derive-docs"
|
||||||
description = "A tool for generating documentation from Rust derive macros"
|
description = "A tool for generating documentation from Rust derive macros"
|
||||||
version = "0.1.74"
|
version = "0.1.75"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-directory-test-macro"
|
name = "kcl-directory-test-macro"
|
||||||
description = "A tool for generating tests from a directory of kcl files"
|
description = "A tool for generating tests from a directory of kcl files"
|
||||||
version = "0.1.74"
|
version = "0.1.75"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-language-server-release"
|
name = "kcl-language-server-release"
|
||||||
version = "0.1.74"
|
version = "0.1.75"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
||||||
publish = false
|
publish = false
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
name = "kcl-language-server"
|
name = "kcl-language-server"
|
||||||
description = "A language server for KCL."
|
description = "A language server for KCL."
|
||||||
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
||||||
version = "0.2.74"
|
version = "0.2.75"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
description = "KittyCAD Language implementation and tools"
|
description = "KittyCAD Language implementation and tools"
|
||||||
version = "0.2.74"
|
version = "0.2.75"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -2,7 +2,7 @@ mod cache;
|
|||||||
|
|
||||||
use kcl_lib::{
|
use kcl_lib::{
|
||||||
test_server::{execute_and_export_step, execute_and_snapshot, execute_and_snapshot_no_auth},
|
test_server::{execute_and_export_step, execute_and_snapshot, execute_and_snapshot_no_auth},
|
||||||
ExecError,
|
BacktraceItem, ExecError, ModuleId, SourceRange,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The minimum permissible difference between asserted twenty-twenty images.
|
/// The minimum permissible difference between asserted twenty-twenty images.
|
||||||
@ -441,10 +441,15 @@ async fn kcl_test_import_file_doesnt_exist() {
|
|||||||
model = cube"#;
|
model = cube"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "File `thing.obj` does not exist.");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([0, 18, 0])], message: "File `thing.obj` does not exist." }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(0, 18, ModuleId::default()),
|
||||||
|
fn_name: None,
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,10 +524,18 @@ import 'e2e/executor/inputs/cube.gltf'
|
|||||||
model = cube"#;
|
model = cube"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.message(),
|
||||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([32, 70, 0])], message: "The given format does not match the file extension. Expected: `gltf`, Given: `obj`" }"#
|
"The given format does not match the file extension. Expected: `gltf`, Given: `obj`"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
err.backtrace(),
|
||||||
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(32, 70, ModuleId::default()),
|
||||||
|
fn_name: None,
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1666,10 +1679,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have an x constrained angle of 90 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 111, 0])], message: "Cannot have an x constrained angle of 90 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(70, 111, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1686,10 +1704,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have an x constrained angle of 270 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 112, 0])], message: "Cannot have an x constrained angle of 270 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(70, 112, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1706,10 +1729,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have a y constrained angle of 0 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 110, 0])], message: "Cannot have a y constrained angle of 0 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(70, 110, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1726,10 +1754,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 112, 0])], message: "Cannot have a y constrained angle of 180 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(70, 112, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1746,10 +1779,15 @@ extrusion = extrude(sketch001, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have an x constrained angle of 90 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([66, 116, 0])], message: "Cannot have an x constrained angle of 90 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(66, 116, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1757,7 +1795,7 @@ extrusion = extrude(sketch001, length = 10)
|
|||||||
async fn kcl_test_angled_line_of_x_length_270() {
|
async fn kcl_test_angled_line_of_x_length_270() {
|
||||||
let code = r#"sketch001 = startSketchOn(XZ)
|
let code = r#"sketch001 = startSketchOn(XZ)
|
||||||
|> startProfile(at = [0, 0])
|
|> startProfile(at = [0, 0])
|
||||||
|> angledLine(angle = 90, lengthX = 90, tag = $edge1)
|
|> angledLine(angle = 270, lengthX = 90, tag = $edge1)
|
||||||
|> angledLine(angle = -15, lengthX = -15, tag = $edge2)
|
|> angledLine(angle = -15, lengthX = -15, tag = $edge2)
|
||||||
|> line(end = [0, -5])
|
|> line(end = [0, -5])
|
||||||
|> close(tag = $edge3)
|
|> close(tag = $edge3)
|
||||||
@ -1766,10 +1804,15 @@ extrusion = extrude(sketch001, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have an x constrained angle of 270 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([66, 116, 0])], message: "Cannot have an x constrained angle of 90 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(66, 117, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1788,10 +1831,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have a y constrained angle of 0 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([95, 130, 0])], message: "Cannot have a y constrained angle of 0 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(95, 130, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1810,10 +1858,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([95, 132, 0])], message: "Cannot have a y constrained angle of 180 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(95, 132, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1832,10 +1885,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([95, 133, 0])], message: "Cannot have a y constrained angle of 180 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(95, 133, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1849,10 +1907,31 @@ someFunction('INVALID')
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.message(),
|
||||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([46, 55, 0]), SourceRange([60, 83, 0])], message: "This function expected the input argument to be Solid or Plane but it's actually of type string" }"#
|
"This function expected the input argument to be Solid or Plane but it's actually of type string"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
err.source_ranges(),
|
||||||
|
vec![
|
||||||
|
SourceRange::new(46, 55, ModuleId::default()),
|
||||||
|
SourceRange::new(60, 83, ModuleId::default()),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
err.backtrace(),
|
||||||
|
vec![
|
||||||
|
BacktraceItem {
|
||||||
|
source_range: SourceRange::new(46, 55, ModuleId::default()),
|
||||||
|
fn_name: Some("someFunction".to_owned()),
|
||||||
|
},
|
||||||
|
BacktraceItem {
|
||||||
|
source_range: SourceRange::new(60, 83, ModuleId::default()),
|
||||||
|
fn_name: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1873,12 +1952,14 @@ async fn kcl_test_error_no_auth_websocket() {
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot_no_auth(code, None).await;
|
let result = execute_and_snapshot_no_auth(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
assert!(result
|
let err = err.as_kcl_error().unwrap();
|
||||||
.err()
|
assert!(
|
||||||
.unwrap()
|
err.message()
|
||||||
.to_string()
|
.contains("Please send the following object over this websocket"),
|
||||||
.contains("Please send the following object over this websocket"));
|
"actual: {}",
|
||||||
|
err.message()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@ -631,6 +631,8 @@ impl FnData {
|
|||||||
return "clone(${0:part001})".to_owned();
|
return "clone(${0:part001})".to_owned();
|
||||||
} else if self.name == "hole" {
|
} else if self.name == "hole" {
|
||||||
return "hole(${0:holeSketch}, ${1:%})".to_owned();
|
return "hole(${0:holeSketch}, ${1:%})".to_owned();
|
||||||
|
} else if self.name == "circle" {
|
||||||
|
return "circle(center = [${0:3.14}, ${1:3.14}], diameter = ${2:3.14})".to_owned();
|
||||||
}
|
}
|
||||||
let mut args = Vec::new();
|
let mut args = Vec::new();
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
|
@ -1018,7 +1018,7 @@ mod tests {
|
|||||||
let snippet = circle_fn.to_autocomplete_snippet();
|
let snippet = circle_fn.to_autocomplete_snippet();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snippet,
|
snippet,
|
||||||
r#"circle(center = [${0:3.14}, ${1:3.14}], radius = ${2:3.14})"#
|
r#"circle(center = [${0:3.14}, ${1:3.14}], diameter = ${2:3.14})"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,12 +439,7 @@ impl EngineManager for EngineConnection {
|
|||||||
request_sent: tx,
|
request_sent: tx,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| KclError::Engine(KclErrorDetails::new(format!("Failed to send debug: {}", e), vec![])))?;
|
||||||
KclError::Engine(KclErrorDetails {
|
|
||||||
message: format!("Failed to send debug: {}", e),
|
|
||||||
source_ranges: vec![],
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let _ = rx.await;
|
let _ = rx.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -479,25 +474,25 @@ impl EngineManager for EngineConnection {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to send modeling command: {}", e),
|
format!("Failed to send modeling command: {}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Wait for the request to be sent.
|
// Wait for the request to be sent.
|
||||||
rx.await
|
rx.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("could not send request to the engine actor: {e}"),
|
format!("could not send request to the engine actor: {e}"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?
|
})?
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("could not send request to the engine: {e}"),
|
format!("could not send request to the engine: {e}"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -521,15 +516,15 @@ impl EngineManager for EngineConnection {
|
|||||||
// Check if we have any pending errors.
|
// Check if we have any pending errors.
|
||||||
let pe = self.pending_errors.read().await;
|
let pe = self.pending_errors.read().await;
|
||||||
if !pe.is_empty() {
|
if !pe.is_empty() {
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: pe.join(", ").to_string(),
|
pe.join(", ").to_string(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
} else {
|
} else {
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: "Modeling command failed: websocket closed early".to_string(),
|
"Modeling command failed: websocket closed early".to_string(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -548,10 +543,10 @@ impl EngineManager for EngineConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(KclError::Engine(KclErrorDetails {
|
Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Modeling command timed out `{}`", id),
|
format!("Modeling command timed out `{}`", id),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_session_data(&self) -> Option<ModelingSessionData> {
|
async fn get_session_data(&self) -> Option<ModelingSessionData> {
|
||||||
|
@ -147,32 +147,27 @@ impl EngineConnection {
|
|||||||
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
|
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
|
||||||
) -> Result<(), KclError> {
|
) -> Result<(), KclError> {
|
||||||
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
|
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to serialize source range: {:?}", e),
|
format!("Failed to serialize source range: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
|
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to serialize modeling command: {:?}", e),
|
format!("Failed to serialize modeling command: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
|
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to serialize id to source range: {:?}", e),
|
format!("Failed to serialize id to source range: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.manager
|
self.manager
|
||||||
.fire_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
|
.fire_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
|
||||||
.map_err(|e| {
|
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
|
||||||
KclError::Engine(KclErrorDetails {
|
|
||||||
message: e.to_string().into(),
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -185,33 +180,28 @@ impl EngineConnection {
|
|||||||
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
|
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
|
||||||
) -> Result<WebSocketResponse, KclError> {
|
) -> Result<WebSocketResponse, KclError> {
|
||||||
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
|
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to serialize source range: {:?}", e),
|
format!("Failed to serialize source range: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
|
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to serialize modeling command: {:?}", e),
|
format!("Failed to serialize modeling command: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
|
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to serialize id to source range: {:?}", e),
|
format!("Failed to serialize id to source range: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let promise = self
|
let promise = self
|
||||||
.manager
|
.manager
|
||||||
.send_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
|
.send_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
|
||||||
.map_err(|e| {
|
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
|
||||||
KclError::Engine(KclErrorDetails {
|
|
||||||
message: e.to_string().into(),
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| {
|
let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| {
|
||||||
// Try to parse the error as an engine error.
|
// Try to parse the error as an engine error.
|
||||||
@ -219,53 +209,52 @@ impl EngineConnection {
|
|||||||
if let Ok(kittycad_modeling_cmds::websocket::FailureWebSocketResponse { errors, .. }) =
|
if let Ok(kittycad_modeling_cmds::websocket::FailureWebSocketResponse { errors, .. }) =
|
||||||
serde_json::from_str(&err_str)
|
serde_json::from_str(&err_str)
|
||||||
{
|
{
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
|
errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
} else if let Ok(data) =
|
} else if let Ok(data) =
|
||||||
serde_json::from_str::<Vec<kittycad_modeling_cmds::websocket::FailureWebSocketResponse>>(&err_str)
|
serde_json::from_str::<Vec<kittycad_modeling_cmds::websocket::FailureWebSocketResponse>>(&err_str)
|
||||||
{
|
{
|
||||||
if let Some(data) = data.first() {
|
if let Some(data) = data.first() {
|
||||||
// It could also be an array of responses.
|
// It could also be an array of responses.
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: data
|
data.errors
|
||||||
.errors
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| e.message.clone())
|
.map(|e| e.message.clone())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
} else {
|
} else {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: "Received empty response from engine".into(),
|
"Received empty response from engine".into(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to wait for promise from send modeling command: {:?}", e),
|
format!("Failed to wait for promise from send modeling command: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if value.is_null() || value.is_undefined() {
|
if value.is_null() || value.is_undefined() {
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: "Received null or undefined response from engine".into(),
|
"Received null or undefined response from engine".into(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert JsValue to a Uint8Array
|
// Convert JsValue to a Uint8Array
|
||||||
let data = js_sys::Uint8Array::from(value);
|
let data = js_sys::Uint8Array::from(value);
|
||||||
|
|
||||||
let ws_result: WebSocketResponse = bson::from_slice(&data.to_vec()).map_err(|e| {
|
let ws_result: WebSocketResponse = bson::from_slice(&data.to_vec()).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to deserialize bson response from engine: {:?}", e),
|
format!("Failed to deserialize bson response from engine: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(ws_result)
|
Ok(ws_result)
|
||||||
@ -316,18 +305,16 @@ impl crate::engine::EngineManager for EngineConnection {
|
|||||||
*self.default_planes.write().await = Some(new_planes);
|
*self.default_planes.write().await = Some(new_planes);
|
||||||
|
|
||||||
// Start a new session.
|
// Start a new session.
|
||||||
let promise = self.manager.start_new_session().map_err(|e| {
|
let promise = self
|
||||||
KclError::Engine(KclErrorDetails {
|
.manager
|
||||||
message: e.to_string().into(),
|
.start_new_session()
|
||||||
source_ranges: vec![source_range],
|
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
crate::wasm::JsFuture::from(promise).await.map_err(|e| {
|
crate::wasm::JsFuture::from(promise).await.map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to wait for promise from start new session: {:?}", e),
|
format!("Failed to wait for promise from start new session: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -276,10 +276,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
{
|
{
|
||||||
let duration = instant::Duration::from_millis(1);
|
let duration = instant::Duration::from_millis(1);
|
||||||
wasm_timer::Delay::new(duration).await.map_err(|err| {
|
wasm_timer::Delay::new(duration).await.map_err(|err| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("Failed to sleep: {:?}", err),
|
format!("Failed to sleep: {:?}", err),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@ -293,10 +293,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
return Ok(response);
|
return Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(KclError::Engine(KclErrorDetails {
|
Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: "async command timed out".to_string(),
|
"async command timed out".to_string(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure ALL async commands have been completed.
|
/// Ensure ALL async commands have been completed.
|
||||||
@ -547,10 +547,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
id_to_source_range.insert(Uuid::from(*cmd_id), *range);
|
id_to_source_range.insert(Uuid::from(*cmd_id), *range);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("The request is not a modeling command: {:?}", req),
|
format!("The request is not a modeling command: {:?}", req),
|
||||||
source_ranges: vec![*range],
|
vec![*range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -595,10 +595,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
|
self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
|
||||||
} else {
|
} else {
|
||||||
// We should never get here.
|
// We should never get here.
|
||||||
Err(KclError::Engine(KclErrorDetails {
|
Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to get batch response: {:?}", response),
|
format!("Failed to get batch response: {:?}", response),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
|
WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
|
||||||
@ -610,20 +610,20 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
// request so we need the original request source range in case the engine returns
|
// request so we need the original request source range in case the engine returns
|
||||||
// an error.
|
// an error.
|
||||||
let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
|
let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to get source range for command ID: {:?}", cmd_id),
|
format!("Failed to get source range for command ID: {:?}", cmd_id),
|
||||||
source_ranges: vec![],
|
vec![],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let ws_resp = self
|
let ws_resp = self
|
||||||
.inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
|
.inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
|
||||||
.await?;
|
.await?;
|
||||||
self.parse_websocket_response(ws_resp, source_range)
|
self.parse_websocket_response(ws_resp, source_range)
|
||||||
}
|
}
|
||||||
_ => Err(KclError::Engine(KclErrorDetails {
|
_ => Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("The final request is not a modeling command: {:?}", final_req),
|
format!("The final request is not a modeling command: {:?}", final_req),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -729,10 +729,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
for (name, plane_id, color) in plane_settings {
|
for (name, plane_id, color) in plane_settings {
|
||||||
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
|
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
|
||||||
// We should never get here.
|
// We should never get here.
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to get default plane info for: {:?}", name),
|
format!("Failed to get default plane info for: {:?}", name),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
planes.insert(
|
planes.insert(
|
||||||
name,
|
name,
|
||||||
@ -763,15 +763,14 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
WebSocketResponse::Success(success) => Ok(success.resp),
|
WebSocketResponse::Success(success) => Ok(success.resp),
|
||||||
WebSocketResponse::Failure(fail) => {
|
WebSocketResponse::Failure(fail) => {
|
||||||
let _request_id = fail.request_id;
|
let _request_id = fail.request_id;
|
||||||
Err(KclError::Engine(KclErrorDetails {
|
Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: fail
|
fail.errors
|
||||||
.errors
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| e.message.clone())
|
.map(|e| e.message.clone())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -806,25 +805,25 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
BatchResponse::Failure { errors } => {
|
BatchResponse::Failure { errors } => {
|
||||||
// Get the source range for the command.
|
// Get the source range for the command.
|
||||||
let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
|
let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to get source range for command ID: {:?}", cmd_id),
|
format!("Failed to get source range for command ID: {:?}", cmd_id),
|
||||||
source_ranges: vec![],
|
vec![],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
|
errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return an error that we did not get an error or the response we wanted.
|
// Return an error that we did not get an error or the response we wanted.
|
||||||
// This should never happen but who knows.
|
// This should never happen but who knows.
|
||||||
Err(KclError::Engine(KclErrorDetails {
|
Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to find response for command ID: {:?}", id),
|
format!("Failed to find response for command ID: {:?}", id),
|
||||||
source_ranges: vec![],
|
vec![],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn modify_grid(
|
async fn modify_grid(
|
||||||
|
@ -380,20 +380,39 @@ impl miette::Diagnostic for Report {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
|
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
#[error("{message}")]
|
#[error("{message}")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct KclErrorDetails {
|
pub struct KclErrorDetails {
|
||||||
#[serde(rename = "sourceRanges")]
|
|
||||||
#[label(collection, "Errors")]
|
#[label(collection, "Errors")]
|
||||||
pub source_ranges: Vec<SourceRange>,
|
pub source_ranges: Vec<SourceRange>,
|
||||||
|
pub backtrace: Vec<BacktraceItem>,
|
||||||
#[serde(rename = "msg")]
|
#[serde(rename = "msg")]
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl KclErrorDetails {
|
||||||
|
pub fn new(message: String, source_ranges: Vec<SourceRange>) -> KclErrorDetails {
|
||||||
|
let backtrace = source_ranges
|
||||||
|
.iter()
|
||||||
|
.map(|s| BacktraceItem {
|
||||||
|
source_range: *s,
|
||||||
|
fn_name: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
KclErrorDetails {
|
||||||
|
source_ranges,
|
||||||
|
backtrace,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl KclError {
|
impl KclError {
|
||||||
pub fn internal(message: String) -> KclError {
|
pub fn internal(message: String) -> KclError {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails {
|
||||||
source_ranges: Default::default(),
|
source_ranges: Default::default(),
|
||||||
|
backtrace: Default::default(),
|
||||||
message,
|
message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -455,45 +474,122 @@ impl KclError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn backtrace(&self) -> Vec<BacktraceItem> {
|
||||||
|
match self {
|
||||||
|
KclError::Lexical(e)
|
||||||
|
| KclError::Syntax(e)
|
||||||
|
| KclError::Semantic(e)
|
||||||
|
| KclError::ImportCycle(e)
|
||||||
|
| KclError::Type(e)
|
||||||
|
| KclError::Io(e)
|
||||||
|
| KclError::Unexpected(e)
|
||||||
|
| KclError::ValueAlreadyDefined(e)
|
||||||
|
| KclError::UndefinedValue(e)
|
||||||
|
| KclError::InvalidExpression(e)
|
||||||
|
| KclError::Engine(e)
|
||||||
|
| KclError::Internal(e) => e.backtrace.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
|
pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
|
||||||
let mut new = self.clone();
|
let mut new = self.clone();
|
||||||
match &mut new {
|
match &mut new {
|
||||||
KclError::Lexical(e) => e.source_ranges = source_ranges,
|
KclError::Lexical(e)
|
||||||
KclError::Syntax(e) => e.source_ranges = source_ranges,
|
| KclError::Syntax(e)
|
||||||
KclError::Semantic(e) => e.source_ranges = source_ranges,
|
| KclError::Semantic(e)
|
||||||
KclError::ImportCycle(e) => e.source_ranges = source_ranges,
|
| KclError::ImportCycle(e)
|
||||||
KclError::Type(e) => e.source_ranges = source_ranges,
|
| KclError::Type(e)
|
||||||
KclError::Io(e) => e.source_ranges = source_ranges,
|
| KclError::Io(e)
|
||||||
KclError::Unexpected(e) => e.source_ranges = source_ranges,
|
| KclError::Unexpected(e)
|
||||||
KclError::ValueAlreadyDefined(e) => e.source_ranges = source_ranges,
|
| KclError::ValueAlreadyDefined(e)
|
||||||
KclError::UndefinedValue(e) => e.source_ranges = source_ranges,
|
| KclError::UndefinedValue(e)
|
||||||
KclError::InvalidExpression(e) => e.source_ranges = source_ranges,
|
| KclError::InvalidExpression(e)
|
||||||
KclError::Engine(e) => e.source_ranges = source_ranges,
|
| KclError::Engine(e)
|
||||||
KclError::Internal(e) => e.source_ranges = source_ranges,
|
| KclError::Internal(e) => {
|
||||||
|
e.backtrace = source_ranges
|
||||||
|
.iter()
|
||||||
|
.map(|s| BacktraceItem {
|
||||||
|
source_range: *s,
|
||||||
|
fn_name: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
e.source_ranges = source_ranges;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new
|
new
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
|
pub(crate) fn set_last_backtrace_fn_name(&self, last_fn_name: Option<String>) -> Self {
|
||||||
let mut new = self.clone();
|
let mut new = self.clone();
|
||||||
match &mut new {
|
match &mut new {
|
||||||
KclError::Lexical(e) => e.source_ranges.extend(source_ranges),
|
KclError::Lexical(e)
|
||||||
KclError::Syntax(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Syntax(e)
|
||||||
KclError::Semantic(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Semantic(e)
|
||||||
KclError::ImportCycle(e) => e.source_ranges.extend(source_ranges),
|
| KclError::ImportCycle(e)
|
||||||
KclError::Type(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Type(e)
|
||||||
KclError::Io(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Io(e)
|
||||||
KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Unexpected(e)
|
||||||
KclError::ValueAlreadyDefined(e) => e.source_ranges.extend(source_ranges),
|
| KclError::ValueAlreadyDefined(e)
|
||||||
KclError::UndefinedValue(e) => e.source_ranges.extend(source_ranges),
|
| KclError::UndefinedValue(e)
|
||||||
KclError::InvalidExpression(e) => e.source_ranges.extend(source_ranges),
|
| KclError::InvalidExpression(e)
|
||||||
KclError::Engine(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Engine(e)
|
||||||
KclError::Internal(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Internal(e) => {
|
||||||
|
if let Some(item) = e.backtrace.last_mut() {
|
||||||
|
item.fn_name = last_fn_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new
|
new
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
|
||||||
|
let mut new = self.clone();
|
||||||
|
match &mut new {
|
||||||
|
KclError::Lexical(e)
|
||||||
|
| KclError::Syntax(e)
|
||||||
|
| KclError::Semantic(e)
|
||||||
|
| KclError::ImportCycle(e)
|
||||||
|
| KclError::Type(e)
|
||||||
|
| KclError::Io(e)
|
||||||
|
| KclError::Unexpected(e)
|
||||||
|
| KclError::ValueAlreadyDefined(e)
|
||||||
|
| KclError::UndefinedValue(e)
|
||||||
|
| KclError::InvalidExpression(e)
|
||||||
|
| KclError::Engine(e)
|
||||||
|
| KclError::Internal(e) => {
|
||||||
|
if let Some(item) = e.backtrace.last_mut() {
|
||||||
|
item.fn_name = last_fn_name;
|
||||||
|
}
|
||||||
|
e.backtrace.push(BacktraceItem {
|
||||||
|
source_range,
|
||||||
|
fn_name: None,
|
||||||
|
});
|
||||||
|
e.source_ranges.push(source_range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct BacktraceItem {
|
||||||
|
pub source_range: SourceRange,
|
||||||
|
pub fn_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for BacktraceItem {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(fn_name) = &self.fn_name {
|
||||||
|
write!(f, "{fn_name}: {:?}", self.source_range)
|
||||||
|
} else {
|
||||||
|
write!(f, "(fn): {:?}", self.source_range)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoDiagnostic for KclError {
|
impl IntoDiagnostic for KclError {
|
||||||
@ -551,6 +647,7 @@ impl From<pyo3::PyErr> for KclError {
|
|||||||
fn from(error: pyo3::PyErr) -> Self {
|
fn from(error: pyo3::PyErr) -> Self {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails {
|
||||||
source_ranges: vec![],
|
source_ranges: vec![],
|
||||||
|
backtrace: Default::default(),
|
||||||
message: error.to_string(),
|
message: error.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -629,8 +726,13 @@ impl CompilationError {
|
|||||||
|
|
||||||
impl From<CompilationError> for KclErrorDetails {
|
impl From<CompilationError> for KclErrorDetails {
|
||||||
fn from(err: CompilationError) -> Self {
|
fn from(err: CompilationError) -> Self {
|
||||||
|
let backtrace = vec![BacktraceItem {
|
||||||
|
source_range: err.source_range,
|
||||||
|
fn_name: None,
|
||||||
|
}];
|
||||||
KclErrorDetails {
|
KclErrorDetails {
|
||||||
source_ranges: vec![err.source_range],
|
source_ranges: vec![err.source_range],
|
||||||
|
backtrace,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,10 +70,10 @@ pub(super) fn expect_properties<'a>(
|
|||||||
) -> Result<&'a [Node<ObjectProperty>], KclError> {
|
) -> Result<&'a [Node<ObjectProperty>], KclError> {
|
||||||
assert_eq!(annotation.name().unwrap(), for_key);
|
assert_eq!(annotation.name().unwrap(), for_key);
|
||||||
Ok(&**annotation.properties.as_ref().ok_or_else(|| {
|
Ok(&**annotation.properties.as_ref().ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Empty `{for_key}` annotation"),
|
format!("Empty `{for_key}` annotation"),
|
||||||
source_ranges: vec![annotation.as_source_range()],
|
vec![annotation.as_source_range()],
|
||||||
})
|
))
|
||||||
})?)
|
})?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,10 +84,10 @@ pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
|
"Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
|
||||||
source_ranges: vec![expr.into()],
|
vec![expr.into()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the unparsed number literal.
|
// Returns the unparsed number literal.
|
||||||
@ -98,10 +98,10 @@ pub(super) fn expect_number(expr: &Expr) -> Result<String, KclError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Unexpected settings value, expected a number, e.g., `1.0`".to_owned(),
|
"Unexpected settings value, expected a number, e.g., `1.0`".to_owned(),
|
||||||
source_ranges: vec![expr.into()],
|
vec![expr.into()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn get_impl(annotations: &[Node<Annotation>], source_range: SourceRange) -> Result<Option<Impl>, KclError> {
|
pub(super) fn get_impl(annotations: &[Node<Annotation>], source_range: SourceRange) -> Result<Option<Impl>, KclError> {
|
||||||
@ -113,14 +113,14 @@ pub(super) fn get_impl(annotations: &[Node<Annotation>], source_range: SourceRan
|
|||||||
if &*p.key.name == IMPL {
|
if &*p.key.name == IMPL {
|
||||||
if let Some(s) = p.value.ident_name() {
|
if let Some(s) = p.value.ident_name() {
|
||||||
return Impl::from_str(s).map(Some).map_err(|_| {
|
return Impl::from_str(s).map(Some).map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Invalid value for {} attribute, expected one of: {}",
|
"Invalid value for {} attribute, expected one of: {}",
|
||||||
IMPL,
|
IMPL,
|
||||||
IMPL_VALUES.join(", ")
|
IMPL_VALUES.join(", ")
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,12 +139,12 @@ impl UnitLen {
|
|||||||
"inch" | "in" => Ok(UnitLen::Inches),
|
"inch" | "in" => Ok(UnitLen::Inches),
|
||||||
"ft" => Ok(UnitLen::Feet),
|
"ft" => Ok(UnitLen::Feet),
|
||||||
"yd" => Ok(UnitLen::Yards),
|
"yd" => Ok(UnitLen::Yards),
|
||||||
value => Err(KclError::Semantic(KclErrorDetails {
|
value => Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Unexpected value for length units: `{value}`; expected one of `mm`, `cm`, `m`, `in`, `ft`, `yd`"
|
"Unexpected value for length units: `{value}`; expected one of `mm`, `cm`, `m`, `in`, `ft`, `yd`"
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,10 +154,10 @@ impl UnitAngle {
|
|||||||
match s {
|
match s {
|
||||||
"deg" => Ok(UnitAngle::Degrees),
|
"deg" => Ok(UnitAngle::Degrees),
|
||||||
"rad" => Ok(UnitAngle::Radians),
|
"rad" => Ok(UnitAngle::Radians),
|
||||||
value => Err(KclError::Semantic(KclErrorDetails {
|
value => Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Unexpected value for angle units: `{value}`; expected one of `deg`, `rad`"),
|
format!("Unexpected value for angle units: `{value}`; expected one of `deg`, `rad`"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -941,12 +941,10 @@ fn artifacts_to_update(
|
|||||||
ModelingCmd::StartPath(_) => {
|
ModelingCmd::StartPath(_) => {
|
||||||
let mut return_arr = Vec::new();
|
let mut return_arr = Vec::new();
|
||||||
let current_plane_id = path_to_plane_id_map.get(&artifact_command.cmd_id).ok_or_else(|| {
|
let current_plane_id = path_to_plane_id_map.get(&artifact_command.cmd_id).ok_or_else(|| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!(
|
format!("Expected a current plane ID when processing StartPath command, but we have none: {id:?}"),
|
||||||
"Expected a current plane ID when processing StartPath command, but we have none: {id:?}"
|
vec![range],
|
||||||
),
|
))
|
||||||
source_ranges: vec![range],
|
|
||||||
})
|
|
||||||
})?;
|
})?;
|
||||||
return_arr.push(Artifact::Path(Path {
|
return_arr.push(Artifact::Path(Path {
|
||||||
id,
|
id,
|
||||||
@ -1065,10 +1063,10 @@ fn artifacts_to_update(
|
|||||||
// TODO: Using the first one. Make sure to revisit this
|
// TODO: Using the first one. Make sure to revisit this
|
||||||
// choice, don't think it matters for now.
|
// choice, don't think it matters for now.
|
||||||
path_id: ArtifactId::new(*loft_cmd.section_ids.first().ok_or_else(|| {
|
path_id: ArtifactId::new(*loft_cmd.section_ids.first().ok_or_else(|| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"),
|
format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"),
|
||||||
source_ranges: vec![range],
|
vec![range],
|
||||||
})
|
))
|
||||||
})?),
|
})?),
|
||||||
surface_ids: Vec::new(),
|
surface_ids: Vec::new(),
|
||||||
edge_ids: Vec::new(),
|
edge_ids: Vec::new(),
|
||||||
@ -1108,12 +1106,12 @@ fn artifacts_to_update(
|
|||||||
};
|
};
|
||||||
last_path = Some(path);
|
last_path = Some(path);
|
||||||
let path_sweep_id = path.sweep_id.ok_or_else(|| {
|
let path_sweep_id = path.sweep_id.ok_or_else(|| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message:format!(
|
format!(
|
||||||
"Expected a sweep ID on the path when processing Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
|
"Expected a sweep ID on the path when processing Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
|
||||||
),
|
),
|
||||||
source_ranges: vec![range],
|
vec![range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let extra_artifact = exec_artifacts.values().find(|a| {
|
let extra_artifact = exec_artifacts.values().find(|a| {
|
||||||
if let Artifact::StartSketchOnFace(s) = a {
|
if let Artifact::StartSketchOnFace(s) = a {
|
||||||
@ -1162,12 +1160,12 @@ fn artifacts_to_update(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let path_sweep_id = path.sweep_id.ok_or_else(|| {
|
let path_sweep_id = path.sweep_id.ok_or_else(|| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message:format!(
|
format!(
|
||||||
"Expected a sweep ID on the path when processing last path's Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
|
"Expected a sweep ID on the path when processing last path's Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
|
||||||
),
|
),
|
||||||
source_ranges: vec![range],
|
vec![range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let extra_artifact = exec_artifacts.values().find(|a| {
|
let extra_artifact = exec_artifacts.values().find(|a| {
|
||||||
if let Artifact::StartSketchOnFace(s) = a {
|
if let Artifact::StartSketchOnFace(s) = a {
|
||||||
|
@ -86,7 +86,7 @@ impl ExecutorContext {
|
|||||||
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError> {
|
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError> {
|
||||||
crate::log::log(format!("enter module {path} {}", exec_state.stack()));
|
crate::log::log(format!("enter module {path} {}", exec_state.stack()));
|
||||||
|
|
||||||
let mut local_state = ModuleState::new(path.std_path(), exec_state.stack().memory.clone(), Some(module_id));
|
let mut local_state = ModuleState::new(path.clone(), exec_state.stack().memory.clone(), Some(module_id));
|
||||||
if !preserve_mem {
|
if !preserve_mem {
|
||||||
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
||||||
}
|
}
|
||||||
@ -131,16 +131,21 @@ impl ExecutorContext {
|
|||||||
match statement {
|
match statement {
|
||||||
BodyItem::ImportStatement(import_stmt) => {
|
BodyItem::ImportStatement(import_stmt) => {
|
||||||
if !matches!(body_type, BodyType::Root) {
|
if !matches!(body_type, BodyType::Root) {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Imports are only supported at the top-level of a file.".to_owned(),
|
"Imports are only supported at the top-level of a file.".to_owned(),
|
||||||
source_ranges: vec![import_stmt.into()],
|
vec![import_stmt.into()],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let source_range = SourceRange::from(import_stmt);
|
let source_range = SourceRange::from(import_stmt);
|
||||||
let attrs = &import_stmt.outer_attrs;
|
let attrs = &import_stmt.outer_attrs;
|
||||||
|
let module_path = ModulePath::from_import_path(
|
||||||
|
&import_stmt.path,
|
||||||
|
&self.settings.project_directory,
|
||||||
|
&exec_state.mod_local.path,
|
||||||
|
)?;
|
||||||
let module_id = self
|
let module_id = self
|
||||||
.open_module(&import_stmt.path, attrs, exec_state, source_range)
|
.open_module(&import_stmt.path, attrs, &module_path, exec_state, source_range)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
match &import_stmt.selector {
|
match &import_stmt.selector {
|
||||||
@ -157,28 +162,25 @@ impl ExecutorContext {
|
|||||||
let mut ty = mem.get_from(&ty_name, env_ref, import_item.into(), 0).cloned();
|
let mut ty = mem.get_from(&ty_name, env_ref, import_item.into(), 0).cloned();
|
||||||
|
|
||||||
if value.is_err() && ty.is_err() {
|
if value.is_err() && ty.is_err() {
|
||||||
return Err(KclError::UndefinedValue(KclErrorDetails {
|
return Err(KclError::UndefinedValue(KclErrorDetails::new(
|
||||||
message: format!("{} is not defined in module", import_item.name.name),
|
format!("{} is not defined in module", import_item.name.name),
|
||||||
source_ranges: vec![SourceRange::from(&import_item.name)],
|
vec![SourceRange::from(&import_item.name)],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the item is allowed to be imported (in at least one namespace).
|
// Check that the item is allowed to be imported (in at least one namespace).
|
||||||
if value.is_ok() && !module_exports.contains(&import_item.name.name) {
|
if value.is_ok() && !module_exports.contains(&import_item.name.name) {
|
||||||
value = Err(KclError::Semantic(KclErrorDetails {
|
value = Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
|
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
|
||||||
import_item.name.name
|
import_item.name.name
|
||||||
),
|
),
|
||||||
source_ranges: vec![SourceRange::from(&import_item.name)],
|
vec![SourceRange::from(&import_item.name)],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ty.is_ok() && !module_exports.contains(&ty_name) {
|
if ty.is_ok() && !module_exports.contains(&ty_name) {
|
||||||
ty = Err(KclError::Semantic(KclErrorDetails {
|
ty = Err(KclError::Semantic(KclErrorDetails::new(String::new(), vec![])));
|
||||||
message: String::new(),
|
|
||||||
source_ranges: vec![],
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if value.is_err() && ty.is_err() {
|
if value.is_err() && ty.is_err() {
|
||||||
@ -225,10 +227,10 @@ impl ExecutorContext {
|
|||||||
.memory
|
.memory
|
||||||
.get_from(name, env_ref, source_range, 0)
|
.get_from(name, env_ref, source_range, 0)
|
||||||
.map_err(|_err| {
|
.map_err(|_err| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("{} is not defined in module (but was exported?)", name),
|
format!("{} is not defined in module (but was exported?)", name),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?
|
})?
|
||||||
.clone();
|
.clone();
|
||||||
exec_state.mut_stack().add(name.to_owned(), item, source_range)?;
|
exec_state.mut_stack().add(name.to_owned(), item, source_range)?;
|
||||||
@ -284,7 +286,14 @@ impl ExecutorContext {
|
|||||||
|
|
||||||
// Track exports.
|
// Track exports.
|
||||||
if let ItemVisibility::Export = variable_declaration.visibility {
|
if let ItemVisibility::Export = variable_declaration.visibility {
|
||||||
exec_state.mod_local.module_exports.push(var_name);
|
if matches!(body_type, BodyType::Root) {
|
||||||
|
exec_state.mod_local.module_exports.push(var_name);
|
||||||
|
} else {
|
||||||
|
exec_state.err(CompilationError::err(
|
||||||
|
variable_declaration.as_source_range(),
|
||||||
|
"Exports are only supported at the top-level of a file. Remove `export` or move it to the top-level.",
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Variable declaration can be the return value of a module.
|
// Variable declaration can be the return value of a module.
|
||||||
last_expr = matches!(body_type, BodyType::Root).then_some(value);
|
last_expr = matches!(body_type, BodyType::Root).then_some(value);
|
||||||
@ -294,13 +303,13 @@ impl ExecutorContext {
|
|||||||
let impl_kind = annotations::get_impl(&ty.outer_attrs, metadata.source_range)?.unwrap_or_default();
|
let impl_kind = annotations::get_impl(&ty.outer_attrs, metadata.source_range)?.unwrap_or_default();
|
||||||
match impl_kind {
|
match impl_kind {
|
||||||
annotations::Impl::Rust => {
|
annotations::Impl::Rust => {
|
||||||
let std_path = match &exec_state.mod_local.std_path {
|
let std_path = match &exec_state.mod_local.path {
|
||||||
Some(p) => p,
|
ModulePath::Std { value } => value,
|
||||||
None => {
|
ModulePath::Local { .. } | ModulePath::Main => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "User-defined types are not yet supported.".to_owned(),
|
"User-defined types are not yet supported.".to_owned(),
|
||||||
source_ranges: vec![metadata.source_range],
|
vec![metadata.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (t, props) = crate::std::std_ty(std_path, &ty.name.name);
|
let (t, props) = crate::std::std_ty(std_path, &ty.name.name);
|
||||||
@ -313,10 +322,10 @@ impl ExecutorContext {
|
|||||||
.mut_stack()
|
.mut_stack()
|
||||||
.add(name_in_mem.clone(), value, metadata.source_range)
|
.add(name_in_mem.clone(), value, metadata.source_range)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Redefinition of type {}.", ty.name.name),
|
format!("Redefinition of type {}.", ty.name.name),
|
||||||
source_ranges: vec![metadata.source_range],
|
vec![metadata.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if let ItemVisibility::Export = ty.visibility {
|
if let ItemVisibility::Export = ty.visibility {
|
||||||
@ -343,10 +352,10 @@ impl ExecutorContext {
|
|||||||
.mut_stack()
|
.mut_stack()
|
||||||
.add(name_in_mem.clone(), value, metadata.source_range)
|
.add(name_in_mem.clone(), value, metadata.source_range)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Redefinition of type {}.", ty.name.name),
|
format!("Redefinition of type {}.", ty.name.name),
|
||||||
source_ranges: vec![metadata.source_range],
|
vec![metadata.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if let ItemVisibility::Export = ty.visibility {
|
if let ItemVisibility::Export = ty.visibility {
|
||||||
@ -354,10 +363,10 @@ impl ExecutorContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "User-defined types are not yet supported.".to_owned(),
|
"User-defined types are not yet supported.".to_owned(),
|
||||||
source_ranges: vec![metadata.source_range],
|
vec![metadata.source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -368,10 +377,10 @@ impl ExecutorContext {
|
|||||||
let metadata = Metadata::from(return_statement);
|
let metadata = Metadata::from(return_statement);
|
||||||
|
|
||||||
if matches!(body_type, BodyType::Root) {
|
if matches!(body_type, BodyType::Root) {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Cannot return from outside a function.".to_owned(),
|
"Cannot return from outside a function.".to_owned(),
|
||||||
source_ranges: vec![metadata.source_range],
|
vec![metadata.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = self
|
let value = self
|
||||||
@ -387,10 +396,10 @@ impl ExecutorContext {
|
|||||||
.mut_stack()
|
.mut_stack()
|
||||||
.add(memory::RETURN_NAME.to_owned(), value, metadata.source_range)
|
.add(memory::RETURN_NAME.to_owned(), value, metadata.source_range)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Multiple returns from a single function.".to_owned(),
|
"Multiple returns from a single function.".to_owned(),
|
||||||
source_ranges: vec![metadata.source_range],
|
vec![metadata.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
last_expr = None;
|
last_expr = None;
|
||||||
}
|
}
|
||||||
@ -416,16 +425,15 @@ impl ExecutorContext {
|
|||||||
&self,
|
&self,
|
||||||
path: &ImportPath,
|
path: &ImportPath,
|
||||||
attrs: &[Node<Annotation>],
|
attrs: &[Node<Annotation>],
|
||||||
|
resolved_path: &ModulePath,
|
||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
source_range: SourceRange,
|
source_range: SourceRange,
|
||||||
) -> Result<ModuleId, KclError> {
|
) -> Result<ModuleId, KclError> {
|
||||||
let resolved_path = ModulePath::from_import_path(path, &self.settings.project_directory);
|
|
||||||
|
|
||||||
match path {
|
match path {
|
||||||
ImportPath::Kcl { .. } => {
|
ImportPath::Kcl { .. } => {
|
||||||
exec_state.global.mod_loader.cycle_check(&resolved_path, source_range)?;
|
exec_state.global.mod_loader.cycle_check(resolved_path, source_range)?;
|
||||||
|
|
||||||
if let Some(id) = exec_state.id_for_module(&resolved_path) {
|
if let Some(id) = exec_state.id_for_module(resolved_path) {
|
||||||
return Ok(id);
|
return Ok(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,12 +444,12 @@ impl ExecutorContext {
|
|||||||
exec_state.add_id_to_source(id, source.clone());
|
exec_state.add_id_to_source(id, source.clone());
|
||||||
// TODO handle parsing errors properly
|
// TODO handle parsing errors properly
|
||||||
let parsed = crate::parsing::parse_str(&source.source, id).parse_errs_as_err()?;
|
let parsed = crate::parsing::parse_str(&source.source, id).parse_errs_as_err()?;
|
||||||
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed, None));
|
exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Kcl(parsed, None));
|
||||||
|
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
ImportPath::Foreign { .. } => {
|
ImportPath::Foreign { .. } => {
|
||||||
if let Some(id) = exec_state.id_for_module(&resolved_path) {
|
if let Some(id) = exec_state.id_for_module(resolved_path) {
|
||||||
return Ok(id);
|
return Ok(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,11 +459,11 @@ impl ExecutorContext {
|
|||||||
exec_state.add_path_to_source_id(resolved_path.clone(), id);
|
exec_state.add_path_to_source_id(resolved_path.clone(), id);
|
||||||
let format = super::import::format_from_annotations(attrs, path, source_range)?;
|
let format = super::import::format_from_annotations(attrs, path, source_range)?;
|
||||||
let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?;
|
let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?;
|
||||||
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom, None));
|
exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Foreign(geom, None));
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
ImportPath::Std { .. } => {
|
ImportPath::Std { .. } => {
|
||||||
if let Some(id) = exec_state.id_for_module(&resolved_path) {
|
if let Some(id) = exec_state.id_for_module(resolved_path) {
|
||||||
return Ok(id);
|
return Ok(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -467,7 +475,7 @@ impl ExecutorContext {
|
|||||||
let parsed = crate::parsing::parse_str(&source.source, id)
|
let parsed = crate::parsing::parse_str(&source.source, id)
|
||||||
.parse_errs_as_err()
|
.parse_errs_as_err()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed, None));
|
exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Kcl(parsed, None));
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -493,10 +501,10 @@ impl ExecutorContext {
|
|||||||
*cache = Some((val, er, items.clone()));
|
*cache = Some((val, er, items.clone()));
|
||||||
(er, items)
|
(er, items)
|
||||||
}),
|
}),
|
||||||
ModuleRepr::Foreign(geom, _) => Err(KclError::Semantic(KclErrorDetails {
|
ModuleRepr::Foreign(geom, _) => Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Cannot import items from foreign modules".to_owned(),
|
"Cannot import items from foreign modules".to_owned(),
|
||||||
source_ranges: vec![geom.source_range],
|
vec![geom.source_range],
|
||||||
})),
|
))),
|
||||||
ModuleRepr::Dummy => unreachable!("Looking up {}, but it is still being interpreted", path),
|
ModuleRepr::Dummy => unreachable!("Looking up {}, but it is still being interpreted", path),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -572,13 +580,13 @@ impl ExecutorContext {
|
|||||||
err.override_source_ranges(vec![source_range])
|
err.override_source_ranges(vec![source_range])
|
||||||
} else {
|
} else {
|
||||||
// TODO would be great to have line/column for the underlying error here
|
// TODO would be great to have line/column for the underlying error here
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Error loading imported file ({path}). Open it to view more details.\n {}",
|
"Error loading imported file ({path}). Open it to view more details.\n {}",
|
||||||
err.message()
|
err.message()
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -628,7 +636,7 @@ impl ExecutorContext {
|
|||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if rust_impl {
|
if rust_impl {
|
||||||
if let Some(std_path) = &exec_state.mod_local.std_path {
|
if let ModulePath::Std { value: std_path } = &exec_state.mod_local.path {
|
||||||
let (func, props) = crate::std::std_fn(std_path, statement_kind.expect_name());
|
let (func, props) = crate::std::std_fn(std_path, statement_kind.expect_name());
|
||||||
KclValue::Function {
|
KclValue::Function {
|
||||||
value: FunctionSource::Std {
|
value: FunctionSource::Std {
|
||||||
@ -639,11 +647,10 @@ impl ExecutorContext {
|
|||||||
meta: vec![metadata.to_owned()],
|
meta: vec![metadata.to_owned()],
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Rust implementation of functions is restricted to the standard library"
|
"Rust implementation of functions is restricted to the standard library".to_owned(),
|
||||||
.to_owned(),
|
vec![metadata.source_range],
|
||||||
source_ranges: vec![metadata.source_range],
|
)));
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Snapshotting memory here is crucial for semantics so that we close
|
// Snapshotting memory here is crucial for semantics so that we close
|
||||||
@ -667,18 +674,18 @@ impl ExecutorContext {
|
|||||||
"you cannot declare variable {name} as %, because % can only be used in function calls"
|
"you cannot declare variable {name} as %, because % can only be used in function calls"
|
||||||
);
|
);
|
||||||
|
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message,
|
message,
|
||||||
source_ranges: vec![pipe_substitution.into()],
|
vec![pipe_substitution.into()],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() {
|
StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => {
|
None => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "cannot use % outside a pipe expression".to_owned(),
|
"cannot use % outside a pipe expression".to_owned(),
|
||||||
source_ranges: vec![pipe_substitution.into()],
|
vec![pipe_substitution.into()],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -750,13 +757,13 @@ fn apply_ascription(
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"could not coerce {} value to type {ty}{suggestion}",
|
"could not coerce value of type {} to type {ty}{suggestion}",
|
||||||
value.human_friendly_type()
|
value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -783,10 +790,10 @@ impl Node<Name> {
|
|||||||
ctx: &ExecutorContext,
|
ctx: &ExecutorContext,
|
||||||
) -> Result<&'a KclValue, KclError> {
|
) -> Result<&'a KclValue, KclError> {
|
||||||
if self.abs_path {
|
if self.abs_path {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Absolute paths (names beginning with `::` are not yet supported)".to_owned(),
|
"Absolute paths (names beginning with `::` are not yet supported)".to_owned(),
|
||||||
source_ranges: self.as_source_ranges(),
|
self.as_source_ranges(),
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.path.is_empty() {
|
if self.path.is_empty() {
|
||||||
@ -798,10 +805,10 @@ impl Node<Name> {
|
|||||||
let value = match mem_spec {
|
let value = match mem_spec {
|
||||||
Some((env, exports)) => {
|
Some((env, exports)) => {
|
||||||
if !exports.contains(&p.name) {
|
if !exports.contains(&p.name) {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Item {} not found in module's exported items", p.name),
|
format!("Item {} not found in module's exported items", p.name),
|
||||||
source_ranges: p.as_source_ranges(),
|
p.as_source_ranges(),
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
exec_state
|
exec_state
|
||||||
@ -813,13 +820,13 @@ impl Node<Name> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let KclValue::Module { value: module_id, .. } = value else {
|
let KclValue::Module { value: module_id, .. } = value else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Identifier in path must refer to a module, found {}",
|
"Identifier in path must refer to a module, found {}",
|
||||||
value.human_friendly_type()
|
value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: p.as_source_ranges(),
|
p.as_source_ranges(),
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
mem_spec = Some(
|
mem_spec = Some(
|
||||||
@ -830,10 +837,10 @@ impl Node<Name> {
|
|||||||
|
|
||||||
let (env, exports) = mem_spec.unwrap();
|
let (env, exports) = mem_spec.unwrap();
|
||||||
if !exports.contains(&self.name.name) {
|
if !exports.contains(&self.name.name) {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Item {} not found in module's exported items", self.name.name),
|
format!("Item {} not found in module's exported items", self.name.name),
|
||||||
source_ranges: self.name.as_source_ranges(),
|
self.name.as_source_ranges(),
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
exec_state
|
exec_state
|
||||||
@ -861,46 +868,44 @@ impl Node<MemberExpression> {
|
|||||||
if let Some(value) = map.get(&property) {
|
if let Some(value) = map.get(&property) {
|
||||||
Ok(value.to_owned())
|
Ok(value.to_owned())
|
||||||
} else {
|
} else {
|
||||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
Err(KclError::UndefinedValue(KclErrorDetails::new(
|
||||||
message: format!("Property '{property}' not found in object"),
|
format!("Property '{property}' not found in object"),
|
||||||
source_ranges: vec![self.clone().into()],
|
vec![self.clone().into()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KclValue::Object { .. }, Property::String(property), true) => Err(KclError::Semantic(KclErrorDetails {
|
(KclValue::Object { .. }, Property::String(property), true) => {
|
||||||
message: format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"),
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.clone().into()],
|
format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"),
|
||||||
})),
|
vec![self.clone().into()],
|
||||||
|
)))
|
||||||
|
}
|
||||||
(KclValue::Object { .. }, p, _) => {
|
(KclValue::Object { .. }, p, _) => {
|
||||||
let t = p.type_name();
|
let t = p.type_name();
|
||||||
let article = article_for(t);
|
let article = article_for(t);
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!("Only strings can be used as the property of an object, but you're using {article} {t}",),
|
||||||
"Only strings can be used as the property of an object, but you're using {article} {t}",
|
vec![self.clone().into()],
|
||||||
),
|
)))
|
||||||
source_ranges: vec![self.clone().into()],
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
(KclValue::HomArray { value: arr, .. }, Property::UInt(index), _) => {
|
(KclValue::HomArray { value: arr, .. }, Property::UInt(index), _) => {
|
||||||
let value_of_arr = arr.get(index);
|
let value_of_arr = arr.get(index);
|
||||||
if let Some(value) = value_of_arr {
|
if let Some(value) = value_of_arr {
|
||||||
Ok(value.to_owned())
|
Ok(value.to_owned())
|
||||||
} else {
|
} else {
|
||||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
Err(KclError::UndefinedValue(KclErrorDetails::new(
|
||||||
message: format!("The array doesn't have any item at index {index}"),
|
format!("The array doesn't have any item at index {index}"),
|
||||||
source_ranges: vec![self.clone().into()],
|
vec![self.clone().into()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KclValue::HomArray { .. }, p, _) => {
|
(KclValue::HomArray { .. }, p, _) => {
|
||||||
let t = p.type_name();
|
let t = p.type_name();
|
||||||
let article = article_for(t);
|
let article = article_for(t);
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!("Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",),
|
||||||
"Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",
|
vec![self.clone().into()],
|
||||||
),
|
)))
|
||||||
source_ranges: vec![self.clone().into()],
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
(KclValue::Solid { value }, Property::String(prop), false) if prop == "sketch" => Ok(KclValue::Sketch {
|
(KclValue::Solid { value }, Property::String(prop), false) if prop == "sketch" => Ok(KclValue::Sketch {
|
||||||
value: Box::new(value.sketch),
|
value: Box::new(value.sketch),
|
||||||
@ -918,10 +923,10 @@ impl Node<MemberExpression> {
|
|||||||
(being_indexed, _, _) => {
|
(being_indexed, _, _) => {
|
||||||
let t = being_indexed.human_friendly_type();
|
let t = being_indexed.human_friendly_type();
|
||||||
let article = article_for(&t);
|
let article = article_for(&t);
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
|
format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
|
||||||
source_ranges: vec![self.clone().into()],
|
vec![self.clone().into()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -996,26 +1001,26 @@ impl Node<BinaryExpression> {
|
|||||||
meta: _,
|
meta: _,
|
||||||
} = left_value
|
} = left_value
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Cannot apply logical operator to non-boolean value: {}",
|
"Cannot apply logical operator to non-boolean value: {}",
|
||||||
left_value.human_friendly_type()
|
left_value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: vec![self.left.clone().into()],
|
vec![self.left.clone().into()],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
let KclValue::Bool {
|
let KclValue::Bool {
|
||||||
value: right_value,
|
value: right_value,
|
||||||
meta: _,
|
meta: _,
|
||||||
} = right_value
|
} = right_value
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Cannot apply logical operator to non-boolean value: {}",
|
"Cannot apply logical operator to non-boolean value: {}",
|
||||||
right_value.human_friendly_type()
|
right_value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: vec![self.right.clone().into()],
|
vec![self.right.clone().into()],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
let raw_value = match self.operator {
|
let raw_value = match self.operator {
|
||||||
BinaryOperator::Or => left_value || right_value,
|
BinaryOperator::Or => left_value || right_value,
|
||||||
@ -1115,13 +1120,13 @@ impl Node<UnaryExpression> {
|
|||||||
meta: _,
|
meta: _,
|
||||||
} = value
|
} = value
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Cannot apply unary operator ! to non-boolean value: {}",
|
"Cannot apply unary operator ! to non-boolean value: {}",
|
||||||
value.human_friendly_type()
|
value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: vec![self.into()],
|
vec![self.into()],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
let meta = vec![Metadata {
|
let meta = vec![Metadata {
|
||||||
source_range: self.into(),
|
source_range: self.into(),
|
||||||
@ -1136,13 +1141,13 @@ impl Node<UnaryExpression> {
|
|||||||
|
|
||||||
let value = &self.argument.get_result(exec_state, ctx).await?;
|
let value = &self.argument.get_result(exec_state, ctx).await?;
|
||||||
let err = || {
|
let err = || {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"You can only negate numbers, planes, or lines, but this is a {}",
|
"You can only negate numbers, planes, or lines, but this is a {}",
|
||||||
value.human_friendly_type()
|
value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: vec![self.into()],
|
vec![self.into()],
|
||||||
})
|
))
|
||||||
};
|
};
|
||||||
match value {
|
match value {
|
||||||
KclValue::Number { value, ty, .. } => {
|
KclValue::Number { value, ty, .. } => {
|
||||||
@ -1239,10 +1244,10 @@ pub(crate) async fn execute_pipe_body(
|
|||||||
ctx: &ExecutorContext,
|
ctx: &ExecutorContext,
|
||||||
) -> Result<KclValue, KclError> {
|
) -> Result<KclValue, KclError> {
|
||||||
let Some((first, body)) = body.split_first() else {
|
let Some((first, body)) = body.split_first() else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Pipe expressions cannot be empty".to_owned(),
|
"Pipe expressions cannot be empty".to_owned(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
// Evaluate the first element in the pipeline.
|
// Evaluate the first element in the pipeline.
|
||||||
// They use the pipe_value from some AST node above this, so that if pipe expression is nested in a larger pipe expression,
|
// They use the pipe_value from some AST node above this, so that if pipe expression is nested in a larger pipe expression,
|
||||||
@ -1277,10 +1282,10 @@ async fn inner_execute_pipe_body(
|
|||||||
) -> Result<KclValue, KclError> {
|
) -> Result<KclValue, KclError> {
|
||||||
for expression in body {
|
for expression in body {
|
||||||
if let Expr::TagDeclarator(_) = expression {
|
if let Expr::TagDeclarator(_) = expression {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("This cannot be in a PipeExpression: {:?}", expression),
|
format!("This cannot be in a PipeExpression: {:?}", expression),
|
||||||
source_ranges: vec![expression.into()],
|
vec![expression.into()],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
let metadata = Metadata {
|
let metadata = Metadata {
|
||||||
source_range: SourceRange::from(expression),
|
source_range: SourceRange::from(expression),
|
||||||
@ -1349,35 +1354,37 @@ impl Node<ArrayRangeExpression> {
|
|||||||
StatementKind::Expression,
|
StatementKind::Expression,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let (start, start_ty) = start_val.as_int_with_ty().ok_or(KclError::Semantic(KclErrorDetails {
|
let (start, start_ty) = start_val
|
||||||
source_ranges: vec![self.into()],
|
.as_int_with_ty()
|
||||||
message: format!("Expected int but found {}", start_val.human_friendly_type()),
|
.ok_or(KclError::Semantic(KclErrorDetails::new(
|
||||||
}))?;
|
format!("Expected int but found {}", start_val.human_friendly_type()),
|
||||||
|
vec![self.into()],
|
||||||
|
)))?;
|
||||||
let metadata = Metadata::from(&self.end_element);
|
let metadata = Metadata::from(&self.end_element);
|
||||||
let end_val = ctx
|
let end_val = ctx
|
||||||
.execute_expr(&self.end_element, exec_state, &metadata, &[], StatementKind::Expression)
|
.execute_expr(&self.end_element, exec_state, &metadata, &[], StatementKind::Expression)
|
||||||
.await?;
|
.await?;
|
||||||
let (end, end_ty) = end_val.as_int_with_ty().ok_or(KclError::Semantic(KclErrorDetails {
|
let (end, end_ty) = end_val.as_int_with_ty().ok_or(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.into()],
|
format!("Expected int but found {}", end_val.human_friendly_type()),
|
||||||
message: format!("Expected int but found {}", end_val.human_friendly_type()),
|
vec![self.into()],
|
||||||
}))?;
|
)))?;
|
||||||
|
|
||||||
if start_ty != end_ty {
|
if start_ty != end_ty {
|
||||||
let start = start_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: start_ty });
|
let start = start_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: start_ty });
|
||||||
let start = fmt::human_display_number(start.n, start.ty);
|
let start = fmt::human_display_number(start.n, start.ty);
|
||||||
let end = end_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: end_ty });
|
let end = end_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: end_ty });
|
||||||
let end = fmt::human_display_number(end.n, end.ty);
|
let end = fmt::human_display_number(end.n, end.ty);
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.into()],
|
format!("Range start and end must be of the same type, but found {start} and {end}"),
|
||||||
message: format!("Range start and end must be of the same type, but found {start} and {end}"),
|
vec![self.into()],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if end < start {
|
if end < start {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.into()],
|
format!("Range start is greater than range end: {start} .. {end}"),
|
||||||
message: format!("Range start is greater than range end: {start} .. {end}"),
|
vec![self.into()],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let range: Vec<_> = if self.end_inclusive {
|
let range: Vec<_> = if self.end_inclusive {
|
||||||
@ -1438,10 +1445,10 @@ fn article_for<S: AsRef<str>>(s: S) -> &'static str {
|
|||||||
fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
|
fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
|
||||||
v.as_ty_f64().ok_or_else(|| {
|
v.as_ty_f64().ok_or_else(|| {
|
||||||
let actual_type = v.human_friendly_type();
|
let actual_type = v.human_friendly_type();
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![source_range],
|
format!("Expected a number, but found {actual_type}",),
|
||||||
message: format!("Expected a number, but found {actual_type}",),
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1530,16 +1537,16 @@ impl Property {
|
|||||||
if let Some(x) = crate::try_f64_to_usize(value) {
|
if let Some(x) = crate::try_f64_to_usize(value) {
|
||||||
Ok(Property::UInt(x))
|
Ok(Property::UInt(x))
|
||||||
} else {
|
} else {
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: property_sr,
|
format!("{value} is not a valid index, indices must be whole numbers >= 0"),
|
||||||
message: format!("{value} is not a valid index, indices must be whole numbers >= 0"),
|
property_sr,
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(KclError::Semantic(KclErrorDetails {
|
_ => Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![sr],
|
"Only numbers (>= 0) can be indexes".to_owned(),
|
||||||
message: "Only numbers (>= 0) can be indexes".to_owned(),
|
vec![sr],
|
||||||
})),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1547,12 +1554,7 @@ impl Property {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> {
|
fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> {
|
||||||
let make_err = |message: String| {
|
let make_err = |message: String| Err::<Property, _>(KclError::Semantic(KclErrorDetails::new(message, property_sr)));
|
||||||
Err::<Property, _>(KclError::Semantic(KclErrorDetails {
|
|
||||||
source_ranges: property_sr,
|
|
||||||
message,
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
match value {
|
match value {
|
||||||
KclValue::Number{value: num, .. } => {
|
KclValue::Number{value: num, .. } => {
|
||||||
let num = *num;
|
let num = *num;
|
||||||
@ -1650,7 +1652,7 @@ a = 42: string
|
|||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
err.to_string()
|
err.to_string()
|
||||||
.contains("could not coerce number(default units) value to type string"),
|
.contains("could not coerce value of type number(default units) to type string"),
|
||||||
"Expected error but found {err:?}"
|
"Expected error but found {err:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1661,7 +1663,7 @@ a = 42: Plane
|
|||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
err.to_string()
|
err.to_string()
|
||||||
.contains("could not coerce number(default units) value to type Plane"),
|
.contains("could not coerce value of type number(default units) to type Plane"),
|
||||||
"Expected error but found {err:?}"
|
"Expected error but found {err:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1671,8 +1673,9 @@ arr = [0]: [string]
|
|||||||
let result = parse_execute(program).await;
|
let result = parse_execute(program).await;
|
||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
err.to_string()
|
err.to_string().contains(
|
||||||
.contains("could not coerce [any; 1] value to type [string]"),
|
"could not coerce value of type array of number(default units) with 1 value to type [string]"
|
||||||
|
),
|
||||||
"Expected error but found {err:?}"
|
"Expected error but found {err:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1683,7 +1686,7 @@ mixedArr = [0, "a"]: [number(mm)]
|
|||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
err.to_string()
|
err.to_string()
|
||||||
.contains("could not coerce [any; 2] value to type [number(mm)]"),
|
.contains("could not coerce value of type array of number(default units), string with 2 values to type [number(mm)]"),
|
||||||
"Expected error but found {err:?}"
|
"Expected error but found {err:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1795,10 +1798,10 @@ d = b + c
|
|||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
KclError::Internal(crate::errors::KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("Failed to create mock engine connection: {}", err),
|
format!("Failed to create mock engine connection: {}", err),
|
||||||
source_ranges: vec![SourceRange::default()],
|
vec![SourceRange::default()],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)),
|
)),
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
use crate::execution::cad_op::{Group, OpArg, OpKclValue, Operation};
|
use super::{types::ArrayLen, EnvironmentRef};
|
||||||
use crate::{
|
use crate::{
|
||||||
docs::StdLibFn,
|
docs::StdLibFn,
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
execution::{
|
execution::{
|
||||||
kcl_value::FunctionSource, memory, types::RuntimeType, BodyType, ExecState, ExecutorContext, KclValue,
|
cad_op::{Group, OpArg, OpKclValue, Operation},
|
||||||
Metadata, StatementKind, TagEngineInfo, TagIdentifier,
|
kcl_value::FunctionSource,
|
||||||
|
memory,
|
||||||
|
types::RuntimeType,
|
||||||
|
BodyType, ExecState, ExecutorContext, KclValue, Metadata, StatementKind, TagEngineInfo, TagIdentifier,
|
||||||
},
|
},
|
||||||
parsing::ast::types::{CallExpressionKw, DefaultParamVal, FunctionExpression, Node, Program, Type},
|
parsing::ast::types::{CallExpressionKw, DefaultParamVal, FunctionExpression, Node, Program, Type},
|
||||||
source_range::SourceRange,
|
source_range::SourceRange,
|
||||||
@ -15,9 +18,6 @@ use crate::{
|
|||||||
CompilationError,
|
CompilationError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::types::ArrayLen;
|
|
||||||
use super::EnvironmentRef;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
/// Positional args.
|
/// Positional args.
|
||||||
@ -281,6 +281,13 @@ impl Node<CallExpressionKw> {
|
|||||||
def.call_kw(Some(func.name()), exec_state, ctx, args, callsite)
|
def.call_kw(Some(func.name()), exec_state, ctx, args, callsite)
|
||||||
.await
|
.await
|
||||||
.map(Option::unwrap)
|
.map(Option::unwrap)
|
||||||
|
.map_err(|e| {
|
||||||
|
// This is used for the backtrace display. We don't add
|
||||||
|
// another location the way we do for user-defined
|
||||||
|
// functions because the error uses the Args, which
|
||||||
|
// already points here.
|
||||||
|
e.set_last_backtrace_fn_name(Some(func.name()))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Clone the function so that we can use a mutable reference to
|
// Clone the function so that we can use a mutable reference to
|
||||||
@ -288,10 +295,10 @@ impl Node<CallExpressionKw> {
|
|||||||
let func = fn_name.get_result(exec_state, ctx).await?.clone();
|
let func = fn_name.get_result(exec_state, ctx).await?.clone();
|
||||||
|
|
||||||
let Some(fn_src) = func.as_fn() else {
|
let Some(fn_src) = func.as_fn() else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "cannot call this because it isn't a function".to_string(),
|
"cannot call this because it isn't a function".to_string(),
|
||||||
source_ranges: vec![callsite],
|
vec![callsite],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let return_value = fn_src
|
let return_value = fn_src
|
||||||
@ -299,7 +306,10 @@ impl Node<CallExpressionKw> {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
// Add the call expression to the source ranges.
|
// Add the call expression to the source ranges.
|
||||||
e.add_source_ranges(vec![callsite])
|
//
|
||||||
|
// TODO: Use the name that the function was defined
|
||||||
|
// with, not the identifier it was used with.
|
||||||
|
e.add_unwind_location(Some(fn_name.name.name.clone()), callsite)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let result = return_value.ok_or_else(move || {
|
let result = return_value.ok_or_else(move || {
|
||||||
@ -308,10 +318,10 @@ impl Node<CallExpressionKw> {
|
|||||||
if let KclValue::Function { meta, .. } = func {
|
if let KclValue::Function { meta, .. } = func {
|
||||||
source_ranges = meta.iter().map(|m| m.source_range).collect();
|
source_ranges = meta.iter().map(|m| m.source_range).collect();
|
||||||
};
|
};
|
||||||
KclError::UndefinedValue(KclErrorDetails {
|
KclError::UndefinedValue(KclErrorDetails::new(
|
||||||
message: format!("Result of user-defined function {} is undefined", fn_name),
|
format!("Result of user-defined function {} is undefined", fn_name),
|
||||||
source_ranges,
|
source_ranges,
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@ -490,10 +500,10 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
|
|||||||
let tag_id = if let Some(t) = value.sketch.tags.get(&tag.name) {
|
let tag_id = if let Some(t) = value.sketch.tags.get(&tag.name) {
|
||||||
let mut t = t.clone();
|
let mut t = t.clone();
|
||||||
let Some(info) = t.get_cur_info() else {
|
let Some(info) = t.get_cur_info() else {
|
||||||
return Err(KclError::Internal(KclErrorDetails {
|
return Err(KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("Tag {} does not have path info", tag.name),
|
format!("Tag {} does not have path info", tag.name),
|
||||||
source_ranges: vec![tag.into()],
|
vec![tag.into()],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut info = info.clone();
|
let mut info = info.clone();
|
||||||
@ -608,10 +618,10 @@ fn type_check_params_kw(
|
|||||||
// TODO if we have access to the AST for the argument we could choose which example to suggest.
|
// TODO if we have access to the AST for the argument we could choose which example to suggest.
|
||||||
message = format!("{message}\n\nYou may need to add information about the type of the argument, for example:\n using a numeric suffix: `42{ty}`\n or using type ascription: `foo(): number({ty})`");
|
message = format!("{message}\n\nYou may need to add information about the type of the argument, for example:\n using a numeric suffix: `42{ty}`\n or using type ascription: `foo(): number({ty})`");
|
||||||
}
|
}
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message,
|
message,
|
||||||
source_ranges: vec![arg.source_range],
|
vec![arg.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -673,8 +683,8 @@ fn type_check_params_kw(
|
|||||||
exec_state,
|
exec_state,
|
||||||
)
|
)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"The input argument of {} requires a value with type `{}`, but found {}",
|
"The input argument of {} requires a value with type `{}`, but found {}",
|
||||||
fn_name
|
fn_name
|
||||||
.map(|n| format!("`{}`", n))
|
.map(|n| format!("`{}`", n))
|
||||||
@ -682,8 +692,8 @@ fn type_check_params_kw(
|
|||||||
ty,
|
ty,
|
||||||
arg.1.value.human_friendly_type()
|
arg.1.value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: vec![arg.1.source_range],
|
vec![arg.1.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
} else if let Some((name, _)) = &fn_def.input_arg {
|
} else if let Some((name, _)) = &fn_def.input_arg {
|
||||||
@ -730,13 +740,13 @@ fn assign_args_to_params_kw(
|
|||||||
.add(name.clone(), value, default_val.source_range())?;
|
.add(name.clone(), value, default_val.source_range())?;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges,
|
format!(
|
||||||
message: format!(
|
|
||||||
"This function requires a parameter {}, but you haven't passed it one.",
|
"This function requires a parameter {}, but you haven't passed it one.",
|
||||||
name
|
name
|
||||||
),
|
),
|
||||||
}));
|
source_ranges,
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -747,16 +757,15 @@ fn assign_args_to_params_kw(
|
|||||||
|
|
||||||
let Some(unlabeled) = unlabelled else {
|
let Some(unlabeled) = unlabelled else {
|
||||||
return Err(if args.kw_args.labeled.contains_key(param_name) {
|
return Err(if args.kw_args.labeled.contains_key(param_name) {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
|
format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"),
|
||||||
source_ranges,
|
source_ranges,
|
||||||
message: format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"),
|
))
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
|
"This function expects an unlabeled first parameter, but you haven't passed it one.".to_owned(),
|
||||||
source_ranges,
|
source_ranges,
|
||||||
message: "This function expects an unlabeled first parameter, but you haven't passed it one."
|
))
|
||||||
.to_owned(),
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
exec_state.mut_stack().add(
|
exec_state.mut_stack().add(
|
||||||
@ -789,14 +798,14 @@ fn coerce_result_type(
|
|||||||
ty = RuntimeType::Union(vec![(**inner).clone(), ty]);
|
ty = RuntimeType::Union(vec![(**inner).clone(), ty]);
|
||||||
}
|
}
|
||||||
let val = val.coerce(&ty, exec_state).map_err(|_| {
|
let val = val.coerce(&ty, exec_state).map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"This function requires its result to be of type `{}`, but found {}",
|
"This function requires its result to be of type `{}`, but found {}",
|
||||||
ty.human_friendly_type(),
|
ty.human_friendly_type(),
|
||||||
val.human_friendly_type(),
|
val.human_friendly_type(),
|
||||||
),
|
),
|
||||||
source_ranges: ret_ty.as_source_ranges(),
|
ret_ty.as_source_ranges(),
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
Ok(Some(val))
|
Ok(Some(val))
|
||||||
} else {
|
} else {
|
||||||
@ -873,10 +882,10 @@ mod test {
|
|||||||
"all params required, none given, should error",
|
"all params required, none given, should error",
|
||||||
vec![req_param("x")],
|
vec![req_param("x")],
|
||||||
vec![],
|
vec![],
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![SourceRange::default()],
|
"This function requires a parameter x, but you haven't passed it one.".to_owned(),
|
||||||
message: "This function requires a parameter x, but you haven't passed it one.".to_owned(),
|
vec![SourceRange::default()],
|
||||||
})),
|
))),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"all params optional, none given, should be OK",
|
"all params optional, none given, should be OK",
|
||||||
@ -888,10 +897,10 @@ mod test {
|
|||||||
"mixed params, too few given",
|
"mixed params, too few given",
|
||||||
vec![req_param("x"), opt_param("y")],
|
vec![req_param("x"), opt_param("y")],
|
||||||
vec![],
|
vec![],
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![SourceRange::default()],
|
"This function requires a parameter x, but you haven't passed it one.".to_owned(),
|
||||||
message: "This function requires a parameter x, but you haven't passed it one.".to_owned(),
|
vec![SourceRange::default()],
|
||||||
})),
|
))),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"mixed params, minimum given, should be OK",
|
"mixed params, minimum given, should be OK",
|
||||||
|
@ -469,18 +469,18 @@ impl TryFrom<PlaneData> for PlaneInfo {
|
|||||||
PlaneData::NegYZ => PlaneName::NegYz,
|
PlaneData::NegYZ => PlaneName::NegYz,
|
||||||
PlaneData::Plane(_) => {
|
PlaneData::Plane(_) => {
|
||||||
// We will never get here since we already checked for PlaneData::Plane.
|
// We will never get here since we already checked for PlaneData::Plane.
|
||||||
return Err(KclError::Internal(KclErrorDetails {
|
return Err(KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("PlaneData {:?} not found", value),
|
format!("PlaneData {:?} not found", value),
|
||||||
source_ranges: Default::default(),
|
Default::default(),
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
|
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("Plane {} not found", name),
|
format!("Plane {} not found", name),
|
||||||
source_ranges: Default::default(),
|
Default::default(),
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(info.clone())
|
Ok(info.clone())
|
||||||
|
@ -37,53 +37,43 @@ pub async fn import_foreign(
|
|||||||
) -> Result<PreImportedGeometry, KclError> {
|
) -> Result<PreImportedGeometry, KclError> {
|
||||||
// Make sure the file exists.
|
// Make sure the file exists.
|
||||||
if !ctxt.fs.exists(file_path, source_range).await? {
|
if !ctxt.fs.exists(file_path, source_range).await? {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("File `{}` does not exist.", file_path.display()),
|
format!("File `{}` does not exist.", file_path.display()),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let ext_format = get_import_format_from_extension(file_path.extension().ok_or_else(|| {
|
let ext_format = get_import_format_from_extension(file_path.extension().ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("No file extension found for `{}`", file_path.display()),
|
format!("No file extension found for `{}`", file_path.display()),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?)
|
})?)
|
||||||
.map_err(|e| {
|
.map_err(|e| KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?;
|
||||||
KclError::Semantic(KclErrorDetails {
|
|
||||||
message: e.to_string(),
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Get the format type from the extension of the file.
|
// Get the format type from the extension of the file.
|
||||||
let format = if let Some(format) = format {
|
let format = if let Some(format) = format {
|
||||||
// Validate the given format with the extension format.
|
// Validate the given format with the extension format.
|
||||||
validate_extension_format(ext_format, format.clone()).map_err(|e| {
|
validate_extension_format(ext_format, format.clone())
|
||||||
KclError::Semantic(KclErrorDetails {
|
.map_err(|e| KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?;
|
||||||
message: e.to_string(),
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
format
|
format
|
||||||
} else {
|
} else {
|
||||||
ext_format
|
ext_format
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the file contents for each file path.
|
// Get the file contents for each file path.
|
||||||
let file_contents = ctxt.fs.read(file_path, source_range).await.map_err(|e| {
|
let file_contents = ctxt
|
||||||
KclError::Semantic(KclErrorDetails {
|
.fs
|
||||||
message: e.to_string(),
|
.read(file_path, source_range)
|
||||||
source_ranges: vec![source_range],
|
.await
|
||||||
})
|
.map_err(|e| KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?;
|
||||||
})?;
|
|
||||||
|
|
||||||
// We want the file_path to be without the parent.
|
// We want the file_path to be without the parent.
|
||||||
let file_name = file_path.file_name().map(|p| p.to_string()).ok_or_else(|| {
|
let file_name = file_path.file_name().map(|p| p.to_string()).ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Could not get the file name from the path `{}`", file_path.display()),
|
format!("Could not get the file name from the path `{}`", file_path.display()),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let mut import_files = vec![kcmc::ImportFile {
|
let mut import_files = vec![kcmc::ImportFile {
|
||||||
path: file_name.to_string(),
|
path: file_name.to_string(),
|
||||||
@ -96,12 +86,8 @@ pub async fn import_foreign(
|
|||||||
// Check if the file is a binary gltf file, in that case we don't need to import the bin
|
// Check if the file is a binary gltf file, in that case we don't need to import the bin
|
||||||
// file.
|
// file.
|
||||||
if !file_contents.starts_with(b"glTF") {
|
if !file_contents.starts_with(b"glTF") {
|
||||||
let json = gltf_json::Root::from_slice(&file_contents).map_err(|e| {
|
let json = gltf_json::Root::from_slice(&file_contents)
|
||||||
KclError::Semantic(KclErrorDetails {
|
.map_err(|e| KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?;
|
||||||
message: e.to_string(),
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Read the gltf file and check if there is a bin file.
|
// Read the gltf file and check if there is a bin file.
|
||||||
for buffer in json.buffers.iter() {
|
for buffer in json.buffers.iter() {
|
||||||
@ -109,18 +95,16 @@ pub async fn import_foreign(
|
|||||||
if !uri.starts_with("data:") {
|
if !uri.starts_with("data:") {
|
||||||
// We want this path relative to the file_path given.
|
// We want this path relative to the file_path given.
|
||||||
let bin_path = file_path.parent().map(|p| p.join(uri)).ok_or_else(|| {
|
let bin_path = file_path.parent().map(|p| p.join(uri)).ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Could not get the parent path of the file `{}`", file_path.display()),
|
format!("Could not get the parent path of the file `{}`", file_path.display()),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let bin_contents = ctxt.fs.read(&bin_path, source_range).await.map_err(|e| {
|
let bin_contents =
|
||||||
KclError::Semantic(KclErrorDetails {
|
ctxt.fs.read(&bin_path, source_range).await.map_err(|e| {
|
||||||
message: e.to_string(),
|
KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range]))
|
||||||
source_ranges: vec![source_range],
|
})?;
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
import_files.push(ImportFile {
|
import_files.push(ImportFile {
|
||||||
path: uri.to_string(),
|
path: uri.to_string(),
|
||||||
@ -157,13 +141,13 @@ pub(super) fn format_from_annotations(
|
|||||||
if p.key.name == annotations::IMPORT_FORMAT {
|
if p.key.name == annotations::IMPORT_FORMAT {
|
||||||
result = Some(
|
result = Some(
|
||||||
get_import_format_from_extension(annotations::expect_ident(&p.value)?).map_err(|_| {
|
get_import_format_from_extension(annotations::expect_ident(&p.value)?).map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Unknown format for import, expected one of: {}",
|
"Unknown format for import, expected one of: {}",
|
||||||
crate::IMPORT_FILE_EXTENSIONS.join(", ")
|
crate::IMPORT_FILE_EXTENSIONS.join(", ")
|
||||||
),
|
),
|
||||||
source_ranges: vec![p.as_source_range()],
|
vec![p.as_source_range()],
|
||||||
})
|
))
|
||||||
})?,
|
})?,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -175,10 +159,10 @@ pub(super) fn format_from_annotations(
|
|||||||
path.extension()
|
path.extension()
|
||||||
.and_then(|ext| get_import_format_from_extension(ext).ok())
|
.and_then(|ext| get_import_format_from_extension(ext).ok())
|
||||||
})
|
})
|
||||||
.ok_or(KclError::Semantic(KclErrorDetails {
|
.ok_or(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Unknown or missing extension, and no specified format for imported file".to_owned(),
|
"Unknown or missing extension, and no specified format for imported file".to_owned(),
|
||||||
source_ranges: vec![import_source_range],
|
vec![import_source_range],
|
||||||
}))?;
|
)))?;
|
||||||
|
|
||||||
for p in props {
|
for p in props {
|
||||||
match p.key.name.as_str() {
|
match p.key.name.as_str() {
|
||||||
@ -190,15 +174,15 @@ pub(super) fn format_from_annotations(
|
|||||||
}
|
}
|
||||||
annotations::IMPORT_FORMAT => {}
|
annotations::IMPORT_FORMAT => {}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Unexpected annotation for import, expected one of: {}, {}, {}",
|
"Unexpected annotation for import, expected one of: {}, {}, {}",
|
||||||
annotations::IMPORT_FORMAT,
|
annotations::IMPORT_FORMAT,
|
||||||
annotations::IMPORT_COORDS,
|
annotations::IMPORT_COORDS,
|
||||||
annotations::IMPORT_LENGTH_UNIT
|
annotations::IMPORT_LENGTH_UNIT
|
||||||
),
|
),
|
||||||
source_ranges: vec![p.as_source_range()],
|
vec![p.as_source_range()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -215,8 +199,8 @@ fn set_coords(fmt: &mut InputFormat3d, coords_str: &str, source_range: SourceRan
|
|||||||
}
|
}
|
||||||
|
|
||||||
let Some(coords) = coords else {
|
let Some(coords) = coords else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Unknown coordinate system: {coords_str}, expected one of: {}",
|
"Unknown coordinate system: {coords_str}, expected one of: {}",
|
||||||
annotations::IMPORT_COORDS_VALUES
|
annotations::IMPORT_COORDS_VALUES
|
||||||
.iter()
|
.iter()
|
||||||
@ -224,8 +208,8 @@ fn set_coords(fmt: &mut InputFormat3d, coords_str: &str, source_range: SourceRan
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
match fmt {
|
match fmt {
|
||||||
@ -233,13 +217,13 @@ fn set_coords(fmt: &mut InputFormat3d, coords_str: &str, source_range: SourceRan
|
|||||||
InputFormat3d::Ply(opts) => opts.coords = coords,
|
InputFormat3d::Ply(opts) => opts.coords = coords,
|
||||||
InputFormat3d::Stl(opts) => opts.coords = coords,
|
InputFormat3d::Stl(opts) => opts.coords = coords,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"`{}` option cannot be applied to the specified format",
|
"`{}` option cannot be applied to the specified format",
|
||||||
annotations::IMPORT_COORDS
|
annotations::IMPORT_COORDS
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,13 +238,13 @@ fn set_length_unit(fmt: &mut InputFormat3d, units_str: &str, source_range: Sourc
|
|||||||
InputFormat3d::Ply(opts) => opts.units = units.into(),
|
InputFormat3d::Ply(opts) => opts.units = units.into(),
|
||||||
InputFormat3d::Stl(opts) => opts.units = units.into(),
|
InputFormat3d::Stl(opts) => opts.units = units.into(),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"`{}` option cannot be applied to the specified format",
|
"`{}` option cannot be applied to the specified format",
|
||||||
annotations::IMPORT_LENGTH_UNIT
|
annotations::IMPORT_LENGTH_UNIT
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,8 +281,34 @@ impl KclValue {
|
|||||||
/// Human readable type name used in error messages. Should not be relied
|
/// Human readable type name used in error messages. Should not be relied
|
||||||
/// on for program logic.
|
/// on for program logic.
|
||||||
pub(crate) fn human_friendly_type(&self) -> String {
|
pub(crate) fn human_friendly_type(&self) -> String {
|
||||||
if let Some(t) = self.principal_type() {
|
self.inner_human_friendly_type(1)
|
||||||
return t.to_string();
|
}
|
||||||
|
|
||||||
|
fn inner_human_friendly_type(&self, max_depth: usize) -> String {
|
||||||
|
if let Some(pt) = self.principal_type() {
|
||||||
|
if max_depth > 0 {
|
||||||
|
// The principal type of an array uses the array's element type,
|
||||||
|
// which is oftentimes `any`, and that's not a helpful message. So
|
||||||
|
// we show the actual elements.
|
||||||
|
if let Some(elements) = self.as_array() {
|
||||||
|
// If it's empty, we want to show the type of the array.
|
||||||
|
if !elements.is_empty() {
|
||||||
|
// A max of 3 is good because it's common to use 3D points.
|
||||||
|
let max = 3;
|
||||||
|
let len = elements.len();
|
||||||
|
let ellipsis = if len > max { ", ..." } else { "" };
|
||||||
|
let element_label = if len == 1 { "value" } else { "values" };
|
||||||
|
let element_tys = elements
|
||||||
|
.iter()
|
||||||
|
.take(max)
|
||||||
|
.map(|elem| elem.inner_human_friendly_type(max_depth - 1))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
return format!("array of {element_tys}{ellipsis} with {len} {element_label}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pt.to_string();
|
||||||
}
|
}
|
||||||
match self {
|
match self {
|
||||||
KclValue::Uuid { .. } => "Unique ID (uuid)",
|
KclValue::Uuid { .. } => "Unique ID (uuid)",
|
||||||
@ -543,17 +569,13 @@ impl KclValue {
|
|||||||
/// If this value fits in a u32, return it.
|
/// If this value fits in a u32, return it.
|
||||||
pub fn get_u32(&self, source_ranges: Vec<SourceRange>) -> Result<u32, KclError> {
|
pub fn get_u32(&self, source_ranges: Vec<SourceRange>) -> Result<u32, KclError> {
|
||||||
let u = self.as_int().and_then(|n| u64::try_from(n).ok()).ok_or_else(|| {
|
let u = self.as_int().and_then(|n| u64::try_from(n).ok()).ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Expected an integer >= 0".to_owned(),
|
"Expected an integer >= 0".to_owned(),
|
||||||
source_ranges: source_ranges.clone(),
|
source_ranges.clone(),
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
u32::try_from(u).map_err(|_| {
|
u32::try_from(u)
|
||||||
KclError::Semantic(KclErrorDetails {
|
.map_err(|_| KclError::Semantic(KclErrorDetails::new("Number was too big".to_owned(), source_ranges)))
|
||||||
message: "Number was too big".to_owned(),
|
|
||||||
source_ranges,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this value is of type function, return it.
|
/// If this value is of type function, return it.
|
||||||
@ -568,10 +590,10 @@ impl KclValue {
|
|||||||
pub fn get_tag_identifier(&self) -> Result<TagIdentifier, KclError> {
|
pub fn get_tag_identifier(&self) -> Result<TagIdentifier, KclError> {
|
||||||
match self {
|
match self {
|
||||||
KclValue::TagIdentifier(t) => Ok(*t.clone()),
|
KclValue::TagIdentifier(t) => Ok(*t.clone()),
|
||||||
_ => Err(KclError::Semantic(KclErrorDetails {
|
_ => Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Not a tag identifier: {:?}", self),
|
format!("Not a tag identifier: {:?}", self),
|
||||||
source_ranges: self.clone().into(),
|
self.clone().into(),
|
||||||
})),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -579,20 +601,20 @@ impl KclValue {
|
|||||||
pub fn get_tag_declarator(&self) -> Result<TagNode, KclError> {
|
pub fn get_tag_declarator(&self) -> Result<TagNode, KclError> {
|
||||||
match self {
|
match self {
|
||||||
KclValue::TagDeclarator(t) => Ok((**t).clone()),
|
KclValue::TagDeclarator(t) => Ok((**t).clone()),
|
||||||
_ => Err(KclError::Semantic(KclErrorDetails {
|
_ => Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Not a tag declarator: {:?}", self),
|
format!("Not a tag declarator: {:?}", self),
|
||||||
source_ranges: self.clone().into(),
|
self.clone().into(),
|
||||||
})),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this KCL value is a bool, retrieve it.
|
/// If this KCL value is a bool, retrieve it.
|
||||||
pub fn get_bool(&self) -> Result<bool, KclError> {
|
pub fn get_bool(&self) -> Result<bool, KclError> {
|
||||||
let Self::Bool { value: b, .. } = self else {
|
let Self::Bool { value: b, .. } = self else {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails::new(
|
||||||
source_ranges: self.into(),
|
format!("Expected bool, found {}", self.human_friendly_type()),
|
||||||
message: format!("Expected bool, found {}", self.human_friendly_type()),
|
self.into(),
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
Ok(*b)
|
Ok(*b)
|
||||||
}
|
}
|
||||||
@ -648,3 +670,88 @@ impl From<GeometryWithImportedGeometry> for KclValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_human_friendly_type() {
|
||||||
|
let len = KclValue::Number {
|
||||||
|
value: 1.0,
|
||||||
|
ty: NumericType::Known(UnitType::Length(UnitLen::Unknown)),
|
||||||
|
meta: vec![],
|
||||||
|
};
|
||||||
|
assert_eq!(len.human_friendly_type(), "number(Length)".to_string());
|
||||||
|
|
||||||
|
let unknown = KclValue::Number {
|
||||||
|
value: 1.0,
|
||||||
|
ty: NumericType::Unknown,
|
||||||
|
meta: vec![],
|
||||||
|
};
|
||||||
|
assert_eq!(unknown.human_friendly_type(), "number(unknown units)".to_string());
|
||||||
|
|
||||||
|
let mm = KclValue::Number {
|
||||||
|
value: 1.0,
|
||||||
|
ty: NumericType::Known(UnitType::Length(UnitLen::Mm)),
|
||||||
|
meta: vec![],
|
||||||
|
};
|
||||||
|
assert_eq!(mm.human_friendly_type(), "number(mm)".to_string());
|
||||||
|
|
||||||
|
let array1_mm = KclValue::HomArray {
|
||||||
|
value: vec![mm.clone()],
|
||||||
|
ty: RuntimeType::any(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
array1_mm.human_friendly_type(),
|
||||||
|
"array of number(mm) with 1 value".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let array2_mm = KclValue::HomArray {
|
||||||
|
value: vec![mm.clone(), mm.clone()],
|
||||||
|
ty: RuntimeType::any(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
array2_mm.human_friendly_type(),
|
||||||
|
"array of number(mm), number(mm) with 2 values".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let array3_mm = KclValue::HomArray {
|
||||||
|
value: vec![mm.clone(), mm.clone(), mm.clone()],
|
||||||
|
ty: RuntimeType::any(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
array3_mm.human_friendly_type(),
|
||||||
|
"array of number(mm), number(mm), number(mm) with 3 values".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let inches = KclValue::Number {
|
||||||
|
value: 1.0,
|
||||||
|
ty: NumericType::Known(UnitType::Length(UnitLen::Inches)),
|
||||||
|
meta: vec![],
|
||||||
|
};
|
||||||
|
let array4 = KclValue::HomArray {
|
||||||
|
value: vec![mm.clone(), mm.clone(), inches.clone(), mm.clone()],
|
||||||
|
ty: RuntimeType::any(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
array4.human_friendly_type(),
|
||||||
|
"array of number(mm), number(mm), number(in), ... with 4 values".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let empty_array = KclValue::HomArray {
|
||||||
|
value: vec![],
|
||||||
|
ty: RuntimeType::any(),
|
||||||
|
};
|
||||||
|
assert_eq!(empty_array.human_friendly_type(), "[any; 0]".to_string());
|
||||||
|
|
||||||
|
let array_nested = KclValue::HomArray {
|
||||||
|
value: vec![array2_mm.clone()],
|
||||||
|
ty: RuntimeType::any(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
array_nested.human_friendly_type(),
|
||||||
|
"array of [any; 2] with 1 value".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -364,10 +364,10 @@ impl ProgramMemory {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
Err(KclError::UndefinedValue(KclErrorDetails::new(
|
||||||
message: format!("`{}` is not defined", var),
|
format!("`{}` is not defined", var),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over all key/value pairs in the specified environment which satisfy the provided
|
/// Iterate over all key/value pairs in the specified environment which satisfy the provided
|
||||||
@ -485,10 +485,10 @@ impl ProgramMemory {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
Err(KclError::UndefinedValue(KclErrorDetails::new(
|
||||||
message: format!("`{}` is not defined", var),
|
format!("`{}` is not defined", var),
|
||||||
source_ranges: vec![],
|
vec![],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -643,10 +643,10 @@ impl Stack {
|
|||||||
pub fn add(&mut self, key: String, value: KclValue, source_range: SourceRange) -> Result<(), KclError> {
|
pub fn add(&mut self, key: String, value: KclValue, source_range: SourceRange) -> Result<(), KclError> {
|
||||||
let env = self.memory.get_env(self.current_env.index());
|
let env = self.memory.get_env(self.current_env.index());
|
||||||
if env.contains_key(&key) {
|
if env.contains_key(&key) {
|
||||||
return Err(KclError::ValueAlreadyDefined(KclErrorDetails {
|
return Err(KclError::ValueAlreadyDefined(KclErrorDetails::new(
|
||||||
message: format!("Cannot redefine `{}`", key),
|
format!("Cannot redefine `{}`", key),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.memory.stats.mutation_count.fetch_add(1, Ordering::Relaxed);
|
self.memory.stats.mutation_count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
@ -858,10 +858,9 @@ impl ExecutorContext {
|
|||||||
|
|
||||||
for module in modules {
|
for module in modules {
|
||||||
let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
|
let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
|
||||||
return Err(KclErrorWithOutputs::no_outputs(KclError::Internal(KclErrorDetails {
|
return Err(KclErrorWithOutputs::no_outputs(KclError::Internal(
|
||||||
message: format!("Module {module} not found in universe"),
|
KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
|
||||||
source_ranges: Default::default(),
|
)));
|
||||||
})));
|
|
||||||
};
|
};
|
||||||
let module_id = *module_id;
|
let module_id = *module_id;
|
||||||
let module_path = module_path.clone();
|
let module_path = module_path.clone();
|
||||||
@ -921,10 +920,10 @@ impl ExecutorContext {
|
|||||||
|
|
||||||
result.map(|val| ModuleRepr::Foreign(geom.clone(), val))
|
result.map(|val| ModuleRepr::Foreign(geom.clone(), val))
|
||||||
}
|
}
|
||||||
ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::Internal(KclErrorDetails {
|
ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("Module {module_path} not found in universe"),
|
format!("Module {module_path} not found in universe"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})),
|
))),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1045,6 +1044,7 @@ impl ExecutorContext {
|
|||||||
|
|
||||||
let root_imports = crate::walk::import_universe(
|
let root_imports = crate::walk::import_universe(
|
||||||
self,
|
self,
|
||||||
|
&ModulePath::Main,
|
||||||
&ModuleRepr::Kcl(program.ast.clone(), None),
|
&ModuleRepr::Kcl(program.ast.clone(), None),
|
||||||
&mut universe,
|
&mut universe,
|
||||||
exec_state,
|
exec_state,
|
||||||
@ -1212,15 +1212,10 @@ impl ExecutorContext {
|
|||||||
/// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
|
/// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
|
||||||
async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
|
async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
|
||||||
if exec_state.stack().memory.requires_std() {
|
if exec_state.stack().memory.requires_std() {
|
||||||
|
let path = vec!["std".to_owned(), "prelude".to_owned()];
|
||||||
|
let resolved_path = ModulePath::from_std_import_path(&path)?;
|
||||||
let id = self
|
let id = self
|
||||||
.open_module(
|
.open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
|
||||||
&ImportPath::Std {
|
|
||||||
path: vec!["std".to_owned(), "prelude".to_owned()],
|
|
||||||
},
|
|
||||||
&[],
|
|
||||||
exec_state,
|
|
||||||
source_range,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
|
let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
|
||||||
|
|
||||||
@ -1288,10 +1283,10 @@ impl ExecutorContext {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
|
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
|
||||||
return Err(KclError::Internal(crate::errors::KclErrorDetails {
|
return Err(KclError::Internal(crate::errors::KclErrorDetails::new(
|
||||||
message: format!("Expected Export response, got {resp:?}",),
|
format!("Expected Export response, got {resp:?}",),
|
||||||
source_ranges: vec![SourceRange::default()],
|
vec![SourceRange::default()],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(files)
|
Ok(files)
|
||||||
@ -1308,10 +1303,10 @@ impl ExecutorContext {
|
|||||||
coords: *kittycad_modeling_cmds::coord::KITTYCAD,
|
coords: *kittycad_modeling_cmds::coord::KITTYCAD,
|
||||||
created: if deterministic_time {
|
created: if deterministic_time {
|
||||||
Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
|
Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
|
||||||
KclError::Internal(crate::errors::KclErrorDetails {
|
KclError::Internal(crate::errors::KclErrorDetails::new(
|
||||||
message: format!("Failed to parse date: {}", e),
|
format!("Failed to parse date: {}", e),
|
||||||
source_ranges: vec![SourceRange::default()],
|
vec![SourceRange::default()],
|
||||||
})
|
))
|
||||||
})?)
|
})?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -1388,10 +1383,10 @@ pub(crate) async fn parse_execute_with_project_dir(
|
|||||||
let exec_ctxt = ExecutorContext {
|
let exec_ctxt = ExecutorContext {
|
||||||
engine: Arc::new(Box::new(
|
engine: Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
|
crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
|
||||||
KclError::Internal(crate::errors::KclErrorDetails {
|
KclError::Internal(crate::errors::KclErrorDetails::new(
|
||||||
message: format!("Failed to create mock engine connection: {}", err),
|
format!("Failed to create mock engine connection: {}", err),
|
||||||
source_ranges: vec![SourceRange::default()],
|
vec![SourceRange::default()],
|
||||||
})
|
))
|
||||||
})?,
|
})?,
|
||||||
)),
|
)),
|
||||||
fs: Arc::new(crate::fs::FileManager::new()),
|
fs: Arc::new(crate::fs::FileManager::new()),
|
||||||
@ -1804,10 +1799,10 @@ foo
|
|||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err,
|
err,
|
||||||
KclError::Syntax(KclErrorDetails {
|
KclError::Syntax(KclErrorDetails::new(
|
||||||
message: "Unexpected token: #".to_owned(),
|
"Unexpected token: #".to_owned(),
|
||||||
source_ranges: vec![SourceRange::new(14, 15, ModuleId::default())],
|
vec![SourceRange::new(14, 15, ModuleId::default())],
|
||||||
}),
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2063,10 +2058,10 @@ notTagIdentifier = !myTag";
|
|||||||
// TODO: We don't currently parse this, but we should. It should be
|
// TODO: We don't currently parse this, but we should. It should be
|
||||||
// a runtime error instead.
|
// a runtime error instead.
|
||||||
parse_execute(code10).await.unwrap_err(),
|
parse_execute(code10).await.unwrap_err(),
|
||||||
KclError::Syntax(KclErrorDetails {
|
KclError::Syntax(KclErrorDetails::new(
|
||||||
message: "Unexpected token: !".to_owned(),
|
"Unexpected token: !".to_owned(),
|
||||||
source_ranges: vec![SourceRange::new(10, 11, ModuleId::default())],
|
vec![SourceRange::new(10, 11, ModuleId::default())],
|
||||||
})
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
let code11 = "
|
let code11 = "
|
||||||
@ -2076,10 +2071,10 @@ notPipeSub = 1 |> identity(!%))";
|
|||||||
// TODO: We don't currently parse this, but we should. It should be
|
// TODO: We don't currently parse this, but we should. It should be
|
||||||
// a runtime error instead.
|
// a runtime error instead.
|
||||||
parse_execute(code11).await.unwrap_err(),
|
parse_execute(code11).await.unwrap_err(),
|
||||||
KclError::Syntax(KclErrorDetails {
|
KclError::Syntax(KclErrorDetails::new(
|
||||||
message: "Unexpected token: |>".to_owned(),
|
"Unexpected token: |>".to_owned(),
|
||||||
source_ranges: vec![SourceRange::new(44, 46, ModuleId::default())],
|
vec![SourceRange::new(44, 46, ModuleId::default())],
|
||||||
})
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Add these tests when we support these types.
|
// TODO: Add these tests when we support these types.
|
||||||
|
@ -85,14 +85,14 @@ pub(super) struct ModuleState {
|
|||||||
/// Settings specified from annotations.
|
/// Settings specified from annotations.
|
||||||
pub settings: MetaSettings,
|
pub settings: MetaSettings,
|
||||||
pub(super) explicit_length_units: bool,
|
pub(super) explicit_length_units: bool,
|
||||||
pub(super) std_path: Option<String>,
|
pub(super) path: ModulePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExecState {
|
impl ExecState {
|
||||||
pub fn new(exec_context: &super::ExecutorContext) -> Self {
|
pub fn new(exec_context: &super::ExecutorContext) -> Self {
|
||||||
ExecState {
|
ExecState {
|
||||||
global: GlobalState::new(&exec_context.settings),
|
global: GlobalState::new(&exec_context.settings),
|
||||||
mod_local: ModuleState::new(None, ProgramMemory::new(), Default::default()),
|
mod_local: ModuleState::new(ModulePath::Main, ProgramMemory::new(), Default::default()),
|
||||||
exec_context: Some(exec_context.clone()),
|
exec_context: Some(exec_context.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ impl ExecState {
|
|||||||
|
|
||||||
*self = ExecState {
|
*self = ExecState {
|
||||||
global,
|
global,
|
||||||
mod_local: ModuleState::new(None, ProgramMemory::new(), Default::default()),
|
mod_local: ModuleState::new(self.mod_local.path.clone(), ProgramMemory::new(), Default::default()),
|
||||||
exec_context: Some(exec_context.clone()),
|
exec_context: Some(exec_context.clone()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -276,8 +276,8 @@ impl ExecState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn circular_import_error(&self, path: &ModulePath, source_range: SourceRange) -> KclError {
|
pub(super) fn circular_import_error(&self, path: &ModulePath, source_range: SourceRange) -> KclError {
|
||||||
KclError::ImportCycle(KclErrorDetails {
|
KclError::ImportCycle(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"circular import of modules is not allowed: {} -> {}",
|
"circular import of modules is not allowed: {} -> {}",
|
||||||
self.global
|
self.global
|
||||||
.mod_loader
|
.mod_loader
|
||||||
@ -288,8 +288,8 @@ impl ExecState {
|
|||||||
.join(" -> "),
|
.join(" -> "),
|
||||||
path,
|
path,
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pipe_value(&self) -> Option<&KclValue> {
|
pub(crate) fn pipe_value(&self) -> Option<&KclValue> {
|
||||||
@ -337,14 +337,14 @@ impl GlobalState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ModuleState {
|
impl ModuleState {
|
||||||
pub(super) fn new(std_path: Option<String>, memory: Arc<ProgramMemory>, module_id: Option<ModuleId>) -> Self {
|
pub(super) fn new(path: ModulePath, memory: Arc<ProgramMemory>, module_id: Option<ModuleId>) -> Self {
|
||||||
ModuleState {
|
ModuleState {
|
||||||
id_generator: IdGenerator::new(module_id),
|
id_generator: IdGenerator::new(module_id),
|
||||||
stack: memory.new_stack(),
|
stack: memory.new_stack(),
|
||||||
pipe_value: Default::default(),
|
pipe_value: Default::default(),
|
||||||
module_exports: Default::default(),
|
module_exports: Default::default(),
|
||||||
explicit_length_units: false,
|
explicit_length_units: false,
|
||||||
std_path,
|
path,
|
||||||
settings: MetaSettings {
|
settings: MetaSettings {
|
||||||
default_length_units: Default::default(),
|
default_length_units: Default::default(),
|
||||||
default_angle_units: Default::default(),
|
default_angle_units: Default::default(),
|
||||||
@ -389,14 +389,14 @@ impl MetaSettings {
|
|||||||
self.kcl_version = value;
|
self.kcl_version = value;
|
||||||
}
|
}
|
||||||
name => {
|
name => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Unexpected settings key: `{name}`; expected one of `{}`, `{}`",
|
"Unexpected settings key: `{name}`; expected one of `{}`, `{}`",
|
||||||
annotations::SETTINGS_UNIT_LENGTH,
|
annotations::SETTINGS_UNIT_LENGTH,
|
||||||
annotations::SETTINGS_UNIT_ANGLE
|
annotations::SETTINGS_UNIT_ANGLE
|
||||||
),
|
),
|
||||||
source_ranges: vec![annotation.as_source_range()],
|
vec![annotation.as_source_range()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,31 +35,28 @@ impl Default for TypedPath {
|
|||||||
|
|
||||||
impl From<&String> for TypedPath {
|
impl From<&String> for TypedPath {
|
||||||
fn from(path: &String) -> Self {
|
fn from(path: &String) -> Self {
|
||||||
#[cfg(target_arch = "wasm32")]
|
TypedPath::new(path)
|
||||||
{
|
|
||||||
TypedPath(typed_path::TypedPath::derive(path).to_path_buf())
|
|
||||||
}
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
{
|
|
||||||
TypedPath(std::path::PathBuf::from(path))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for TypedPath {
|
impl From<&str> for TypedPath {
|
||||||
fn from(path: &str) -> Self {
|
fn from(path: &str) -> Self {
|
||||||
|
TypedPath::new(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypedPath {
|
||||||
|
pub fn new(path: &str) -> Self {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
TypedPath(typed_path::TypedPath::derive(path).to_path_buf())
|
TypedPath(typed_path::TypedPath::derive(path).to_path_buf())
|
||||||
}
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
TypedPath(std::path::PathBuf::from(path))
|
TypedPath(normalise_import(path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl TypedPath {
|
|
||||||
pub fn extension(&self) -> Option<&str> {
|
pub fn extension(&self) -> Option<&str> {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
@ -85,6 +82,17 @@ impl TypedPath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn join_typed(&self, path: &TypedPath) -> Self {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
TypedPath(self.0.join(path.0.to_path()))
|
||||||
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
TypedPath(self.0.join(&path.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parent(&self) -> Option<Self> {
|
pub fn parent(&self) -> Option<Self> {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
@ -206,3 +214,19 @@ impl schemars::JsonSchema for TypedPath {
|
|||||||
gen.subschema_for::<std::path::PathBuf>()
|
gen.subschema_for::<std::path::PathBuf>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn `nested\foo\bar\main.kcl` or `nested/foo/bar/main.kcl`
|
||||||
|
/// into a PathBuf that works on the host OS.
|
||||||
|
///
|
||||||
|
/// * Does **not** touch `..` or symlinks – call `canonicalize()` if you need that.
|
||||||
|
/// * Returns an owned `PathBuf` only when normalisation was required.
|
||||||
|
fn normalise_import<S: AsRef<str>>(raw: S) -> std::path::PathBuf {
|
||||||
|
let s = raw.as_ref();
|
||||||
|
// On Unix we need to swap `\` → `/`. On Windows we leave it alone.
|
||||||
|
// (Windows happily consumes `/`)
|
||||||
|
if cfg!(unix) && s.contains('\\') {
|
||||||
|
std::path::PathBuf::from(s.replace('\\', "/"))
|
||||||
|
} else {
|
||||||
|
std::path::Path::new(s).to_path_buf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -155,9 +155,8 @@ impl RuntimeType {
|
|||||||
.map(RuntimeType::Union),
|
.map(RuntimeType::Union),
|
||||||
Type::Object { properties } => properties
|
Type::Object { properties } => properties
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| {
|
.map(|(id, ty)| {
|
||||||
RuntimeType::from_parsed(p.type_.unwrap().inner, exec_state, source_range)
|
RuntimeType::from_parsed(ty.inner, exec_state, source_range).map(|ty| (id.name.clone(), ty))
|
||||||
.map(|ty| (p.identifier.inner.name, ty))
|
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, CompilationError>>()
|
.collect::<Result<Vec<_>, CompilationError>>()
|
||||||
.map(RuntimeType::Object),
|
.map(RuntimeType::Object),
|
||||||
|
@ -28,19 +28,19 @@ impl Default for FileManager {
|
|||||||
impl FileSystem for FileManager {
|
impl FileSystem for FileManager {
|
||||||
async fn read(&self, path: &TypedPath, source_range: SourceRange) -> Result<Vec<u8>, KclError> {
|
async fn read(&self, path: &TypedPath, source_range: SourceRange) -> Result<Vec<u8>, KclError> {
|
||||||
tokio::fs::read(&path.0).await.map_err(|e| {
|
tokio::fs::read(&path.0).await.map_err(|e| {
|
||||||
KclError::Io(KclErrorDetails {
|
KclError::Io(KclErrorDetails::new(
|
||||||
message: format!("Failed to read file `{}`: {}", path.display(), e),
|
format!("Failed to read file `{}`: {}", path.display(), e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_to_string(&self, path: &TypedPath, source_range: SourceRange) -> Result<String, KclError> {
|
async fn read_to_string(&self, path: &TypedPath, source_range: SourceRange) -> Result<String, KclError> {
|
||||||
tokio::fs::read_to_string(&path.0).await.map_err(|e| {
|
tokio::fs::read_to_string(&path.0).await.map_err(|e| {
|
||||||
KclError::Io(KclErrorDetails {
|
KclError::Io(KclErrorDetails::new(
|
||||||
message: format!("Failed to read file `{}`: {}", path.display(), e),
|
format!("Failed to read file `{}`: {}", path.display(), e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,10 +49,10 @@ impl FileSystem for FileManager {
|
|||||||
if e.kind() == std::io::ErrorKind::NotFound {
|
if e.kind() == std::io::ErrorKind::NotFound {
|
||||||
Ok(false)
|
Ok(false)
|
||||||
} else {
|
} else {
|
||||||
Err(KclError::Io(KclErrorDetails {
|
Err(KclError::Io(KclErrorDetails::new(
|
||||||
message: format!("Failed to check if file `{}` exists: {}", path.display(), e),
|
format!("Failed to check if file `{}` exists: {}", path.display(), e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -71,10 +71,10 @@ impl FileSystem for FileManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut read_dir = tokio::fs::read_dir(&path).await.map_err(|e| {
|
let mut read_dir = tokio::fs::read_dir(&path).await.map_err(|e| {
|
||||||
KclError::Io(KclErrorDetails {
|
KclError::Io(KclErrorDetails::new(
|
||||||
message: format!("Failed to read directory `{}`: {}", path.display(), e),
|
format!("Failed to read directory `{}`: {}", path.display(), e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
while let Ok(Some(entry)) = read_dir.next_entry().await {
|
while let Ok(Some(entry)) = read_dir.next_entry().await {
|
||||||
|
@ -46,18 +46,16 @@ unsafe impl Sync for FileManager {}
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl FileSystem for FileManager {
|
impl FileSystem for FileManager {
|
||||||
async fn read(&self, path: &TypedPath, source_range: SourceRange) -> Result<Vec<u8>, KclError> {
|
async fn read(&self, path: &TypedPath, source_range: SourceRange) -> Result<Vec<u8>, KclError> {
|
||||||
let promise = self.manager.read_file(path.to_string_lossy()).map_err(|e| {
|
let promise = self
|
||||||
KclError::Engine(KclErrorDetails {
|
.manager
|
||||||
message: e.to_string().into(),
|
.read_file(path.to_string_lossy())
|
||||||
source_ranges: vec![source_range],
|
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let value = JsFuture::from(promise).await.map_err(|e| {
|
let value = JsFuture::from(promise).await.map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to wait for promise from engine: {:?}", e),
|
format!("Failed to wait for promise from engine: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let array = js_sys::Uint8Array::new(&value);
|
let array = js_sys::Uint8Array::new(&value);
|
||||||
@ -69,35 +67,33 @@ impl FileSystem for FileManager {
|
|||||||
async fn read_to_string(&self, path: &TypedPath, source_range: SourceRange) -> Result<String, KclError> {
|
async fn read_to_string(&self, path: &TypedPath, source_range: SourceRange) -> Result<String, KclError> {
|
||||||
let bytes = self.read(path, source_range).await?;
|
let bytes = self.read(path, source_range).await?;
|
||||||
let string = String::from_utf8(bytes).map_err(|e| {
|
let string = String::from_utf8(bytes).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to convert bytes to string: {:?}", e),
|
format!("Failed to convert bytes to string: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(string)
|
Ok(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn exists(&self, path: &TypedPath, source_range: SourceRange) -> Result<bool, crate::errors::KclError> {
|
async fn exists(&self, path: &TypedPath, source_range: SourceRange) -> Result<bool, crate::errors::KclError> {
|
||||||
let promise = self.manager.exists(path.to_string_lossy()).map_err(|e| {
|
let promise = self
|
||||||
KclError::Engine(KclErrorDetails {
|
.manager
|
||||||
message: e.to_string().into(),
|
.exists(path.to_string_lossy())
|
||||||
source_ranges: vec![source_range],
|
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let value = JsFuture::from(promise).await.map_err(|e| {
|
let value = JsFuture::from(promise).await.map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to wait for promise from engine: {:?}", e),
|
format!("Failed to wait for promise from engine: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let it_exists = value.as_bool().ok_or_else(|| {
|
let it_exists = value.as_bool().ok_or_else(|| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: "Failed to convert value to bool".to_string(),
|
"Failed to convert value to bool".to_string(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(it_exists)
|
Ok(it_exists)
|
||||||
@ -108,32 +104,30 @@ impl FileSystem for FileManager {
|
|||||||
path: &TypedPath,
|
path: &TypedPath,
|
||||||
source_range: SourceRange,
|
source_range: SourceRange,
|
||||||
) -> Result<Vec<TypedPath>, crate::errors::KclError> {
|
) -> Result<Vec<TypedPath>, crate::errors::KclError> {
|
||||||
let promise = self.manager.get_all_files(path.to_string_lossy()).map_err(|e| {
|
let promise = self
|
||||||
KclError::Engine(KclErrorDetails {
|
.manager
|
||||||
message: e.to_string().into(),
|
.get_all_files(path.to_string_lossy())
|
||||||
source_ranges: vec![source_range],
|
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let value = JsFuture::from(promise).await.map_err(|e| {
|
let value = JsFuture::from(promise).await.map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to wait for promise from javascript: {:?}", e),
|
format!("Failed to wait for promise from javascript: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let s = value.as_string().ok_or_else(|| {
|
let s = value.as_string().ok_or_else(|| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to get string from response from javascript: `{:?}`", value),
|
format!("Failed to get string from response from javascript: `{:?}`", value),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let files: Vec<String> = serde_json::from_str(&s).map_err(|e| {
|
let files: Vec<String> = serde_json::from_str(&s).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to parse json from javascript: `{}` `{:?}`", s, e),
|
format!("Failed to parse json from javascript: `{}` `{:?}`", s, e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(files.into_iter().map(|s| TypedPath::from(&s)).collect())
|
Ok(files.into_iter().map(|s| TypedPath::from(&s)).collect())
|
||||||
|
@ -86,7 +86,8 @@ mod wasm;
|
|||||||
pub use coredump::CoreDump;
|
pub use coredump::CoreDump;
|
||||||
pub use engine::{AsyncTasks, EngineManager, EngineStats};
|
pub use engine::{AsyncTasks, EngineManager, EngineStats};
|
||||||
pub use errors::{
|
pub use errors::{
|
||||||
CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs, Report, ReportWithOutputs,
|
BacktraceItem, CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs, Report,
|
||||||
|
ReportWithOutputs,
|
||||||
};
|
};
|
||||||
pub use execution::{
|
pub use execution::{
|
||||||
bust_cache, clear_mem_cache,
|
bust_cache, clear_mem_cache,
|
||||||
|
@ -58,8 +58,8 @@ impl ModuleLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn import_cycle_error(&self, path: &ModulePath, source_range: SourceRange) -> KclError {
|
pub(crate) fn import_cycle_error(&self, path: &ModulePath, source_range: SourceRange) -> KclError {
|
||||||
KclError::ImportCycle(KclErrorDetails {
|
KclError::ImportCycle(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"circular import of modules is not allowed: {} -> {}",
|
"circular import of modules is not allowed: {} -> {}",
|
||||||
self.import_stack
|
self.import_stack
|
||||||
.iter()
|
.iter()
|
||||||
@ -68,8 +68,8 @@ impl ModuleLoader {
|
|||||||
.join(" -> "),
|
.join(" -> "),
|
||||||
path,
|
path,
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn enter_module(&mut self, path: &ModulePath) {
|
pub(crate) fn enter_module(&mut self, path: &ModulePath) {
|
||||||
@ -153,13 +153,6 @@ impl ModulePath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn std_path(&self) -> Option<String> {
|
|
||||||
match self {
|
|
||||||
ModulePath::Std { value: p } => Some(p.clone()),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn source(&self, fs: &FileManager, source_range: SourceRange) -> Result<ModuleSource, KclError> {
|
pub(crate) async fn source(&self, fs: &FileManager, source_range: SourceRange) -> Result<ModuleSource, KclError> {
|
||||||
match self {
|
match self {
|
||||||
ModulePath::Local { value: p } => Ok(ModuleSource {
|
ModulePath::Local { value: p } => Ok(ModuleSource {
|
||||||
@ -169,10 +162,10 @@ impl ModulePath {
|
|||||||
ModulePath::Std { value: name } => Ok(ModuleSource {
|
ModulePath::Std { value: name } => Ok(ModuleSource {
|
||||||
source: read_std(name)
|
source: read_std(name)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Cannot find standard library module to import: std::{name}."),
|
format!("Cannot find standard library module to import: std::{name}."),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
.map(str::to_owned)?,
|
.map(str::to_owned)?,
|
||||||
path: self.clone(),
|
path: self.clone(),
|
||||||
@ -181,25 +174,53 @@ impl ModulePath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn from_import_path(path: &ImportPath, project_directory: &Option<TypedPath>) -> Self {
|
pub(crate) fn from_import_path(
|
||||||
|
path: &ImportPath,
|
||||||
|
project_directory: &Option<TypedPath>,
|
||||||
|
import_from: &ModulePath,
|
||||||
|
) -> Result<Self, KclError> {
|
||||||
match path {
|
match path {
|
||||||
ImportPath::Kcl { filename: path } | ImportPath::Foreign { path } => {
|
ImportPath::Kcl { filename: path } | ImportPath::Foreign { path } => {
|
||||||
let resolved_path = if let Some(project_dir) = project_directory {
|
let resolved_path = match import_from {
|
||||||
project_dir.join(path)
|
ModulePath::Main => {
|
||||||
} else {
|
if let Some(project_dir) = project_directory {
|
||||||
TypedPath::from(path)
|
project_dir.join_typed(path)
|
||||||
|
} else {
|
||||||
|
path.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ModulePath::Local { value } => {
|
||||||
|
let import_from_dir = value.parent();
|
||||||
|
let base = import_from_dir.as_ref().or(project_directory.as_ref());
|
||||||
|
if let Some(dir) = base {
|
||||||
|
dir.join_typed(path)
|
||||||
|
} else {
|
||||||
|
path.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ModulePath::Std { .. } => {
|
||||||
|
let message = format!("Cannot import a non-std KCL file from std: {path}.");
|
||||||
|
debug_assert!(false, "{}", &message);
|
||||||
|
return Err(KclError::Internal(KclErrorDetails::new(message, vec![])));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
ModulePath::Local { value: resolved_path }
|
|
||||||
}
|
|
||||||
ImportPath::Std { path } => {
|
|
||||||
// For now we only support importing from singly-nested modules inside std.
|
|
||||||
assert_eq!(path.len(), 2);
|
|
||||||
assert_eq!(&path[0], "std");
|
|
||||||
|
|
||||||
ModulePath::Std { value: path[1].clone() }
|
Ok(ModulePath::Local { value: resolved_path })
|
||||||
}
|
}
|
||||||
|
ImportPath::Std { path } => Self::from_std_import_path(path),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_std_import_path(path: &[String]) -> Result<Self, KclError> {
|
||||||
|
// For now we only support importing from singly-nested modules inside std.
|
||||||
|
if path.len() != 2 || path[0] != "std" {
|
||||||
|
let message = format!("Invalid std import path: {path:?}.");
|
||||||
|
debug_assert!(false, "{}", &message);
|
||||||
|
return Err(KclError::Internal(KclErrorDetails::new(message, vec![])));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ModulePath::Std { value: path[1].clone() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ModulePath {
|
impl fmt::Display for ModulePath {
|
||||||
|
@ -212,8 +212,9 @@ impl Type {
|
|||||||
Type::Object { properties } => {
|
Type::Object { properties } => {
|
||||||
hasher.update(b"FnArgType::Object");
|
hasher.update(b"FnArgType::Object");
|
||||||
hasher.update(properties.len().to_ne_bytes());
|
hasher.update(properties.len().to_ne_bytes());
|
||||||
for prop in properties.iter_mut() {
|
for (id, ty) in properties.iter_mut() {
|
||||||
hasher.update(prop.compute_digest());
|
hasher.update(id.compute_digest());
|
||||||
|
hasher.update(ty.compute_digest());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
parsing::{ast::digest::Digest, token::NumericSuffix, PIPE_OPERATOR},
|
parsing::{ast::digest::Digest, token::NumericSuffix, PIPE_OPERATOR},
|
||||||
source_range::SourceRange,
|
source_range::SourceRange,
|
||||||
ModuleId,
|
ModuleId, TypedPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod condition;
|
mod condition;
|
||||||
@ -1741,8 +1741,8 @@ impl ImportSelector {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum ImportPath {
|
pub enum ImportPath {
|
||||||
Kcl { filename: String },
|
Kcl { filename: TypedPath },
|
||||||
Foreign { path: String },
|
Foreign { path: TypedPath },
|
||||||
Std { path: Vec<String> },
|
Std { path: Vec<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1811,16 +1811,25 @@ impl ImportStatement {
|
|||||||
|
|
||||||
match &self.path {
|
match &self.path {
|
||||||
ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => {
|
ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => {
|
||||||
let mut parts = s.split('.');
|
let name = s.to_string_lossy();
|
||||||
let path = parts.next()?;
|
if name.ends_with("/main.kcl") || name.ends_with("\\main.kcl") {
|
||||||
let _ext = parts.next()?;
|
let name = &name[..name.len() - 9];
|
||||||
let rest = parts.next();
|
let start = name.rfind(['/', '\\']).map(|s| s + 1).unwrap_or(0);
|
||||||
|
return Some(name[start..].to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
if rest.is_some() {
|
let name = s.file_name().map(|f| f.to_string())?;
|
||||||
|
if name.contains('\\') || name.contains('/') {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
path.rsplit(&['/', '\\']).next().map(str::to_owned)
|
// Remove the extension if it exists.
|
||||||
|
let extension = s.extension();
|
||||||
|
Some(if let Some(extension) = extension {
|
||||||
|
name.trim_end_matches(extension).trim_end_matches('.').to_string()
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
})
|
||||||
}
|
}
|
||||||
ImportPath::Std { path } => path.last().cloned(),
|
ImportPath::Std { path } => path.last().cloned(),
|
||||||
}
|
}
|
||||||
@ -3315,7 +3324,7 @@ pub enum Type {
|
|||||||
},
|
},
|
||||||
// An object type.
|
// An object type.
|
||||||
Object {
|
Object {
|
||||||
properties: Vec<Parameter>,
|
properties: Vec<(Node<Identifier>, Node<Type>)>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3348,10 +3357,8 @@ impl fmt::Display for Type {
|
|||||||
} else {
|
} else {
|
||||||
write!(f, ",")?;
|
write!(f, ",")?;
|
||||||
}
|
}
|
||||||
write!(f, " {}:", p.identifier.name)?;
|
write!(f, " {}:", p.0.name)?;
|
||||||
if let Some(ty) = &p.type_ {
|
write!(f, " {}", p.1)?;
|
||||||
write!(f, " {}", ty.inner)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
write!(f, " }}")
|
write!(f, " }}")
|
||||||
}
|
}
|
||||||
@ -3988,7 +3995,7 @@ cylinder = startSketchOn(-XZ)
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_parse_type_args_object_on_functions() {
|
async fn test_parse_type_args_object_on_functions() {
|
||||||
let some_program_string = r#"fn thing(arg0: [number], arg1: {thing: number, things: [string], more?: string}, tag?: string) {
|
let some_program_string = r#"fn thing(arg0: [number], arg1: {thing: number, things: [string], more: string}, tag?: string) {
|
||||||
return arg0
|
return arg0
|
||||||
}"#;
|
}"#;
|
||||||
let module_id = ModuleId::default();
|
let module_id = ModuleId::default();
|
||||||
@ -4015,8 +4022,8 @@ cylinder = startSketchOn(-XZ)
|
|||||||
params[1].type_.as_ref().unwrap().inner,
|
params[1].type_.as_ref().unwrap().inner,
|
||||||
Type::Object {
|
Type::Object {
|
||||||
properties: vec![
|
properties: vec![
|
||||||
Parameter {
|
(
|
||||||
identifier: Node::new(
|
Node::new(
|
||||||
Identifier {
|
Identifier {
|
||||||
name: "thing".to_owned(),
|
name: "thing".to_owned(),
|
||||||
digest: None,
|
digest: None,
|
||||||
@ -4025,18 +4032,15 @@ cylinder = startSketchOn(-XZ)
|
|||||||
37,
|
37,
|
||||||
module_id,
|
module_id,
|
||||||
),
|
),
|
||||||
type_: Some(Node::new(
|
Node::new(
|
||||||
Type::Primitive(PrimitiveType::Number(NumericSuffix::None)),
|
Type::Primitive(PrimitiveType::Number(NumericSuffix::None)),
|
||||||
39,
|
39,
|
||||||
45,
|
45,
|
||||||
module_id
|
module_id
|
||||||
)),
|
),
|
||||||
default_value: None,
|
),
|
||||||
labeled: true,
|
(
|
||||||
digest: None,
|
Node::new(
|
||||||
},
|
|
||||||
Parameter {
|
|
||||||
identifier: Node::new(
|
|
||||||
Identifier {
|
Identifier {
|
||||||
name: "things".to_owned(),
|
name: "things".to_owned(),
|
||||||
digest: None,
|
digest: None,
|
||||||
@ -4045,7 +4049,7 @@ cylinder = startSketchOn(-XZ)
|
|||||||
53,
|
53,
|
||||||
module_id,
|
module_id,
|
||||||
),
|
),
|
||||||
type_: Some(Node::new(
|
Node::new(
|
||||||
Type::Array {
|
Type::Array {
|
||||||
ty: Box::new(Type::Primitive(PrimitiveType::String)),
|
ty: Box::new(Type::Primitive(PrimitiveType::String)),
|
||||||
len: ArrayLen::None
|
len: ArrayLen::None
|
||||||
@ -4053,13 +4057,10 @@ cylinder = startSketchOn(-XZ)
|
|||||||
56,
|
56,
|
||||||
62,
|
62,
|
||||||
module_id
|
module_id
|
||||||
)),
|
)
|
||||||
default_value: None,
|
),
|
||||||
labeled: true,
|
(
|
||||||
digest: None
|
Node::new(
|
||||||
},
|
|
||||||
Parameter {
|
|
||||||
identifier: Node::new(
|
|
||||||
Identifier {
|
Identifier {
|
||||||
name: "more".to_owned(),
|
name: "more".to_owned(),
|
||||||
digest: None
|
digest: None
|
||||||
@ -4068,11 +4069,8 @@ cylinder = startSketchOn(-XZ)
|
|||||||
69,
|
69,
|
||||||
module_id,
|
module_id,
|
||||||
),
|
),
|
||||||
type_: Some(Node::new(Type::Primitive(PrimitiveType::String), 72, 78, module_id)),
|
Node::new(Type::Primitive(PrimitiveType::String), 71, 77, module_id),
|
||||||
labeled: true,
|
)
|
||||||
default_value: Some(DefaultParamVal::none()),
|
|
||||||
digest: None
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -4343,4 +4341,20 @@ startSketchOn(XY)
|
|||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn module_name() {
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_mod_name(stmt: &str, name: &str) {
|
||||||
|
let tokens = crate::parsing::token::lex(stmt, ModuleId::default()).unwrap();
|
||||||
|
let stmt = crate::parsing::parser::import_stmt(&mut tokens.as_slice()).unwrap();
|
||||||
|
assert_eq!(stmt.module_name().unwrap(), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_mod_name("import 'foo.kcl'", "foo");
|
||||||
|
assert_mod_name("import 'foo.kcl' as bar", "bar");
|
||||||
|
assert_mod_name("import 'main.kcl'", "main");
|
||||||
|
assert_mod_name("import 'foo/main.kcl'", "foo");
|
||||||
|
assert_mod_name("import 'foo\\bar\\main.kcl'", "bar");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ pub fn parse_tokens(mut tokens: TokenStream) -> ParseResult {
|
|||||||
} else {
|
} else {
|
||||||
format!("found unknown tokens [{}]", token_list.join(", "))
|
format!("found unknown tokens [{}]", token_list.join(", "))
|
||||||
};
|
};
|
||||||
return KclError::Lexical(KclErrorDetails { source_ranges, message }).into();
|
return KclError::Lexical(KclErrorDetails::new(message, source_ranges)).into();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Important, to not call this before the unknown tokens check.
|
// Important, to not call this before the unknown tokens check.
|
||||||
|
@ -35,7 +35,7 @@ use crate::{
|
|||||||
token::{Token, TokenSlice, TokenType},
|
token::{Token, TokenSlice, TokenType},
|
||||||
PIPE_OPERATOR, PIPE_SUBSTITUTION_OPERATOR,
|
PIPE_OPERATOR, PIPE_SUBSTITUTION_OPERATOR,
|
||||||
},
|
},
|
||||||
SourceRange, IMPORT_FILE_EXTENSIONS,
|
SourceRange, TypedPath, IMPORT_FILE_EXTENSIONS,
|
||||||
};
|
};
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
@ -436,7 +436,7 @@ fn pipe_expression(i: &mut TokenSlice) -> PResult<Node<PipeExpression>> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bool_value(i: &mut TokenSlice) -> PResult<BoxNode<Literal>> {
|
fn bool_value(i: &mut TokenSlice) -> PResult<Node<Literal>> {
|
||||||
let (value, token) = any
|
let (value, token) = any
|
||||||
.try_map(|token: Token| match token.token_type {
|
.try_map(|token: Token| match token.token_type {
|
||||||
TokenType::Keyword if token.value == "true" => Ok((true, token)),
|
TokenType::Keyword if token.value == "true" => Ok((true, token)),
|
||||||
@ -448,7 +448,7 @@ fn bool_value(i: &mut TokenSlice) -> PResult<BoxNode<Literal>> {
|
|||||||
})
|
})
|
||||||
.context(expected("a boolean literal (either true or false)"))
|
.context(expected("a boolean literal (either true or false)"))
|
||||||
.parse_next(i)?;
|
.parse_next(i)?;
|
||||||
Ok(Box::new(Node::new(
|
Ok(Node::new(
|
||||||
Literal {
|
Literal {
|
||||||
value: LiteralValue::Bool(value),
|
value: LiteralValue::Bool(value),
|
||||||
raw: value.to_string(),
|
raw: value.to_string(),
|
||||||
@ -457,11 +457,11 @@ fn bool_value(i: &mut TokenSlice) -> PResult<BoxNode<Literal>> {
|
|||||||
token.start,
|
token.start,
|
||||||
token.end,
|
token.end,
|
||||||
token.module_id,
|
token.module_id,
|
||||||
)))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn literal(i: &mut TokenSlice) -> PResult<BoxNode<Literal>> {
|
fn literal(i: &mut TokenSlice) -> PResult<BoxNode<Literal>> {
|
||||||
alt((string_literal, unsigned_number_literal))
|
alt((string_literal, unsigned_number_literal, bool_value))
|
||||||
.map(Box::new)
|
.map(Box::new)
|
||||||
.context(expected("a KCL literal, like 'myPart' or 3"))
|
.context(expected("a KCL literal, like 'myPart' or 3"))
|
||||||
.parse_next(i)
|
.parse_next(i)
|
||||||
@ -1729,7 +1729,7 @@ fn glob(i: &mut TokenSlice) -> PResult<Token> {
|
|||||||
.parse_next(i)
|
.parse_next(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn import_stmt(i: &mut TokenSlice) -> PResult<BoxNode<ImportStatement>> {
|
pub(super) fn import_stmt(i: &mut TokenSlice) -> PResult<BoxNode<ImportStatement>> {
|
||||||
let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace))
|
let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace))
|
||||||
.parse_next(i)?
|
.parse_next(i)?
|
||||||
.map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1)));
|
.map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1)));
|
||||||
@ -1862,18 +1862,50 @@ fn validate_path_string(path_string: String, var_name: bool, path_range: SourceR
|
|||||||
let path = if path_string.ends_with(".kcl") {
|
let path = if path_string.ends_with(".kcl") {
|
||||||
if path_string
|
if path_string
|
||||||
.chars()
|
.chars()
|
||||||
.any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != '.')
|
.any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != '.' && c != '/' && c != '\\')
|
||||||
{
|
{
|
||||||
return Err(ErrMode::Cut(
|
return Err(ErrMode::Cut(
|
||||||
CompilationError::fatal(
|
CompilationError::fatal(
|
||||||
path_range,
|
path_range,
|
||||||
"import path may only contain alphanumeric characters, underscore, hyphen, and period. KCL files in other directories are not yet supported.",
|
"import path may only contain alphanumeric characters, `_`, `-`, `.`, `/`, and `\\`.",
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportPath::Kcl { filename: path_string }
|
if path_string.starts_with("..") {
|
||||||
|
return Err(ErrMode::Cut(
|
||||||
|
CompilationError::fatal(
|
||||||
|
path_range,
|
||||||
|
"import path may not start with '..'. Cannot traverse to something outside the bounds of your project. If this path is inside your project please find a better way to reference it.",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure they are not using an absolute path.
|
||||||
|
if path_string.starts_with('/') || path_string.starts_with('\\') {
|
||||||
|
return Err(ErrMode::Cut(
|
||||||
|
CompilationError::fatal(
|
||||||
|
path_range,
|
||||||
|
"import path may not start with '/' or '\\'. Cannot traverse to something outside the bounds of your project. If this path is inside your project please find a better way to reference it.",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path_string.contains('/') || path_string.contains('\\'))
|
||||||
|
&& !(path_string.ends_with("/main.kcl") || path_string.ends_with("\\main.kcl"))
|
||||||
|
{
|
||||||
|
return Err(ErrMode::Cut(
|
||||||
|
CompilationError::fatal(path_range, "import path to a subdirectory must only refer to main.kcl.")
|
||||||
|
.into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportPath::Kcl {
|
||||||
|
filename: TypedPath::new(&path_string),
|
||||||
|
}
|
||||||
} else if path_string.starts_with("std::") {
|
} else if path_string.starts_with("std::") {
|
||||||
ParseContext::warn(CompilationError::err(
|
ParseContext::warn(CompilationError::err(
|
||||||
path_range,
|
path_range,
|
||||||
@ -1910,7 +1942,9 @@ fn validate_path_string(path_string: String, var_name: bool, path_range: SourceR
|
|||||||
format!("unsupported import path format. KCL files can be imported from the current project, CAD files with the following formats are supported: {}", IMPORT_FILE_EXTENSIONS.join(", ")),
|
format!("unsupported import path format. KCL files can be imported from the current project, CAD files with the following formats are supported: {}", IMPORT_FILE_EXTENSIONS.join(", ")),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
ImportPath::Foreign { path: path_string }
|
ImportPath::Foreign {
|
||||||
|
path: TypedPath::new(&path_string),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(ErrMode::Cut(
|
return Err(ErrMode::Cut(
|
||||||
CompilationError::fatal(
|
CompilationError::fatal(
|
||||||
@ -2051,7 +2085,7 @@ fn unnecessarily_bracketed(i: &mut TokenSlice) -> PResult<Expr> {
|
|||||||
fn expr_allowed_in_pipe_expr(i: &mut TokenSlice) -> PResult<Expr> {
|
fn expr_allowed_in_pipe_expr(i: &mut TokenSlice) -> PResult<Expr> {
|
||||||
alt((
|
alt((
|
||||||
member_expression.map(Box::new).map(Expr::MemberExpression),
|
member_expression.map(Box::new).map(Expr::MemberExpression),
|
||||||
bool_value.map(Expr::Literal),
|
bool_value.map(Box::new).map(Expr::Literal),
|
||||||
tag.map(Box::new).map(Expr::TagDeclarator),
|
tag.map(Box::new).map(Expr::TagDeclarator),
|
||||||
literal.map(Expr::Literal),
|
literal.map(Expr::Literal),
|
||||||
fn_call_kw.map(Box::new).map(Expr::CallExpressionKw),
|
fn_call_kw.map(Box::new).map(Expr::CallExpressionKw),
|
||||||
@ -2070,7 +2104,7 @@ fn expr_allowed_in_pipe_expr(i: &mut TokenSlice) -> PResult<Expr> {
|
|||||||
fn possible_operands(i: &mut TokenSlice) -> PResult<Expr> {
|
fn possible_operands(i: &mut TokenSlice) -> PResult<Expr> {
|
||||||
let mut expr = alt((
|
let mut expr = alt((
|
||||||
unary_expression.map(Box::new).map(Expr::UnaryExpression),
|
unary_expression.map(Box::new).map(Expr::UnaryExpression),
|
||||||
bool_value.map(Expr::Literal),
|
bool_value.map(Box::new).map(Expr::Literal),
|
||||||
member_expression.map(Box::new).map(Expr::MemberExpression),
|
member_expression.map(Box::new).map(Expr::MemberExpression),
|
||||||
literal.map(Expr::Literal),
|
literal.map(Expr::Literal),
|
||||||
fn_call_kw.map(Box::new).map(Expr::CallExpressionKw),
|
fn_call_kw.map(Box::new).map(Expr::CallExpressionKw),
|
||||||
@ -2780,27 +2814,31 @@ fn labeled_argument(i: &mut TokenSlice) -> PResult<LabeledArg> {
|
|||||||
.parse_next(i)
|
.parse_next(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn record_ty_field(i: &mut TokenSlice) -> PResult<(Node<Identifier>, Node<Type>)> {
|
||||||
|
(identifier, colon, opt(whitespace), type_)
|
||||||
|
.map(|(id, _, _, ty)| (id, ty))
|
||||||
|
.parse_next(i)
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a type in various positions.
|
/// Parse a type in various positions.
|
||||||
fn type_(i: &mut TokenSlice) -> PResult<Node<Type>> {
|
fn type_(i: &mut TokenSlice) -> PResult<Node<Type>> {
|
||||||
let type_ = alt((
|
let type_ = alt((
|
||||||
// Object types
|
// Object types
|
||||||
// TODO it is buggy to treat object fields like parameters since the parameters parser assumes a terminating `)`.
|
(
|
||||||
(open_brace, parameters, close_brace).try_map(|(open, params, close)| {
|
open_brace,
|
||||||
for p in ¶ms {
|
opt(whitespace),
|
||||||
if p.type_.is_none() {
|
separated(0.., record_ty_field, comma_sep),
|
||||||
return Err(CompilationError::fatal(
|
opt(whitespace),
|
||||||
p.identifier.as_source_range(),
|
close_brace,
|
||||||
"Missing type for field in record type",
|
)
|
||||||
));
|
.try_map(|(open, _, params, _, close)| {
|
||||||
}
|
Ok(Node::new(
|
||||||
}
|
Type::Object { properties: params },
|
||||||
Ok(Node::new(
|
open.start,
|
||||||
Type::Object { properties: params },
|
close.end,
|
||||||
open.start,
|
open.module_id,
|
||||||
close.end,
|
))
|
||||||
open.module_id,
|
}),
|
||||||
))
|
|
||||||
}),
|
|
||||||
// Array types
|
// Array types
|
||||||
array_type,
|
array_type,
|
||||||
// Primitive or union types
|
// Primitive or union types
|
||||||
@ -4530,9 +4568,24 @@ e
|
|||||||
fn bad_imports() {
|
fn bad_imports() {
|
||||||
assert_err(
|
assert_err(
|
||||||
r#"import cube from "../cube.kcl""#,
|
r#"import cube from "../cube.kcl""#,
|
||||||
"import path may only contain alphanumeric characters, underscore, hyphen, and period. KCL files in other directories are not yet supported.",
|
"import path may not start with '..'. Cannot traverse to something outside the bounds of your project. If this path is inside your project please find a better way to reference it.",
|
||||||
[17, 30],
|
[17, 30],
|
||||||
);
|
);
|
||||||
|
assert_err(
|
||||||
|
r#"import cube from "/cube.kcl""#,
|
||||||
|
"import path may not start with '/' or '\\'. Cannot traverse to something outside the bounds of your project. If this path is inside your project please find a better way to reference it.",
|
||||||
|
[17, 28],
|
||||||
|
);
|
||||||
|
assert_err(
|
||||||
|
r#"import cube from "C:\cube.kcl""#,
|
||||||
|
"import path may only contain alphanumeric characters, `_`, `-`, `.`, `/`, and `\\`.",
|
||||||
|
[17, 30],
|
||||||
|
);
|
||||||
|
assert_err(
|
||||||
|
r#"import cube from "cube/cube.kcl""#,
|
||||||
|
"import path to a subdirectory must only refer to main.kcl.",
|
||||||
|
[17, 32],
|
||||||
|
);
|
||||||
assert_err(
|
assert_err(
|
||||||
r#"import * as foo from "dsfs""#,
|
r#"import * as foo from "dsfs""#,
|
||||||
"as is not the 'from' keyword",
|
"as is not the 'from' keyword",
|
||||||
@ -4866,6 +4919,15 @@ let myBox = box(p=[0,0], h=-3, l=-16, w=-10)
|
|||||||
|> line(%, tag = $var01)"#;
|
|> line(%, tag = $var01)"#;
|
||||||
assert_no_err(some_program_string);
|
assert_no_err(some_program_string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_param_bool_default() {
|
||||||
|
let some_program_string = r#"fn patternTransform(
|
||||||
|
use_original?: boolean = false,
|
||||||
|
) {}"#;
|
||||||
|
assert_no_err(some_program_string);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_function_types() {
|
fn parse_function_types() {
|
||||||
let code = r#"foo = x: fn
|
let code = r#"foo = x: fn
|
||||||
@ -4875,6 +4937,8 @@ fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn { return 0 }
|
|||||||
type fn
|
type fn
|
||||||
type foo = fn
|
type foo = fn
|
||||||
type foo = fn(a: string, b: { f: fn(): any })
|
type foo = fn(a: string, b: { f: fn(): any })
|
||||||
|
type foo = fn(a: string, b: {})
|
||||||
|
type foo = fn(a: string, b: { })
|
||||||
type foo = fn([fn])
|
type foo = fn([fn])
|
||||||
type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
|
type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
|
||||||
"#;
|
"#;
|
||||||
|
@ -578,10 +578,10 @@ impl From<ParseError<Input<'_>, winnow::error::ContextError>> for KclError {
|
|||||||
// This is an offset, not an index, and may point to
|
// This is an offset, not an index, and may point to
|
||||||
// the end of input (input.len()) on eof errors.
|
// the end of input (input.len()) on eof errors.
|
||||||
|
|
||||||
return KclError::Lexical(crate::errors::KclErrorDetails {
|
return KclError::Lexical(crate::errors::KclErrorDetails::new(
|
||||||
source_ranges: vec![SourceRange::new(offset, offset, module_id)],
|
"unexpected EOF while parsing".to_owned(),
|
||||||
message: "unexpected EOF while parsing".to_string(),
|
vec![SourceRange::new(offset, offset, module_id)],
|
||||||
});
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add the Winnow tokenizer context to the error.
|
// TODO: Add the Winnow tokenizer context to the error.
|
||||||
@ -589,9 +589,9 @@ impl From<ParseError<Input<'_>, winnow::error::ContextError>> for KclError {
|
|||||||
let bad_token = &input[offset];
|
let bad_token = &input[offset];
|
||||||
// TODO: Add the Winnow parser context to the error.
|
// TODO: Add the Winnow parser context to the error.
|
||||||
// See https://github.com/KittyCAD/modeling-app/issues/784
|
// See https://github.com/KittyCAD/modeling-app/issues/784
|
||||||
KclError::Lexical(crate::errors::KclErrorDetails {
|
KclError::Lexical(crate::errors::KclErrorDetails::new(
|
||||||
source_ranges: vec![SourceRange::new(offset, offset + 1, module_id)],
|
format!("found unknown token '{}'", bad_token),
|
||||||
message: format!("found unknown token '{}'", bad_token),
|
vec![SourceRange::new(offset, offset + 1, module_id)],
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -384,6 +384,27 @@ mod any_type {
|
|||||||
super::execute(TEST_NAME, false).await
|
super::execute(TEST_NAME, false).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mod error_with_point_shows_numeric_units {
|
||||||
|
const TEST_NAME: &str = "error_with_point_shows_numeric_units";
|
||||||
|
|
||||||
|
/// Test parsing KCL.
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
super::parse(TEST_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn unparse() {
|
||||||
|
super::unparse(TEST_NAME).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that KCL is executed correctly.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn kcl_test_execute() {
|
||||||
|
super::execute(TEST_NAME, true).await
|
||||||
|
}
|
||||||
|
}
|
||||||
mod artifact_graph_example_code1 {
|
mod artifact_graph_example_code1 {
|
||||||
const TEST_NAME: &str = "artifact_graph_example_code1";
|
const TEST_NAME: &str = "artifact_graph_example_code1";
|
||||||
|
|
||||||
@ -996,6 +1017,27 @@ mod import_cycle1 {
|
|||||||
super::execute(TEST_NAME, false).await
|
super::execute(TEST_NAME, false).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mod import_only_at_top_level {
|
||||||
|
const TEST_NAME: &str = "import_only_at_top_level";
|
||||||
|
|
||||||
|
/// Test parsing KCL.
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
super::parse(TEST_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn unparse() {
|
||||||
|
super::unparse(TEST_NAME).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that KCL is executed correctly.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn kcl_test_execute() {
|
||||||
|
super::execute(TEST_NAME, false).await
|
||||||
|
}
|
||||||
|
}
|
||||||
mod import_function_not_sketch {
|
mod import_function_not_sketch {
|
||||||
const TEST_NAME: &str = "import_function_not_sketch";
|
const TEST_NAME: &str = "import_function_not_sketch";
|
||||||
|
|
||||||
@ -1164,6 +1206,27 @@ mod import_foreign {
|
|||||||
super::execute(TEST_NAME, false).await
|
super::execute(TEST_NAME, false).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mod export_var_only_at_top_level {
|
||||||
|
const TEST_NAME: &str = "export_var_only_at_top_level";
|
||||||
|
|
||||||
|
/// Test parsing KCL.
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
super::parse(TEST_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn unparse() {
|
||||||
|
super::unparse(TEST_NAME).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that KCL is executed correctly.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn kcl_test_execute() {
|
||||||
|
super::execute(TEST_NAME, false).await
|
||||||
|
}
|
||||||
|
}
|
||||||
mod assembly_non_default_units {
|
mod assembly_non_default_units {
|
||||||
const TEST_NAME: &str = "assembly_non_default_units";
|
const TEST_NAME: &str = "assembly_non_default_units";
|
||||||
|
|
||||||
@ -3188,7 +3251,6 @@ mod revolve_colinear {
|
|||||||
|
|
||||||
/// Test that KCL is executed correctly.
|
/// Test that KCL is executed correctly.
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
#[ignore] // until https://github.com/KittyCAD/engine/pull/3417 lands
|
|
||||||
async fn kcl_test_execute() {
|
async fn kcl_test_execute() {
|
||||||
super::execute(TEST_NAME, true).await
|
super::execute(TEST_NAME, true).await
|
||||||
}
|
}
|
||||||
@ -3277,3 +3339,108 @@ mod subtract_regression10 {
|
|||||||
super::execute(TEST_NAME, true).await
|
super::execute(TEST_NAME, true).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mod nested_main_kcl {
|
||||||
|
const TEST_NAME: &str = "nested_main_kcl";
|
||||||
|
|
||||||
|
/// Test parsing KCL.
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
super::parse(TEST_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn unparse() {
|
||||||
|
super::unparse(TEST_NAME).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that KCL is executed correctly.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn kcl_test_execute() {
|
||||||
|
super::execute(TEST_NAME, true).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mod nested_windows_main_kcl {
|
||||||
|
const TEST_NAME: &str = "nested_windows_main_kcl";
|
||||||
|
|
||||||
|
/// Test parsing KCL.
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
super::parse(TEST_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn unparse() {
|
||||||
|
super::unparse(TEST_NAME).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that KCL is executed correctly.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn kcl_test_execute() {
|
||||||
|
super::execute(TEST_NAME, true).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mod nested_assembly {
|
||||||
|
const TEST_NAME: &str = "nested_assembly";
|
||||||
|
|
||||||
|
/// Test parsing KCL.
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
super::parse(TEST_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn unparse() {
|
||||||
|
super::unparse(TEST_NAME).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that KCL is executed correctly.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn kcl_test_execute() {
|
||||||
|
super::execute(TEST_NAME, true).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mod subtract_regression11 {
|
||||||
|
const TEST_NAME: &str = "subtract_regression11";
|
||||||
|
|
||||||
|
/// Test parsing KCL.
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
super::parse(TEST_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn unparse() {
|
||||||
|
super::unparse(TEST_NAME).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that KCL is executed correctly.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn kcl_test_execute() {
|
||||||
|
super::execute(TEST_NAME, true).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mod subtract_regression12 {
|
||||||
|
const TEST_NAME: &str = "subtract_regression12";
|
||||||
|
|
||||||
|
/// Test parsing KCL.
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
super::parse(TEST_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn unparse() {
|
||||||
|
super::unparse(TEST_NAME).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that KCL is executed correctly.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn kcl_test_execute() {
|
||||||
|
super::execute(TEST_NAME, true).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -32,10 +32,10 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
|
|||||||
|
|
||||||
// Make sure the color if set is valid.
|
// Make sure the color if set is valid.
|
||||||
if !HEX_REGEX.is_match(&color) {
|
if !HEX_REGEX.is_match(&color) {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Invalid hex color (`{}`), try something like `#fff000`", color),
|
format!("Invalid hex color (`{}`), try something like `#fff000`", color),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = inner_appearance(
|
let result = inner_appearance(
|
||||||
@ -282,10 +282,10 @@ async fn inner_appearance(
|
|||||||
for solid_id in solids.ids(&args.ctx).await? {
|
for solid_id in solids.ids(&args.ctx).await? {
|
||||||
// Set the material properties.
|
// Set the material properties.
|
||||||
let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
|
let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Invalid hex color (`{color}`): {err}"),
|
format!("Invalid hex color (`{color}`): {err}"),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let color = Color {
|
let color = Color {
|
||||||
|
@ -9,6 +9,7 @@ use kittycad_modeling_cmds as kcmc;
|
|||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub use crate::execution::fn_call::Args;
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
execution::{
|
execution::{
|
||||||
@ -27,8 +28,6 @@ use crate::{
|
|||||||
ModuleId,
|
ModuleId,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::execution::fn_call::Args;
|
|
||||||
|
|
||||||
const ERROR_STRING_SKETCH_TO_SOLID_HELPER: &str =
|
const ERROR_STRING_SKETCH_TO_SOLID_HELPER: &str =
|
||||||
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`";
|
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`";
|
||||||
|
|
||||||
@ -122,14 +121,14 @@ impl Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
T::from_kcl_val(&arg.value).map(Some).ok_or_else(|| {
|
T::from_kcl_val(&arg.value).map(Some).ok_or_else(|| {
|
||||||
KclError::Type(KclErrorDetails {
|
KclError::Type(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.source_range],
|
format!(
|
||||||
message: format!(
|
|
||||||
"The arg {label} was given, but it was the wrong type. It should be type {} but it was {}",
|
"The arg {label} was given, but it was the wrong type. It should be type {} but it was {}",
|
||||||
tynm::type_name::<T>(),
|
tynm::type_name::<T>(),
|
||||||
arg.value.human_friendly_type(),
|
arg.value.human_friendly_type(),
|
||||||
),
|
),
|
||||||
})
|
vec![self.source_range],
|
||||||
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,10 +154,10 @@ impl Args {
|
|||||||
T: FromKclValue<'a>,
|
T: FromKclValue<'a>,
|
||||||
{
|
{
|
||||||
self.get_kw_arg_opt(label)?.ok_or_else(|| {
|
self.get_kw_arg_opt(label)?.ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.source_range],
|
format!("This function requires a keyword argument '{label}'"),
|
||||||
message: format!("This function requires a keyword argument '{label}'"),
|
vec![self.source_range],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,10 +171,10 @@ impl Args {
|
|||||||
T: for<'a> FromKclValue<'a>,
|
T: for<'a> FromKclValue<'a>,
|
||||||
{
|
{
|
||||||
let Some(arg) = self.kw_args.labeled.get(label) else {
|
let Some(arg) = self.kw_args.labeled.get(label) else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.source_range],
|
format!("This function requires a keyword argument '{label}'"),
|
||||||
message: format!("This function requires a keyword argument '{label}'"),
|
vec![self.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let arg = arg.value.coerce(ty, exec_state).map_err(|_| {
|
let arg = arg.value.coerce(ty, exec_state).map_err(|_| {
|
||||||
@ -206,10 +205,7 @@ impl Args {
|
|||||||
if message.contains("one or more Solids or imported geometry but it's actually of type Sketch") {
|
if message.contains("one or more Solids or imported geometry but it's actually of type Sketch") {
|
||||||
message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
|
message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
|
||||||
}
|
}
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(message, arg.source_ranges()))
|
||||||
source_ranges: arg.source_ranges(),
|
|
||||||
message,
|
|
||||||
})
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// TODO unnecessary cloning
|
// TODO unnecessary cloning
|
||||||
@ -223,21 +219,21 @@ impl Args {
|
|||||||
T: FromKclValue<'a>,
|
T: FromKclValue<'a>,
|
||||||
{
|
{
|
||||||
let Some(arg) = self.kw_args.labeled.get(label) else {
|
let Some(arg) = self.kw_args.labeled.get(label) else {
|
||||||
let err = KclError::Semantic(KclErrorDetails {
|
let err = KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.source_range],
|
format!("This function requires a keyword argument '{label}'"),
|
||||||
message: format!("This function requires a keyword argument '{label}'"),
|
vec![self.source_range],
|
||||||
});
|
));
|
||||||
return Err(err);
|
return Err(err);
|
||||||
};
|
};
|
||||||
let Some(array) = arg.value.as_array() else {
|
let Some(array) = arg.value.as_array() else {
|
||||||
let err = KclError::Semantic(KclErrorDetails {
|
let err = KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![arg.source_range],
|
format!(
|
||||||
message: format!(
|
|
||||||
"Expected an array of {} but found {}",
|
"Expected an array of {} but found {}",
|
||||||
tynm::type_name::<T>(),
|
tynm::type_name::<T>(),
|
||||||
arg.value.human_friendly_type()
|
arg.value.human_friendly_type()
|
||||||
),
|
),
|
||||||
});
|
vec![arg.source_range],
|
||||||
|
));
|
||||||
return Err(err);
|
return Err(err);
|
||||||
};
|
};
|
||||||
array
|
array
|
||||||
@ -245,14 +241,14 @@ impl Args {
|
|||||||
.map(|item| {
|
.map(|item| {
|
||||||
let source = SourceRange::from(item);
|
let source = SourceRange::from(item);
|
||||||
let val = FromKclValue::from_kcl_val(item).ok_or_else(|| {
|
let val = FromKclValue::from_kcl_val(item).ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: arg.source_ranges(),
|
format!(
|
||||||
message: format!(
|
|
||||||
"Expected a {} but found {}",
|
"Expected a {} but found {}",
|
||||||
tynm::type_name::<T>(),
|
tynm::type_name::<T>(),
|
||||||
arg.value.human_friendly_type()
|
arg.value.human_friendly_type()
|
||||||
),
|
),
|
||||||
})
|
arg.source_ranges(),
|
||||||
|
))
|
||||||
})?;
|
})?;
|
||||||
Ok((val, source))
|
Ok((val, source))
|
||||||
})
|
})
|
||||||
@ -267,19 +263,19 @@ impl Args {
|
|||||||
{
|
{
|
||||||
let arg = self
|
let arg = self
|
||||||
.unlabeled_kw_arg_unconverted()
|
.unlabeled_kw_arg_unconverted()
|
||||||
.ok_or(KclError::Semantic(KclErrorDetails {
|
.ok_or(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.source_range],
|
format!("This function requires a value for the special unlabeled first parameter, '{label}'"),
|
||||||
message: format!("This function requires a value for the special unlabeled first parameter, '{label}'"),
|
vec![self.source_range],
|
||||||
}))?;
|
)))?;
|
||||||
|
|
||||||
T::from_kcl_val(&arg.value).ok_or_else(|| {
|
T::from_kcl_val(&arg.value).ok_or_else(|| {
|
||||||
let expected_type_name = tynm::type_name::<T>();
|
let expected_type_name = tynm::type_name::<T>();
|
||||||
let actual_type_name = arg.value.human_friendly_type();
|
let actual_type_name = arg.value.human_friendly_type();
|
||||||
let message = format!("This function expected the input argument to be of type {expected_type_name} but it's actually of type {actual_type_name}");
|
let message = format!("This function expected the input argument to be of type {expected_type_name} but it's actually of type {actual_type_name}");
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: arg.source_ranges(),
|
|
||||||
message,
|
message,
|
||||||
})
|
arg.source_ranges(),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,10 +292,10 @@ impl Args {
|
|||||||
{
|
{
|
||||||
let arg = self
|
let arg = self
|
||||||
.unlabeled_kw_arg_unconverted()
|
.unlabeled_kw_arg_unconverted()
|
||||||
.ok_or(KclError::Semantic(KclErrorDetails {
|
.ok_or(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.source_range],
|
format!("This function requires a value for the special unlabeled first parameter, '{label}'"),
|
||||||
message: format!("This function requires a value for the special unlabeled first parameter, '{label}'"),
|
vec![self.source_range],
|
||||||
}))?;
|
)))?;
|
||||||
|
|
||||||
let arg = arg.value.coerce(ty, exec_state).map_err(|_| {
|
let arg = arg.value.coerce(ty, exec_state).map_err(|_| {
|
||||||
let actual_type = arg.value.principal_type();
|
let actual_type = arg.value.principal_type();
|
||||||
@ -330,17 +326,14 @@ impl Args {
|
|||||||
if message.contains("one or more Solids or imported geometry but it's actually of type Sketch") {
|
if message.contains("one or more Solids or imported geometry but it's actually of type Sketch") {
|
||||||
message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
|
message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
|
||||||
}
|
}
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(message, arg.source_ranges()))
|
||||||
source_ranges: arg.source_ranges(),
|
|
||||||
message,
|
|
||||||
})
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
T::from_kcl_val(&arg).ok_or_else(|| {
|
T::from_kcl_val(&arg).ok_or_else(|| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.source_range],
|
"Mismatch between type coercion and value extraction (this isn't your fault).\nTo assist in bug-reporting, expected type: {ty:?}; actual value: {arg:?}".to_owned(),
|
||||||
message: "Mismatch between type coercion and value extraction (this isn't your fault).\nTo assist in bug-reporting, expected type: {ty:?}; actual value: {arg:?}".to_owned(),
|
vec![self.source_range],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,17 +376,17 @@ impl Args {
|
|||||||
exec_state.stack().get_from_call_stack(&tag.value, self.source_range)?
|
exec_state.stack().get_from_call_stack(&tag.value, self.source_range)?
|
||||||
{
|
{
|
||||||
let info = t.get_info(epoch).ok_or_else(|| {
|
let info = t.get_info(epoch).ok_or_else(|| {
|
||||||
KclError::Type(KclErrorDetails {
|
KclError::Type(KclErrorDetails::new(
|
||||||
message: format!("Tag `{}` does not have engine info", tag.value),
|
format!("Tag `{}` does not have engine info", tag.value),
|
||||||
source_ranges: vec![self.source_range],
|
vec![self.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
Ok(info)
|
Ok(info)
|
||||||
} else {
|
} else {
|
||||||
Err(KclError::Type(KclErrorDetails {
|
Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: format!("Tag `{}` does not exist", tag.value),
|
format!("Tag `{}` does not exist", tag.value),
|
||||||
source_ranges: vec![self.source_range],
|
vec![self.source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -522,19 +515,19 @@ impl Args {
|
|||||||
must_be_planar: bool,
|
must_be_planar: bool,
|
||||||
) -> Result<uuid::Uuid, KclError> {
|
) -> Result<uuid::Uuid, KclError> {
|
||||||
if tag.value.is_empty() {
|
if tag.value.is_empty() {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: "Expected a non-empty tag for the face".to_string(),
|
"Expected a non-empty tag for the face".to_string(),
|
||||||
source_ranges: vec![self.source_range],
|
vec![self.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let engine_info = self.get_tag_engine_info_check_surface(exec_state, tag)?;
|
let engine_info = self.get_tag_engine_info_check_surface(exec_state, tag)?;
|
||||||
|
|
||||||
let surface = engine_info.surface.as_ref().ok_or_else(|| {
|
let surface = engine_info.surface.as_ref().ok_or_else(|| {
|
||||||
KclError::Type(KclErrorDetails {
|
KclError::Type(KclErrorDetails::new(
|
||||||
message: format!("Tag `{}` does not have a surface", tag.value),
|
format!("Tag `{}` does not have a surface", tag.value),
|
||||||
source_ranges: vec![self.source_range],
|
vec![self.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if let Some(face_from_surface) = match surface {
|
if let Some(face_from_surface) = match surface {
|
||||||
@ -550,10 +543,10 @@ impl Args {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The must be planar check must be called before the arc check.
|
// The must be planar check must be called before the arc check.
|
||||||
ExtrudeSurface::ExtrudeArc(_) if must_be_planar => Some(Err(KclError::Type(KclErrorDetails {
|
ExtrudeSurface::ExtrudeArc(_) if must_be_planar => Some(Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: format!("Tag `{}` is a non-planar surface", tag.value),
|
format!("Tag `{}` is a non-planar surface", tag.value),
|
||||||
source_ranges: vec![self.source_range],
|
vec![self.source_range],
|
||||||
}))),
|
)))),
|
||||||
ExtrudeSurface::ExtrudeArc(extrude_arc) => {
|
ExtrudeSurface::ExtrudeArc(extrude_arc) => {
|
||||||
if let Some(arc_tag) = &extrude_arc.tag {
|
if let Some(arc_tag) = &extrude_arc.tag {
|
||||||
if arc_tag.name == tag.value {
|
if arc_tag.name == tag.value {
|
||||||
@ -577,10 +570,10 @@ impl Args {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The must be planar check must be called before the fillet check.
|
// The must be planar check must be called before the fillet check.
|
||||||
ExtrudeSurface::Fillet(_) if must_be_planar => Some(Err(KclError::Type(KclErrorDetails {
|
ExtrudeSurface::Fillet(_) if must_be_planar => Some(Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: format!("Tag `{}` is a non-planar surface", tag.value),
|
format!("Tag `{}` is a non-planar surface", tag.value),
|
||||||
source_ranges: vec![self.source_range],
|
vec![self.source_range],
|
||||||
}))),
|
)))),
|
||||||
ExtrudeSurface::Fillet(fillet) => {
|
ExtrudeSurface::Fillet(fillet) => {
|
||||||
if let Some(fillet_tag) = &fillet.tag {
|
if let Some(fillet_tag) = &fillet.tag {
|
||||||
if fillet_tag.name == tag.value {
|
if fillet_tag.name == tag.value {
|
||||||
@ -597,10 +590,10 @@ impl Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we still haven't found the face, return an error.
|
// If we still haven't found the face, return an error.
|
||||||
Err(KclError::Type(KclErrorDetails {
|
Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: format!("Expected a face with the tag `{}`", tag.value),
|
format!("Expected a face with the tag `{}`", tag.value),
|
||||||
source_ranges: vec![self.source_range],
|
vec![self.source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -622,20 +615,20 @@ where
|
|||||||
{
|
{
|
||||||
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
|
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
|
||||||
let Some(arg) = args.args.get(i) else {
|
let Some(arg) = args.args.get(i) else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Expected an argument at index {i}"),
|
format!("Expected an argument at index {i}"),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
let Some(val) = T::from_kcl_val(&arg.value) else {
|
let Some(val) = T::from_kcl_val(&arg.value) else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Argument at index {i} was supposed to be type {} but found {}",
|
"Argument at index {i} was supposed to be type {} but found {}",
|
||||||
tynm::type_name::<T>(),
|
tynm::type_name::<T>(),
|
||||||
arg.value.human_friendly_type(),
|
arg.value.human_friendly_type(),
|
||||||
),
|
),
|
||||||
source_ranges: arg.source_ranges(),
|
arg.source_ranges(),
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
Ok(val)
|
Ok(val)
|
||||||
}
|
}
|
||||||
@ -651,14 +644,14 @@ where
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let Some(val) = T::from_kcl_val(&arg.value) else {
|
let Some(val) = T::from_kcl_val(&arg.value) else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Argument at index {i} was supposed to be type Option<{}> but found {}",
|
"Argument at index {i} was supposed to be type Option<{}> but found {}",
|
||||||
tynm::type_name::<T>(),
|
tynm::type_name::<T>(),
|
||||||
arg.value.human_friendly_type()
|
arg.value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: arg.source_ranges(),
|
arg.source_ranges(),
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
Ok(Some(val))
|
Ok(Some(val))
|
||||||
}
|
}
|
||||||
|
@ -58,10 +58,10 @@ async fn call_map_closure(
|
|||||||
let output = map_fn.call_kw(None, exec_state, ctxt, args, source_range).await?;
|
let output = map_fn.call_kw(None, exec_state, ctxt, args, source_range).await?;
|
||||||
let source_ranges = vec![source_range];
|
let source_ranges = vec![source_range];
|
||||||
let output = output.ok_or_else(|| {
|
let output = output.ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Map function must return a value".to_string(),
|
"Map function must return a value".to_owned(),
|
||||||
source_ranges,
|
source_ranges,
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
@ -118,10 +118,10 @@ async fn call_reduce_closure(
|
|||||||
// Unpack the returned transform object.
|
// Unpack the returned transform object.
|
||||||
let source_ranges = vec![source_range];
|
let source_ranges = vec![source_range];
|
||||||
let out = transform_fn_return.ok_or_else(|| {
|
let out = transform_fn_return.ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Reducer function must return a value".to_string(),
|
"Reducer function must return a value".to_string(),
|
||||||
source_ranges: source_ranges.clone(),
|
source_ranges.clone(),
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
@ -133,10 +133,10 @@ pub async fn push(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
let KclValue::HomArray { value: values, ty } = array else {
|
let KclValue::HomArray { value: values, ty } = array else {
|
||||||
let meta = vec![args.source_range];
|
let meta = vec![args.source_range];
|
||||||
let actual_type = array.human_friendly_type();
|
let actual_type = array.human_friendly_type();
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: meta,
|
format!("You can't push to a value of type {actual_type}, only an array"),
|
||||||
message: format!("You can't push to a value of type {actual_type}, only an array"),
|
meta,
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
let ty = if item.has_type(&ty) {
|
let ty = if item.has_type(&ty) {
|
||||||
ty
|
ty
|
||||||
@ -161,10 +161,10 @@ pub async fn pop(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
|||||||
let KclValue::HomArray { value: values, ty } = array else {
|
let KclValue::HomArray { value: values, ty } = array else {
|
||||||
let meta = vec![args.source_range];
|
let meta = vec![args.source_range];
|
||||||
let actual_type = array.human_friendly_type();
|
let actual_type = array.human_friendly_type();
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: meta,
|
format!("You can't pop from a value of type {actual_type}, only an array"),
|
||||||
message: format!("You can't pop from a value of type {actual_type}, only an array"),
|
meta,
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_array = inner_pop(values, &args)?;
|
let new_array = inner_pop(values, &args)?;
|
||||||
@ -173,10 +173,10 @@ pub async fn pop(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
|||||||
|
|
||||||
fn inner_pop(array: Vec<KclValue>, args: &Args) -> Result<Vec<KclValue>, KclError> {
|
fn inner_pop(array: Vec<KclValue>, args: &Args) -> Result<Vec<KclValue>, KclError> {
|
||||||
if array.is_empty() {
|
if array.is_empty() {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Cannot pop from an empty array".to_string(),
|
"Cannot pop from an empty array".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new array with all elements except the last one
|
// Create a new array with all elements except the last one
|
||||||
|
@ -12,10 +12,10 @@ use crate::{
|
|||||||
|
|
||||||
async fn _assert(value: bool, message: &str, args: &Args) -> Result<(), KclError> {
|
async fn _assert(value: bool, message: &str, args: &Args) -> Result<(), KclError> {
|
||||||
if !value {
|
if !value {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: format!("assert failed: {}", message),
|
format!("assert failed: {}", message),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -111,19 +111,18 @@ async fn inner_assert(
|
|||||||
.iter()
|
.iter()
|
||||||
.all(|cond| cond.is_none());
|
.all(|cond| cond.is_none());
|
||||||
if no_condition_given {
|
if no_condition_given {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: "You must provide at least one condition in this assert (for example, isEqualTo)".to_owned(),
|
"You must provide at least one condition in this assert (for example, isEqualTo)".to_owned(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if tolerance.is_some() && is_equal_to.is_none() {
|
if tolerance.is_some() && is_equal_to.is_none() {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails::new(
|
||||||
message:
|
"The `tolerance` arg is only used with `isEqualTo`. Either remove `tolerance` or add an `isEqualTo` arg."
|
||||||
"The `tolerance` arg is only used with `isEqualTo`. Either remove `tolerance` or add an `isEqualTo` arg."
|
.to_owned(),
|
||||||
.to_owned(),
|
vec![args.source_range],
|
||||||
source_ranges: vec![args.source_range],
|
)));
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let suffix = if let Some(err_string) = error {
|
let suffix = if let Some(err_string) = error {
|
||||||
|
@ -41,10 +41,10 @@ async fn inner_chamfer(
|
|||||||
// If you try and tag multiple edges with a tagged chamfer, we want to return an
|
// If you try and tag multiple edges with a tagged chamfer, we want to return an
|
||||||
// error to the user that they can only tag one edge at a time.
|
// error to the user that they can only tag one edge at a time.
|
||||||
if tag.is_some() && tags.len() > 1 {
|
if tag.is_some() && tags.len() > 1 {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: "You can only tag one edge at a time with a tagged chamfer. Either delete the tag for the chamfer fn if you don't need it OR separate into individual chamfer functions for each tag.".to_string(),
|
"You can only tag one edge at a time with a tagged chamfer. Either delete the tag for the chamfer fn if you don't need it OR separate into individual chamfer functions for each tag.".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut solid = solid.clone();
|
let mut solid = solid.clone();
|
||||||
|
@ -84,10 +84,10 @@ async fn inner_clone(
|
|||||||
fix_tags_and_references(&mut new_geometry, old_id, exec_state, &args)
|
fix_tags_and_references(&mut new_geometry, old_id, exec_state, &args)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("failed to fix tags and references: {:?}", e),
|
format!("failed to fix tags and references: {:?}", e),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(new_geometry)
|
Ok(new_geometry)
|
||||||
|
@ -24,10 +24,10 @@ pub async fn union(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
|
let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
|
||||||
|
|
||||||
if solids.len() < 2 {
|
if solids.len() < 2 {
|
||||||
return Err(KclError::UndefinedValue(KclErrorDetails {
|
return Err(KclError::UndefinedValue(KclErrorDetails::new(
|
||||||
message: "At least two solids are required for a union operation.".to_string(),
|
"At least two solids are required for a union operation.".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let solids = inner_union(solids, tolerance, exec_state, args).await?;
|
let solids = inner_union(solids, tolerance, exec_state, args).await?;
|
||||||
@ -147,10 +147,10 @@ pub(crate) async fn inner_union(
|
|||||||
modeling_response: OkModelingCmdResponse::BooleanUnion(BooleanUnion { extra_solid_ids }),
|
modeling_response: OkModelingCmdResponse::BooleanUnion(BooleanUnion { extra_solid_ids }),
|
||||||
} = result
|
} = result
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Internal(KclErrorDetails {
|
return Err(KclError::Internal(KclErrorDetails::new(
|
||||||
message: "Failed to get the result of the union operation.".to_string(),
|
"Failed to get the result of the union operation.".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
// If we have more solids, set those as well.
|
// If we have more solids, set those as well.
|
||||||
@ -169,10 +169,10 @@ pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValu
|
|||||||
let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
|
let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
|
||||||
|
|
||||||
if solids.len() < 2 {
|
if solids.len() < 2 {
|
||||||
return Err(KclError::UndefinedValue(KclErrorDetails {
|
return Err(KclError::UndefinedValue(KclErrorDetails::new(
|
||||||
message: "At least two solids are required for an intersect operation.".to_string(),
|
"At least two solids are required for an intersect operation.".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let solids = inner_intersect(solids, tolerance, exec_state, args).await?;
|
let solids = inner_intersect(solids, tolerance, exec_state, args).await?;
|
||||||
@ -273,10 +273,10 @@ pub(crate) async fn inner_intersect(
|
|||||||
modeling_response: OkModelingCmdResponse::BooleanIntersection(BooleanIntersection { extra_solid_ids }),
|
modeling_response: OkModelingCmdResponse::BooleanIntersection(BooleanIntersection { extra_solid_ids }),
|
||||||
} = result
|
} = result
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Internal(KclErrorDetails {
|
return Err(KclError::Internal(KclErrorDetails::new(
|
||||||
message: "Failed to get the result of the intersection operation.".to_string(),
|
"Failed to get the result of the intersection operation.".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
// If we have more solids, set those as well.
|
// If we have more solids, set those as well.
|
||||||
@ -397,10 +397,10 @@ pub(crate) async fn inner_subtract(
|
|||||||
modeling_response: OkModelingCmdResponse::BooleanSubtract(BooleanSubtract { extra_solid_ids }),
|
modeling_response: OkModelingCmdResponse::BooleanSubtract(BooleanSubtract { extra_solid_ids }),
|
||||||
} = result
|
} = result
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Internal(KclErrorDetails {
|
return Err(KclError::Internal(KclErrorDetails::new(
|
||||||
message: "Failed to get the result of the subtract operation.".to_string(),
|
"Failed to get the result of the subtract operation.".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
// If we have more solids, set those as well.
|
// If we have more solids, set those as well.
|
||||||
|
@ -87,10 +87,10 @@ async fn inner_get_opposite_edge(
|
|||||||
modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
|
modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
|
||||||
} = &resp
|
} = &resp
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {:?}", resp),
|
format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {:?}", resp),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(opposite_edge.edge)
|
Ok(opposite_edge.edge)
|
||||||
@ -172,20 +172,20 @@ async fn inner_get_next_adjacent_edge(
|
|||||||
modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
|
modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
|
||||||
} = &resp
|
} = &resp
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {:?}",
|
"mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {:?}",
|
||||||
resp
|
resp
|
||||||
),
|
),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
adjacent_edge.edge.ok_or_else(|| {
|
adjacent_edge.edge.ok_or_else(|| {
|
||||||
KclError::Type(KclErrorDetails {
|
KclError::Type(KclErrorDetails::new(
|
||||||
message: format!("No edge found next adjacent to tag: `{}`", edge.value),
|
format!("No edge found next adjacent to tag: `{}`", edge.value),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,20 +264,20 @@ async fn inner_get_previous_adjacent_edge(
|
|||||||
modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
|
modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
|
||||||
} = &resp
|
} = &resp
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {:?}",
|
"mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {:?}",
|
||||||
resp
|
resp
|
||||||
),
|
),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
adjacent_edge.edge.ok_or_else(|| {
|
adjacent_edge.edge.ok_or_else(|| {
|
||||||
KclError::Type(KclErrorDetails {
|
KclError::Type(KclErrorDetails::new(
|
||||||
message: format!("No edge found previous adjacent to tag: `{}`", edge.value),
|
format!("No edge found previous adjacent to tag: `{}`", edge.value),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,10 +336,10 @@ async fn inner_get_common_edge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if faces.len() != 2 {
|
if faces.len() != 2 {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: "getCommonEdge requires exactly two tags for faces".to_string(),
|
"getCommonEdge requires exactly two tags for faces".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
let first_face_id = args.get_adjacent_face_to_tag(exec_state, &faces[0], false).await?;
|
let first_face_id = args.get_adjacent_face_to_tag(exec_state, &faces[0], false).await?;
|
||||||
let second_face_id = args.get_adjacent_face_to_tag(exec_state, &faces[1], false).await?;
|
let second_face_id = args.get_adjacent_face_to_tag(exec_state, &faces[1], false).await?;
|
||||||
@ -348,10 +348,10 @@ async fn inner_get_common_edge(
|
|||||||
let second_tagged_path = args.get_tag_engine_info(exec_state, &faces[1])?;
|
let second_tagged_path = args.get_tag_engine_info(exec_state, &faces[1])?;
|
||||||
|
|
||||||
if first_tagged_path.sketch != second_tagged_path.sketch {
|
if first_tagged_path.sketch != second_tagged_path.sketch {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: "getCommonEdge requires the faces to be in the same original sketch".to_string(),
|
"getCommonEdge requires the faces to be in the same original sketch".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush the batch for our fillets/chamfers if there are any.
|
// Flush the batch for our fillets/chamfers if there are any.
|
||||||
@ -377,19 +377,19 @@ async fn inner_get_common_edge(
|
|||||||
modeling_response: OkModelingCmdResponse::Solid3dGetCommonEdge(common_edge),
|
modeling_response: OkModelingCmdResponse::Solid3dGetCommonEdge(common_edge),
|
||||||
} = &resp
|
} = &resp
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("mcmd::Solid3dGetCommonEdge response was not as expected: {:?}", resp),
|
format!("mcmd::Solid3dGetCommonEdge response was not as expected: {:?}", resp),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
common_edge.edge.ok_or_else(|| {
|
common_edge.edge.ok_or_else(|| {
|
||||||
KclError::Type(KclErrorDetails {
|
KclError::Type(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"No common edge was found between `{}` and `{}`",
|
"No common edge was found between `{}` and `{}`",
|
||||||
faces[0].value, faces[1].value
|
faces[0].value, faces[1].value
|
||||||
),
|
),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -175,11 +175,11 @@ async fn inner_extrude(
|
|||||||
let mut solids = Vec::new();
|
let mut solids = Vec::new();
|
||||||
|
|
||||||
if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
|
if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![args.source_range],
|
"You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
|
||||||
message: "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
|
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
}));
|
vec![args.source_range],
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
|
let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
|
||||||
@ -262,10 +262,10 @@ pub(crate) async fn do_post_extrude<'a>(
|
|||||||
// The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
|
// The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
|
||||||
// So, let's just use the first one.
|
// So, let's just use the first one.
|
||||||
let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
|
let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: "Expected a non-empty sketch".to_string(),
|
"Expected a non-empty sketch".to_owned(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
any_edge_id
|
any_edge_id
|
||||||
};
|
};
|
||||||
@ -387,13 +387,13 @@ pub(crate) async fn do_post_extrude<'a>(
|
|||||||
// Add the tags for the start or end caps.
|
// Add the tags for the start or end caps.
|
||||||
if let Some(tag_start) = named_cap_tags.start {
|
if let Some(tag_start) = named_cap_tags.start {
|
||||||
let Some(start_cap_id) = start_cap_id else {
|
let Some(start_cap_id) = start_cap_id else {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
|
"Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
|
||||||
tag_start.name, sketch.id
|
tag_start.name, sketch.id
|
||||||
),
|
),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
|
new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
|
||||||
@ -407,13 +407,13 @@ pub(crate) async fn do_post_extrude<'a>(
|
|||||||
}
|
}
|
||||||
if let Some(tag_end) = named_cap_tags.end {
|
if let Some(tag_end) = named_cap_tags.end {
|
||||||
let Some(end_cap_id) = end_cap_id else {
|
let Some(end_cap_id) = end_cap_id else {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
|
"Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
|
||||||
tag_end.name, sketch.id
|
tag_end.name, sketch.id
|
||||||
),
|
),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
|
new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
|
||||||
|
@ -49,10 +49,11 @@ pub(super) fn validate_unique<T: Eq + std::hash::Hash>(tags: &[(T, SourceRange)]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !duplicate_tags_source.is_empty() {
|
if !duplicate_tags_source.is_empty() {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails::new(
|
||||||
message: "The same edge ID is being referenced multiple times, which is not allowed. Please select a different edge".to_string(),
|
"The same edge ID is being referenced multiple times, which is not allowed. Please select a different edge"
|
||||||
source_ranges: duplicate_tags_source,
|
.to_string(),
|
||||||
}));
|
duplicate_tags_source,
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use kittycad_modeling_cmds::{self as kcmc, shared::Point3d};
|
|||||||
|
|
||||||
use super::args::TyF64;
|
use super::args::TyF64;
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::KclError,
|
errors::{KclError, KclErrorDetails},
|
||||||
execution::{
|
execution::{
|
||||||
types::{PrimitiveType, RuntimeType},
|
types::{PrimitiveType, RuntimeType},
|
||||||
ExecState, Helix as HelixValue, KclValue, Solid,
|
ExecState, Helix as HelixValue, KclValue, Solid,
|
||||||
@ -33,50 +33,50 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
|
|
||||||
// Make sure we have a radius if we don't have a cylinder.
|
// Make sure we have a radius if we don't have a cylinder.
|
||||||
if radius.is_none() && cylinder.is_none() {
|
if radius.is_none() && cylinder.is_none() {
|
||||||
return Err(KclError::Semantic(crate::errors::KclErrorDetails {
|
return Err(KclError::Semantic(crate::errors::KclErrorDetails::new(
|
||||||
message: "Radius is required when creating a helix without a cylinder.".to_string(),
|
"Radius is required when creating a helix without a cylinder.".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't have a radius if we have a cylinder.
|
// Make sure we don't have a radius if we have a cylinder.
|
||||||
if radius.is_some() && cylinder.is_some() {
|
if radius.is_some() && cylinder.is_some() {
|
||||||
return Err(KclError::Semantic(crate::errors::KclErrorDetails {
|
return Err(KclError::Semantic(crate::errors::KclErrorDetails::new(
|
||||||
message: "Radius is not allowed when creating a helix with a cylinder.".to_string(),
|
"Radius is not allowed when creating a helix with a cylinder.".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we have an axis if we don't have a cylinder.
|
// Make sure we have an axis if we don't have a cylinder.
|
||||||
if axis.is_none() && cylinder.is_none() {
|
if axis.is_none() && cylinder.is_none() {
|
||||||
return Err(KclError::Semantic(crate::errors::KclErrorDetails {
|
return Err(KclError::Semantic(crate::errors::KclErrorDetails::new(
|
||||||
message: "Axis is required when creating a helix without a cylinder.".to_string(),
|
"Axis is required when creating a helix without a cylinder.".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't have an axis if we have a cylinder.
|
// Make sure we don't have an axis if we have a cylinder.
|
||||||
if axis.is_some() && cylinder.is_some() {
|
if axis.is_some() && cylinder.is_some() {
|
||||||
return Err(KclError::Semantic(crate::errors::KclErrorDetails {
|
return Err(KclError::Semantic(crate::errors::KclErrorDetails::new(
|
||||||
message: "Axis is not allowed when creating a helix with a cylinder.".to_string(),
|
"Axis is not allowed when creating a helix with a cylinder.".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we have a radius if we have an axis.
|
// Make sure we have a radius if we have an axis.
|
||||||
if radius.is_none() && axis.is_some() {
|
if radius.is_none() && axis.is_some() {
|
||||||
return Err(KclError::Semantic(crate::errors::KclErrorDetails {
|
return Err(KclError::Semantic(crate::errors::KclErrorDetails::new(
|
||||||
message: "Radius is required when creating a helix around an axis.".to_string(),
|
"Radius is required when creating a helix around an axis.".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we have an axis if we have a radius.
|
// Make sure we have an axis if we have a radius.
|
||||||
if axis.is_none() && radius.is_some() {
|
if axis.is_none() && radius.is_some() {
|
||||||
return Err(KclError::Semantic(crate::errors::KclErrorDetails {
|
return Err(KclError::Semantic(crate::errors::KclErrorDetails::new(
|
||||||
message: "Axis is required when creating a helix around an axis.".to_string(),
|
"Axis is required when creating a helix around an axis.".to_string(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = inner_helix(
|
let value = inner_helix(
|
||||||
@ -140,10 +140,10 @@ async fn inner_helix(
|
|||||||
Axis3dOrEdgeReference::Axis { direction, origin } => {
|
Axis3dOrEdgeReference::Axis { direction, origin } => {
|
||||||
// Make sure they gave us a length.
|
// Make sure they gave us a length.
|
||||||
let Some(length) = length else {
|
let Some(length) = length else {
|
||||||
return Err(KclError::Semantic(crate::errors::KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Length is required when creating a helix around an axis.".to_string(),
|
"Length is required when creating a helix around an axis.".to_owned(),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
args.batch_modeling_cmd(
|
args.batch_modeling_cmd(
|
||||||
|
@ -148,13 +148,13 @@ async fn inner_loft(
|
|||||||
) -> Result<Box<Solid>, KclError> {
|
) -> Result<Box<Solid>, KclError> {
|
||||||
// Make sure we have at least two sketches.
|
// Make sure we have at least two sketches.
|
||||||
if sketches.len() < 2 {
|
if sketches.len() < 2 {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Loft requires at least two sketches, but only {} were provided.",
|
"Loft requires at least two sketches, but only {} were provided.",
|
||||||
sketches.len()
|
sketches.len()
|
||||||
),
|
),
|
||||||
source_ranges: vec![args.source_range],
|
vec![args.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = exec_state.next_uuid();
|
let id = exec_state.next_uuid();
|
||||||
|
@ -56,13 +56,13 @@ pub async fn sqrt(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
|||||||
let input: TyF64 = args.get_unlabeled_kw_arg_typed("input", &RuntimeType::num_any(), exec_state)?;
|
let input: TyF64 = args.get_unlabeled_kw_arg_typed("input", &RuntimeType::num_any(), exec_state)?;
|
||||||
|
|
||||||
if input.n < 0.0 {
|
if input.n < 0.0 {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![args.source_range],
|
format!(
|
||||||
message: format!(
|
|
||||||
"Attempt to take square root (`sqrt`) of a number less than zero ({})",
|
"Attempt to take square root (`sqrt`) of a number less than zero ({})",
|
||||||
input.n
|
input.n
|
||||||
),
|
),
|
||||||
}));
|
vec![args.source_range],
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = input.n.sqrt();
|
let result = input.n.sqrt();
|
||||||
|