Compare commits
17 Commits
nightly-v2
...
pierremtb/
Author | SHA1 | Date | |
---|---|---|---|
7c935741e4 | |||
87e299e0bb | |||
465e71c12f | |||
df86c93a04 | |||
824669a1c2 | |||
ba8f8a1722 | |||
f4a4e6c5be | |||
0d148e80aa | |||
3300993ac8 | |||
033eaed32e | |||
8aabac0be7 | |||
138728a95d | |||
9a92e7d642 | |||
efedc8de58 | |||
f7ee248a26 | |||
336f4f27ba | |||
e1f128d64a |
@ -29,6 +29,13 @@
|
||||
{
|
||||
"name": "isNaN",
|
||||
"message": "Use Number.isNaN() instead."
|
||||
},
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "CallExpression[callee.object.name='Array'][callee.property.name='isArray']",
|
||||
"message": "Use isArray() in lib/utils.ts instead of Array.isArray()."
|
||||
}
|
||||
],
|
||||
"semi": [
|
||||
|
@ -1,4 +1,4 @@
|
||||
name: E2E Tests
|
||||
name: E2E Flow Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
rust:
|
||||
- 'src/wasm-lib/**'
|
||||
|
||||
electron:
|
||||
flow-tests:
|
||||
timeout-minutes: 60
|
||||
name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }}
|
||||
strategy:
|
||||
@ -46,32 +46,30 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.GH_ORG_APP_ID }}
|
||||
private-key: ${{ secrets.GH_ORG_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- uses: KittyCAD/action-install-cli@main
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: yarn
|
||||
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright/
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
shell: bash
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
- name: Download Wasm Cache
|
||||
id: download-wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
@ -83,29 +81,35 @@ jobs:
|
||||
workflow: build-and-store-wasm.yml
|
||||
branch: main
|
||||
path: src/wasm-lib/pkg
|
||||
|
||||
- name: copy wasm blob
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
shell: bash
|
||||
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
continue-on-error: true
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache Wasm (because rust diff)
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: OR Cache Wasm (because wasm cache failed)
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: install good sed
|
||||
if: ${{ startsWith(matrix.os, 'macos') }}
|
||||
shell: bash
|
||||
run: |
|
||||
brew install gnu-sed
|
||||
echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install vector
|
||||
shell: bash
|
||||
# TODO: figure out what to do with this, it's failing
|
||||
@ -123,81 +127,33 @@ jobs:
|
||||
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
|
||||
cat /tmp/vector.toml
|
||||
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
|
||||
|
||||
- name: Build Wasm (because rust diff)
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
shell: bash
|
||||
run: yarn build:wasm
|
||||
|
||||
- name: OR Build Wasm (because wasm cache failed)
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
shell: bash
|
||||
run: yarn build:wasm
|
||||
|
||||
- name: build web
|
||||
shell: bash
|
||||
run: yarn tronb:vite:dev
|
||||
- name: Run ubuntu/chrome snapshots
|
||||
if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
|
||||
shell: bash
|
||||
# TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest,
|
||||
# but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
|
||||
run: |
|
||||
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: development
|
||||
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
VITE_KC_SKIP_AUTH: true
|
||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
- name: Clean up test-results
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
continue-on-error: true
|
||||
run: rm -r test-results
|
||||
- name: check for changes
|
||||
if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
|
||||
shell: bash
|
||||
id: git-check
|
||||
run: |
|
||||
git add .
|
||||
if git status | grep -q "Changes to be committed"
|
||||
then echo "modified=true" >> $GITHUB_OUTPUT
|
||||
else echo "modified=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Commit changes, if any
|
||||
if: steps.git-check.outputs.modified == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
git add .
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
git fetch origin
|
||||
echo ${{ github.head_ref }}
|
||||
git checkout ${{ github.head_ref }}
|
||||
git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
|
||||
git push
|
||||
git push origin ${{ github.head_ref }}
|
||||
# only upload artifacts if there's actually changes
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: steps.git-check.outputs.modified == 'true'
|
||||
with:
|
||||
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
|
||||
- name: Run playwright/electron flow (with retries)
|
||||
id: retry
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
@ -211,6 +167,7 @@ jobs:
|
||||
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
VITE_KC_SKIP_AUTH: true
|
||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
@ -219,6 +176,7 @@ jobs:
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
@ -227,4 +185,3 @@ jobs:
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
145
.github/workflows/e2e-snapshot-tests.yml
vendored
Normal file
@ -0,0 +1,145 @@
|
||||
name: E2E Snapshot Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
check-rust-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
rust-changed: ${{ steps.filter.outputs.rust }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- id: filter
|
||||
name: Check for Rust changes
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
rust:
|
||||
- 'src/wasm-lib/**'
|
||||
|
||||
snapshot-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright/
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
- name: Download Wasm Cache
|
||||
id: download-wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
uses: dawidd6/action-download-artifact@v7
|
||||
continue-on-error: true
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
name: wasm-bundle
|
||||
workflow: build-and-store-wasm.yml
|
||||
branch: main
|
||||
path: src/wasm-lib/pkg
|
||||
|
||||
- name: copy wasm blob
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
continue-on-error: true
|
||||
|
||||
- name: Setup Rust
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache Wasm (because rust diff)
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: OR Cache Wasm (because wasm cache failed)
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: Build Wasm (because rust diff)
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
run: yarn build:wasm
|
||||
|
||||
- name: OR Build Wasm (because wasm cache failed)
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
run: yarn build:wasm
|
||||
|
||||
- name: build web
|
||||
run: yarn tronb:vite:dev
|
||||
|
||||
- name: Run chrome snapshots
|
||||
run: |
|
||||
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: development
|
||||
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
VITE_KC_SKIP_AUTH: true
|
||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||
|
||||
- name: check for changes
|
||||
id: git-check
|
||||
run: |
|
||||
{
|
||||
echo 'changes<<EOF'
|
||||
git diff --name-only e2e/playwright/snapshot-tests.spec.ts-snapshots
|
||||
echo EOF
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# only upload artifacts if there's actually changes
|
||||
- name: Upload changes, if any
|
||||
if: steps.git-check.outputs.changes != ''
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-snapshots-${{ runner.os }}-${{ github.sha }}
|
||||
path: ${{ steps.git-check.outputs.changes }}
|
||||
|
||||
- name: Upload report, if any
|
||||
uses: actions/upload-artifact@v4
|
||||
if: steps.git-check.outputs.changes != ''
|
||||
with:
|
||||
name: playwright-report-${{ runner.os }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail the run if we have snapshot updates
|
||||
if: steps.git-check.outputs.changes != ''
|
||||
run: exit 1
|
||||
|
||||
# TODO: check if we could comment on the PR as well
|
@ -47,21 +47,6 @@ myObj = { a = 0, b = "thing" }
|
||||
We support two different ways of getting properties from objects, you can call
|
||||
`myObj.a` or `myObj["a"]` both work.
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
We also have support for defining your own functions. Functions can take in any
|
||||
type of argument. Below is an example of the syntax:
|
||||
|
||||
```
|
||||
fn myFn(x) {
|
||||
return x
|
||||
}
|
||||
```
|
||||
|
||||
As you can see above `myFn` just returns whatever it is given.
|
||||
|
||||
|
||||
## Binary expressions
|
||||
|
||||
You can also do math! Let's show an example below:
|
||||
@ -76,6 +61,120 @@ You can nest expressions in parenthesis as well:
|
||||
myMathExpression = 3 + (1 * 2 / (3 - 7))
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
We also have support for defining your own functions. Functions can take in any
|
||||
type of argument. Below is an example of the syntax:
|
||||
|
||||
```
|
||||
fn myFn(x) {
|
||||
return x
|
||||
}
|
||||
```
|
||||
|
||||
As you can see above `myFn` just returns whatever it is given.
|
||||
|
||||
KCL's early drafts used positional arguments, but we now use keyword arguments. If you declare a
|
||||
function like this:
|
||||
|
||||
```
|
||||
fn add(left, right) {
|
||||
return left + right
|
||||
}
|
||||
```
|
||||
|
||||
You can call it like this:
|
||||
|
||||
```
|
||||
total = add(left = 1, right = 2)
|
||||
```
|
||||
|
||||
Functions can also declare one *unlabeled* arg. If you do want to declare an unlabeled arg, it must
|
||||
be the first arg declared.
|
||||
|
||||
```
|
||||
// The @ indicates an argument can be used without a label.
|
||||
// Note that only the first argument can use @.
|
||||
fn increment(@x) {
|
||||
return x + 1
|
||||
}
|
||||
|
||||
fn add(@x, delta) {
|
||||
return x + delta
|
||||
}
|
||||
|
||||
two = increment(1)
|
||||
three = add(1, delta = 2)
|
||||
```
|
||||
|
||||
## Pipelines
|
||||
|
||||
It can be hard to read repeated function calls, because of all the nested brackets.
|
||||
|
||||
```
|
||||
i = 1
|
||||
x = h(g(f(i)))
|
||||
```
|
||||
|
||||
You can make this easier to read by breaking it into many declarations, but that is a bit annoying.
|
||||
|
||||
```
|
||||
i = 1
|
||||
x0 = f(i)
|
||||
x1 = g(x0)
|
||||
x = h(x1)
|
||||
```
|
||||
|
||||
Instead, you can use the pipeline operator (`|>`) to simplify this.
|
||||
|
||||
Basically, `x |> f(%)` is a shorthand for `f(x)`. The left-hand side of the `|>` gets put into
|
||||
the `%` in the right-hand side.
|
||||
|
||||
So, this means `x |> f(%) |> g(%)` is shorthand for `g(f(x))`. The code example above, with its
|
||||
somewhat-clunky `x0` and `x1` constants could be rewritten as
|
||||
|
||||
```
|
||||
i = 1
|
||||
x = i
|
||||
|> f(%)
|
||||
|> g(%)
|
||||
|> h(%)
|
||||
```
|
||||
|
||||
This helps keep your code neat and avoid unnecessary declarations.
|
||||
|
||||
## Pipelines and keyword arguments
|
||||
|
||||
Say you have a long pipeline of sketch functions, like this:
|
||||
|
||||
```
|
||||
startSketch()
|
||||
|> line(%, end = [3, 4])
|
||||
|> line(%, end = [10, 10])
|
||||
|> line(%, end = [-13, -14])
|
||||
|> close(%)
|
||||
```
|
||||
|
||||
In this example, each function call outputs a sketch, and it gets put into the next function call via
|
||||
the `%`, into the first (unlabeled) argument.
|
||||
|
||||
If a function call uses an unlabeled first parameter, it will default to `%` if it's not given. This
|
||||
means that `|> line(%, end = [3, 4])` and `|> line(end = [3, 4])` are equivalent! So the above
|
||||
could be rewritten as
|
||||
|
||||
```
|
||||
startSketch()
|
||||
|> line(end = [3, 4])
|
||||
|> line(end = [10, 10])
|
||||
|> line(end = [-13, -14])
|
||||
|> close()
|
||||
```
|
||||
|
||||
Note that we are still in the process of migrating KCL's standard library to use keyword arguments. So some
|
||||
functions are still unfortunately using positional arguments. We're moving them over, so keep checking back.
|
||||
Some functions like `angledLine`, `startProfileAt` etc are still using the old positional argument syntax.
|
||||
Check the docs page for each function and look at its examples to see.
|
||||
|
||||
## Tags
|
||||
|
||||
Tags are used to give a name (tag) to a specific path.
|
||||
@ -88,17 +187,17 @@ way:
|
||||
```
|
||||
startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
196.99
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
|> angledLine({angle = 0, length = 191.26}, %, $rectangleSegmentA001)
|
||||
|> angledLine({
|
||||
angle = segAng(rectangleSegmentA001) - 90,
|
||||
length = 196.99,
|
||||
}, %, $rectangleSegmentB001)
|
||||
|> angledLine({
|
||||
angle = segAng(rectangleSegmentA001),
|
||||
length = -segLen(rectangleSegmentA001),
|
||||
}, %, $rectangleSegmentC001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
```
|
||||
|
||||
### Tag Identifier
|
||||
@ -121,17 +220,17 @@ However if the code was written like this:
|
||||
fn rect(origin) {
|
||||
return startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
196.99
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
|> angledLine({angle = 0, length = 191.26}, %, $rectangleSegmentA001)
|
||||
|> angledLine({
|
||||
angle = segAng(rectangleSegmentA001) - 90,
|
||||
length = 196.99
|
||||
}, %, $rectangleSegmentB001)
|
||||
|> angledLine({
|
||||
angle = segAng(rectangleSegmentA001),
|
||||
length = -segLen(rectangleSegmentA001)
|
||||
}, %, $rectangleSegmentC001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
}
|
||||
|
||||
rect([0, 0])
|
||||
@ -149,17 +248,17 @@ For example the following code works.
|
||||
fn rect(origin) {
|
||||
return startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
196.99
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
|> angledLine({angle = 0, length = 191.26}, %, $rectangleSegmentA001)
|
||||
|> angledLine({
|
||||
angle = segAng(rectangleSegmentA001) - 90,
|
||||
length = 196.99
|
||||
}, %, $rectangleSegmentB001)
|
||||
|> angledLine({
|
||||
angle = segAng(rectangleSegmentA001),
|
||||
length = -segLen(rectangleSegmentA001)
|
||||
}, %, $rectangleSegmentC001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
}
|
||||
|
||||
rect([0, 0])
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 49 KiB |
@ -116,10 +116,10 @@
|
||||
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\" --quiet",
|
||||
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot' --quiet",
|
||||
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot' --quiet",
|
||||
"test:playwright:electron:local": "yarn tronb:package:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
||||
"test:playwright:electron:windows:local": "yarn tronb:package:dev && set NODE_ENV='development' && playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
||||
"test:playwright:electron:macos:local": "yarn tronb:package:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
||||
"test:playwright:electron:ubuntu:local": "yarn tronb:package:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
|
||||
"test:playwright:electron:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
||||
"test:playwright:electron:windows:local": "yarn tronb:vite:dev && set NODE_ENV='development' && playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
||||
"test:playwright:electron:macos:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
||||
"test:playwright:electron:ubuntu:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
|
||||
"test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000",
|
||||
"test:unit:kcl-samples:local": "yarn simpleserver:bg && yarn test:unit:kcl-samples; kill-port 3000"
|
||||
},
|
||||
|
7
packages/codemirror-lsp-client/src/lib/utils.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* A safer type guard for arrays since the built-in Array.isArray() asserts `any[]`.
|
||||
*/
|
||||
export function isArray(val: any): val is unknown[] {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return Array.isArray(val)
|
||||
}
|
@ -2,6 +2,7 @@ import { Text } from '@codemirror/state'
|
||||
import { Marked } from '@ts-stack/markdown'
|
||||
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { isArray } from '../lib/utils'
|
||||
|
||||
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
|
||||
export function deferExecution<T>(func: (args: T) => any, wait: number) {
|
||||
@ -45,7 +46,7 @@ export function offsetToPos(doc: Text, offset: number) {
|
||||
export function formatMarkdownContents(
|
||||
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
||||
): string {
|
||||
if (Array.isArray(contents)) {
|
||||
if (isArray(contents)) {
|
||||
return contents.map((c) => formatMarkdownContents(c) + '\n\n').join('')
|
||||
} else if (typeof contents === 'string') {
|
||||
return Marked.parse(contents)
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
export function Toolbar({
|
||||
className = '',
|
||||
@ -121,7 +122,7 @@ export function Toolbar({
|
||||
return toolbarConfig[currentMode].items.map((maybeIconConfig) => {
|
||||
if (maybeIconConfig === 'break') {
|
||||
return 'break'
|
||||
} else if (Array.isArray(maybeIconConfig)) {
|
||||
} else if (isArray(maybeIconConfig)) {
|
||||
return maybeIconConfig.map(resolveItemConfig)
|
||||
} else {
|
||||
return resolveItemConfig(maybeIconConfig)
|
||||
@ -180,7 +181,7 @@ export function Toolbar({
|
||||
className="h-5 w-[1px] block bg-chalkboard-30 dark:bg-chalkboard-80"
|
||||
/>
|
||||
)
|
||||
} else if (Array.isArray(maybeIconConfig)) {
|
||||
} else if (isArray(maybeIconConfig)) {
|
||||
// A button with a dropdown
|
||||
return (
|
||||
<ActionButtonDropdown
|
||||
|
@ -7,6 +7,7 @@ import { trap } from 'lib/trap'
|
||||
import { codeToIdSelections } from 'lib/selections'
|
||||
import { codeRefFromRange } from 'lang/std/artifactGraph'
|
||||
import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
export function AstExplorer() {
|
||||
const { context } = useModelingContext()
|
||||
@ -166,12 +167,12 @@ function DisplayObj({
|
||||
{Object.entries(obj).map(([key, value]) => {
|
||||
if (filterKeys.includes(key)) {
|
||||
return null
|
||||
} else if (Array.isArray(value)) {
|
||||
} else if (isArray(value)) {
|
||||
return (
|
||||
<li key={key}>
|
||||
{`${key}: [`}
|
||||
<DisplayBody
|
||||
body={value}
|
||||
body={value as any}
|
||||
filterKeys={filterKeys}
|
||||
node={node}
|
||||
/>
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
} from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { oneDark } from '@codemirror/theme-one-dark'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
//reference: https://github.com/sachinraja/rodemirror/blob/main/src/use-first-render.ts
|
||||
const useFirstRender = () => {
|
||||
@ -86,6 +87,18 @@ const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
|
||||
return <div ref={editor}></div>
|
||||
})
|
||||
|
||||
/**
|
||||
* The extensions type is quite weird. We need a special helper to preserve the
|
||||
* readonly array type.
|
||||
*
|
||||
* @see https://github.com/microsoft/TypeScript/issues/17002
|
||||
*/
|
||||
function isExtensionArray(
|
||||
extensions: Extension
|
||||
): extensions is readonly Extension[] {
|
||||
return isArray(extensions)
|
||||
}
|
||||
|
||||
export function useCodeMirror(props: UseCodeMirror) {
|
||||
const {
|
||||
onCreateEditor,
|
||||
@ -103,7 +116,7 @@ export function useCodeMirror(props: UseCodeMirror) {
|
||||
const isFirstRender = useFirstRender()
|
||||
|
||||
const targetExtensions = useMemo(() => {
|
||||
let exts = Array.isArray(extensions) ? extensions : []
|
||||
let exts = isExtensionArray(extensions) ? extensions : []
|
||||
if (theme === 'dark') {
|
||||
exts = [...exts, oneDark]
|
||||
} else if (theme === 'light') {
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
import { Range, Extension, Text } from '@codemirror/state'
|
||||
import { NodeProp, Tree } from '@lezer/common'
|
||||
import { language, syntaxTree } from '@codemirror/language'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
interface PickerState {
|
||||
from: number
|
||||
@ -79,7 +80,7 @@ function discoverColorsInKCL(
|
||||
)
|
||||
|
||||
if (maybeWidgetOptions) {
|
||||
if (Array.isArray(maybeWidgetOptions)) {
|
||||
if (isArray(maybeWidgetOptions)) {
|
||||
console.error('Unexpected nested overlays')
|
||||
ret.push(...maybeWidgetOptions)
|
||||
} else {
|
||||
@ -150,7 +151,7 @@ function colorPickersDecorations(
|
||||
return
|
||||
}
|
||||
|
||||
if (!Array.isArray(maybeWidgetOptions)) {
|
||||
if (!isArray(maybeWidgetOptions)) {
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new ColorPickerWidget(maybeWidgetOptions),
|
||||
|
@ -36,6 +36,7 @@ import {
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import { KclCommandValue } from 'lib/commandTypes'
|
||||
import { isArray } from 'lib/utils'
|
||||
import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { findKwArg } from 'lang/util'
|
||||
@ -866,10 +867,7 @@ export async function deleteEdgeTreatment(
|
||||
if (!inPipe) {
|
||||
const varDecPathStep = varDec.shallowPath[1]
|
||||
|
||||
if (
|
||||
!Array.isArray(varDecPathStep) ||
|
||||
typeof varDecPathStep[0] !== 'number'
|
||||
) {
|
||||
if (!isArray(varDecPathStep) || typeof varDecPathStep[0] !== 'number') {
|
||||
return new Error(
|
||||
'Invalid shallowPath structure: expected a number at shallowPath[1][0]'
|
||||
)
|
||||
@ -935,7 +933,7 @@ export async function deleteEdgeTreatment(
|
||||
if (err(pipeExpressionNode)) return pipeExpressionNode
|
||||
|
||||
// Ensure that the PipeExpression.body is an array
|
||||
if (!Array.isArray(pipeExpressionNode.node.body)) {
|
||||
if (!isArray(pipeExpressionNode.node.body)) {
|
||||
return new Error('PipeExpression body is not an array')
|
||||
}
|
||||
|
||||
@ -945,10 +943,7 @@ export async function deleteEdgeTreatment(
|
||||
// Remove VariableDeclarator if PipeExpression.body is empty
|
||||
if (pipeExpressionNode.node.body.length === 0) {
|
||||
const varDecPathStep = varDec.shallowPath[1]
|
||||
if (
|
||||
!Array.isArray(varDecPathStep) ||
|
||||
typeof varDecPathStep[0] !== 'number'
|
||||
) {
|
||||
if (!isArray(varDecPathStep) || typeof varDecPathStep[0] !== 'number') {
|
||||
return new Error(
|
||||
'Invalid shallowPath structure: expected a number at shallowPath[1][0]'
|
||||
)
|
||||
|
@ -27,7 +27,7 @@ import {
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
|
||||
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
|
||||
import { getAngle } from '../lib/utils'
|
||||
import { getAngle, isArray } from '../lib/utils'
|
||||
import { ARG_TAG, getArgForEnd, getFirstArg } from './std/sketch'
|
||||
import {
|
||||
getConstraintLevelFromSourceRange,
|
||||
@ -112,7 +112,7 @@ export function getNodeFromPath<T>(
|
||||
}
|
||||
if (
|
||||
typeof stopAt !== 'undefined' &&
|
||||
(Array.isArray(stopAt)
|
||||
(isArray(stopAt)
|
||||
? stopAt.includes(currentNode.type)
|
||||
: currentNode.type === stopAt)
|
||||
) {
|
||||
@ -167,6 +167,7 @@ export function getNodeFromPathCurry(
|
||||
type KCLNode = Node<
|
||||
| Expr
|
||||
| ExpressionStatement
|
||||
| ImportStatement
|
||||
| VariableDeclaration
|
||||
| VariableDeclarator
|
||||
| ReturnStatement
|
||||
@ -263,10 +264,14 @@ export function traverse(
|
||||
// hmm this smell
|
||||
_traverse(_node.object, [...pathToNode, ['object', 'MemberExpression']])
|
||||
_traverse(_node.property, [...pathToNode, ['property', 'MemberExpression']])
|
||||
} else if ('body' in _node && Array.isArray(_node.body)) {
|
||||
_node.body.forEach((expression, index) =>
|
||||
} else if (_node.type === 'ImportStatement') {
|
||||
// Do nothing.
|
||||
} else if ('body' in _node && isArray(_node.body)) {
|
||||
// TODO: Program should have a type field, but it currently doesn't.
|
||||
const program = node as Node<Program>
|
||||
program.body.forEach((expression, index) => {
|
||||
_traverse(expression, [...pathToNode, ['body', ''], [index, 'index']])
|
||||
)
|
||||
})
|
||||
}
|
||||
option?.leave?.(_node)
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ import {
|
||||
mutateObjExpProp,
|
||||
findUniqueName,
|
||||
} from 'lang/modifyAst'
|
||||
import { roundOff, getLength, getAngle } from 'lib/utils'
|
||||
import { roundOff, getLength, getAngle, isArray } from 'lib/utils'
|
||||
import { err } from 'lib/trap'
|
||||
import { perpendicularDistance } from 'sketch-helpers'
|
||||
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
|
||||
@ -96,7 +96,7 @@ export function createFirstArg(
|
||||
sketchFn: ToolTip,
|
||||
val: Expr | [Expr, Expr] | [Expr, Expr, Expr]
|
||||
): Expr | Error {
|
||||
if (Array.isArray(val)) {
|
||||
if (isArray(val)) {
|
||||
if (
|
||||
[
|
||||
'angledLine',
|
||||
|
@ -57,7 +57,7 @@ import {
|
||||
getSketchSegmentFromPathToNode,
|
||||
getSketchSegmentFromSourceRange,
|
||||
} from './sketchConstraints'
|
||||
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
|
||||
import { getAngle, roundOff, normaliseAngle, isArray } from '../../lib/utils'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { findKwArg, findKwArgAny } from 'lang/util'
|
||||
|
||||
@ -122,7 +122,7 @@ function createCallWrapper(
|
||||
tag?: Expr,
|
||||
valueUsedInTransform?: number
|
||||
): CreatedSketchExprResult {
|
||||
if (Array.isArray(val)) {
|
||||
if (isArray(val)) {
|
||||
if (tooltip === 'line') {
|
||||
const labeledArgs = [createLabeledArg('end', createArrayExpression(val))]
|
||||
if (tag) {
|
||||
@ -1330,12 +1330,12 @@ export function getRemoveConstraintsTransform(
|
||||
|
||||
// check if the function has no constraints
|
||||
const isTwoValFree =
|
||||
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
if (isTwoValFree) {
|
||||
return false
|
||||
}
|
||||
const isOneValFree =
|
||||
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
!isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
if (isOneValFree) {
|
||||
return transformInfo
|
||||
}
|
||||
@ -1649,7 +1649,7 @@ export function getConstraintType(
|
||||
// and for one val sketch functions that the arg is NOT locked down
|
||||
// these conditions should have been checked previously.
|
||||
// completely locked down or not locked down at all does not depend on the fnName so we can check that first
|
||||
const isArr = Array.isArray(val)
|
||||
const isArr = isArray(val)
|
||||
if (!isArr) {
|
||||
if (fnName === 'xLine') return 'yRelative'
|
||||
if (fnName === 'yLine') return 'xRelative'
|
||||
@ -2113,9 +2113,9 @@ export function getConstraintLevelFromSourceRange(
|
||||
|
||||
// check if the function has no constraints
|
||||
const isTwoValFree =
|
||||
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
const isOneValFree =
|
||||
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
!isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
|
||||
if (isTwoValFree) return { level: 'free', range: range }
|
||||
if (isOneValFree) return { level: 'partial', range: range }
|
||||
@ -2128,7 +2128,7 @@ export function isLiteralArrayOrStatic(
|
||||
): boolean {
|
||||
if (!val) return false
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
if (isArray(val)) {
|
||||
const a = val[0]
|
||||
const b = val[1]
|
||||
return isLiteralArrayOrStatic(a) && isLiteralArrayOrStatic(b)
|
||||
@ -2142,7 +2142,7 @@ export function isLiteralArrayOrStatic(
|
||||
export function isNotLiteralArrayOrStatic(
|
||||
val: Expr | [Expr, Expr] | [Expr, Expr, Expr]
|
||||
): boolean {
|
||||
if (Array.isArray(val)) {
|
||||
if (isArray(val)) {
|
||||
const a = val[0]
|
||||
const b = val[1]
|
||||
return isNotLiteralArrayOrStatic(a) && isNotLiteralArrayOrStatic(b)
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
NumericSuffix,
|
||||
} from './wasm'
|
||||
import { filterArtifacts } from 'lang/std/artifactGraph'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
import { isArray, isOverlap } from 'lib/utils'
|
||||
|
||||
export function updatePathToNodeFromMap(
|
||||
oldPath: PathToNode,
|
||||
@ -40,8 +40,8 @@ export function isCursorInSketchCommandRange(
|
||||
predicate: (artifact) => {
|
||||
return selectionRanges.graphSelections.some(
|
||||
(selection) =>
|
||||
Array.isArray(selection?.codeRef?.range) &&
|
||||
Array.isArray(artifact?.codeRef?.range) &&
|
||||
isArray(selection?.codeRef?.range) &&
|
||||
isArray(artifact?.codeRef?.range) &&
|
||||
isOverlap(selection?.codeRef?.range, artifact.codeRef.range)
|
||||
)
|
||||
},
|
||||
|
@ -16,7 +16,7 @@ import { isDesktop } from 'lib/isDesktop'
|
||||
import { useRef } from 'react'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { isArray, toSync } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
|
||||
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
|
||||
@ -240,7 +240,7 @@ export function createSettings() {
|
||||
if (
|
||||
inputRef.current &&
|
||||
inputRefVal &&
|
||||
!Array.isArray(inputRefVal)
|
||||
!isArray(inputRefVal)
|
||||
) {
|
||||
updateValue(inputRefVal)
|
||||
} else {
|
||||
|
@ -208,15 +208,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
description: 'Create a hole in a 3D solid.',
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
id: 'helix',
|
||||
onClick: () => console.error('Helix not yet implemented'),
|
||||
icon: 'helix',
|
||||
status: 'kcl-only',
|
||||
title: 'Helix',
|
||||
description: 'Create a helix or spiral in 3D about an axis.',
|
||||
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/helix' }],
|
||||
},
|
||||
'break',
|
||||
[
|
||||
{
|
||||
@ -265,6 +256,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
],
|
||||
},
|
||||
],
|
||||
'break',
|
||||
[
|
||||
{
|
||||
id: 'plane-offset',
|
||||
@ -296,6 +288,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
links: [],
|
||||
},
|
||||
],
|
||||
{
|
||||
id: 'helix',
|
||||
onClick: () => console.error('Helix not yet implemented'),
|
||||
icon: 'helix',
|
||||
status: 'kcl-only',
|
||||
title: 'Helix',
|
||||
description: 'Create a helix or spiral in 3D about an axis.',
|
||||
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/helix' }],
|
||||
},
|
||||
'break',
|
||||
[
|
||||
{
|
||||
|
@ -11,6 +11,7 @@ export const uuidv4 = v4
|
||||
* A safer type guard for arrays since the built-in Array.isArray() asserts `any[]`.
|
||||
*/
|
||||
export function isArray(val: any): val is unknown[] {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return Array.isArray(val)
|
||||
}
|
||||
|
||||
|
752
src/wasm-lib/Cargo.lock
generated
@ -3,9 +3,8 @@ name = "wasm-lib"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
rust-version = "1.73"
|
||||
rust-version = "1.83"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "derive-docs"
|
||||
description = "A tool for generating documentation from Rust derive macros"
|
||||
version = "0.1.34"
|
||||
version = "0.1.35"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -831,7 +831,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
|
||||
if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new(&ctx.settings)).await {
|
||||
if let Err(e) = ctx.run(&program, &mut crate::ExecState::new(&ctx.settings)).await {
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
error: e,
|
||||
filename: format!("{}{}", #fn_name, #index),
|
||||
|
@ -15,7 +15,7 @@ mod test_examples_someFn {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -15,7 +15,7 @@ mod test_examples_someFn {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -16,7 +16,7 @@ mod test_examples_show {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
@ -73,7 +73,7 @@ mod test_examples_show {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -16,7 +16,7 @@ mod test_examples_show {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_my_func {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
@ -74,7 +74,7 @@ mod test_examples_my_func {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_line_to {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
@ -74,7 +74,7 @@ mod test_examples_line_to {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -16,7 +16,7 @@ mod test_examples_min {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
@ -73,7 +73,7 @@ mod test_examples_min {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -16,7 +16,7 @@ mod test_examples_show {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -16,7 +16,7 @@ mod test_examples_import {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -16,7 +16,7 @@ mod test_examples_import {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -16,7 +16,7 @@ mod test_examples_import {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -16,7 +16,7 @@ mod test_examples_show {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -15,7 +15,7 @@ mod test_examples_some_function {
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
if let Err(e) = ctx
|
||||
.run(program.into(), &mut crate::ExecState::new(&ctx.settings))
|
||||
.run(&program, &mut crate::ExecState::new(&ctx.settings))
|
||||
.await
|
||||
{
|
||||
return Err(miette::Report::new(crate::errors::Report {
|
||||
|
@ -151,7 +151,7 @@ async fn handle_request(req: hyper::Request<Body>, state3: Arc<ServerState>) ->
|
||||
/// KCL errors (from engine or the executor) respond with HTTP Bad Gateway.
|
||||
/// Malformed requests are HTTP Bad Request.
|
||||
/// Successful requests contain a PNG as the body.
|
||||
async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body> {
|
||||
async fn snapshot_endpoint(body: Bytes, ctxt: ExecutorContext) -> Response<Body> {
|
||||
let body = match serde_json::from_slice::<RequestBody>(body.as_ref()) {
|
||||
Ok(bd) => bd,
|
||||
Err(e) => return bad_request(format!("Invalid request JSON: {e}")),
|
||||
@ -164,11 +164,11 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
|
||||
};
|
||||
|
||||
eprintln!("Executing {test_name}");
|
||||
let mut exec_state = ExecState::new(&state.settings);
|
||||
let mut exec_state = ExecState::new(&ctxt.settings);
|
||||
// This is a shitty source range, I don't know what else to use for it though.
|
||||
// There's no actual KCL associated with this reset_scene call.
|
||||
if let Err(e) = state
|
||||
.reset_scene(&mut exec_state, kcl_lib::SourceRange::default())
|
||||
if let Err(e) = ctxt
|
||||
.send_clear_scene(&mut exec_state, kcl_lib::SourceRange::default())
|
||||
.await
|
||||
{
|
||||
return kcl_err(e);
|
||||
@ -176,8 +176,11 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
|
||||
// Let users know if the test is taking a long time.
|
||||
let (done_tx, done_rx) = oneshot::channel::<()>();
|
||||
let timer = time_until(done_rx);
|
||||
let snapshot = match state.execute_and_prepare_snapshot(&program, &mut exec_state).await {
|
||||
Ok(sn) => sn,
|
||||
if let Err(e) = ctxt.run(&program, &mut exec_state).await {
|
||||
return kcl_err(e);
|
||||
}
|
||||
let snapshot = match ctxt.prepare_snapshot().await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return kcl_err(e),
|
||||
};
|
||||
let _ = done_tx.send(());
|
||||
|
@ -16,7 +16,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
|
||||
let ctx = ExecutorContext::new_forwarded_mock(Arc::new(Box::new(
|
||||
crate::conn_mock_core::EngineConnection::new(ref_result).await?,
|
||||
)));
|
||||
ctx.run(program.into(), &mut ExecState::new(&ctx.settings)).await?;
|
||||
ctx.run(&program, &mut ExecState::new(&ctx.settings)).await?;
|
||||
|
||||
let result = result.lock().expect("mutex lock").clone();
|
||||
Ok(result)
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.2.33"
|
||||
version = "0.2.34"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -63,6 +63,7 @@ unsafe impl Sync for EngineConnection {}
|
||||
|
||||
impl EngineConnection {
|
||||
pub async fn new(manager: EngineCommandManager) -> Result<EngineConnection, JsValue> {
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
Ok(EngineConnection {
|
||||
manager: Arc::new(manager),
|
||||
batch: Arc::new(Mutex::new(Vec::new())),
|
||||
|
@ -621,3 +621,68 @@ pub enum PlaneName {
|
||||
/// The opposite side of the YZ plane.
|
||||
NegYz,
|
||||
}
|
||||
|
||||
/// Create a new zoo api client.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> anyhow::Result<kittycad::Client> {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60));
|
||||
let ws_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60))
|
||||
.connection_verbose(true)
|
||||
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||
.http1_only();
|
||||
|
||||
let zoo_token_env = std::env::var("ZOO_API_TOKEN");
|
||||
|
||||
let token = if let Some(token) = token {
|
||||
token
|
||||
} else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
|
||||
if let Ok(zoo_token) = zoo_token_env {
|
||||
if zoo_token != token {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
|
||||
token,
|
||||
zoo_token
|
||||
));
|
||||
}
|
||||
}
|
||||
token
|
||||
} else if let Ok(token) = zoo_token_env {
|
||||
token
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"No API token found in environment variables. Use KITTYCAD_API_TOKEN or ZOO_API_TOKEN"
|
||||
));
|
||||
};
|
||||
|
||||
// Create the client.
|
||||
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
// Set an engine address if it's set.
|
||||
let kittycad_host_env = std::env::var("KITTYCAD_HOST");
|
||||
if let Some(addr) = engine_addr {
|
||||
client.set_base_url(addr);
|
||||
} else if let Ok(addr) = std::env::var("ZOO_HOST") {
|
||||
if let Ok(kittycad_host) = kittycad_host_env {
|
||||
if kittycad_host != addr {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
|
||||
kittycad_host,
|
||||
addr
|
||||
));
|
||||
}
|
||||
}
|
||||
client.set_base_url(addr);
|
||||
} else if let Ok(addr) = kittycad_host_env {
|
||||
client.set_base_url(addr);
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
@ -445,6 +445,12 @@ pub struct ArtifactGraph {
|
||||
map: IndexMap<ArtifactId, Artifact>,
|
||||
}
|
||||
|
||||
impl ArtifactGraph {
|
||||
pub fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn build_artifact_graph(
|
||||
artifact_commands: &[ArtifactCommand],
|
||||
responses: &IndexMap<Uuid, WebSocketResponse>,
|
||||
|
@ -1,23 +1,46 @@
|
||||
//! Functions for helping with caching an ast and finding the parts the changed.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
execution::ExecState,
|
||||
execution::{ExecState, ExecutorSettings},
|
||||
parsing::ast::types::{Node, Program},
|
||||
walk::Node as WalkNode,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// A static mutable lock for updating the last successful execution state for the cache.
|
||||
static ref OLD_AST_MEMORY: Arc<RwLock<Option<OldAstState>>> = Default::default();
|
||||
}
|
||||
|
||||
/// Read the old ast memory from the lock.
|
||||
pub(super) async fn read_old_ast_memory() -> Option<OldAstState> {
|
||||
let old_ast = OLD_AST_MEMORY.read().await;
|
||||
old_ast.clone()
|
||||
}
|
||||
|
||||
pub(super) async fn write_old_ast_memory(old_state: OldAstState) {
|
||||
let mut old_ast = OLD_AST_MEMORY.write().await;
|
||||
*old_ast = Some(old_state);
|
||||
}
|
||||
|
||||
pub async fn bust_cache() {
|
||||
let mut old_ast = OLD_AST_MEMORY.write().await;
|
||||
// Set the cache to None.
|
||||
*old_ast = None;
|
||||
}
|
||||
|
||||
/// Information for the caching an AST and smartly re-executing it if we can.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct CacheInformation {
|
||||
/// The old information.
|
||||
pub old: Option<OldAstState>,
|
||||
/// The new ast to executed.
|
||||
pub new_ast: Node<Program>,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CacheInformation<'a> {
|
||||
pub ast: &'a Node<Program>,
|
||||
pub settings: &'a ExecutorSettings,
|
||||
}
|
||||
|
||||
/// The old ast and program memory.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OldAstState {
|
||||
/// The ast.
|
||||
pub ast: Node<Program>,
|
||||
@ -27,20 +50,420 @@ pub struct OldAstState {
|
||||
pub settings: crate::execution::ExecutorSettings,
|
||||
}
|
||||
|
||||
impl From<crate::Program> for CacheInformation {
|
||||
fn from(program: crate::Program) -> Self {
|
||||
CacheInformation {
|
||||
old: None,
|
||||
new_ast: program.ast,
|
||||
/// The result of a cache check.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(super) enum CacheResult {
|
||||
ReExecute {
|
||||
/// Should we clear the scene and start over?
|
||||
clear_scene: bool,
|
||||
/// Do we need to reapply settings?
|
||||
reapply_settings: bool,
|
||||
/// The program that needs to be executed.
|
||||
program: Node<Program>,
|
||||
},
|
||||
/// Argument is whether we need to reapply settings.
|
||||
NoAction(bool),
|
||||
}
|
||||
|
||||
/// Given an old ast, old program memory and new ast, find the parts of the code that need to be
|
||||
/// re-executed.
|
||||
/// This function should never error, because in the case of any internal error, we should just pop
|
||||
/// the cache.
|
||||
///
|
||||
/// Returns `None` when there are no changes to the program, i.e. it is
|
||||
/// fully cached.
|
||||
pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
|
||||
let mut try_reapply_settings = false;
|
||||
|
||||
// If the settings are different we might need to bust the cache.
|
||||
// We specifically do this before checking if they are the exact same.
|
||||
if old.settings != new.settings {
|
||||
// If the units are different we need to re-execute the whole thing.
|
||||
if old.settings.units != new.settings.units {
|
||||
return CacheResult::ReExecute {
|
||||
clear_scene: true,
|
||||
reapply_settings: true,
|
||||
program: new.ast.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
// If anything else is different we may not need to re-execute, but rather just
|
||||
// run the settings again.
|
||||
try_reapply_settings = true;
|
||||
}
|
||||
|
||||
// If the ASTs are the EXACT same we return None.
|
||||
// We don't even need to waste time computing the digests.
|
||||
if old.ast == new.ast {
|
||||
return CacheResult::NoAction(try_reapply_settings);
|
||||
}
|
||||
|
||||
// We have to clone just because the digests are stored inline :-(
|
||||
let mut old_ast = old.ast.clone();
|
||||
let mut new_ast = new.ast.clone();
|
||||
|
||||
// The digests should already be computed, but just in case we don't
|
||||
// want to compare against none.
|
||||
old_ast.compute_digest();
|
||||
new_ast.compute_digest();
|
||||
|
||||
// Check if the digest is the same.
|
||||
if old_ast.digest == new_ast.digest {
|
||||
return CacheResult::NoAction(try_reapply_settings);
|
||||
}
|
||||
|
||||
// Check if the changes were only to Non-code areas, like comments or whitespace.
|
||||
let (clear_scene, program) = generate_changed_program(old_ast, new_ast);
|
||||
CacheResult::ReExecute {
|
||||
clear_scene,
|
||||
reapply_settings: try_reapply_settings,
|
||||
program,
|
||||
}
|
||||
}
|
||||
|
||||
/// Force-generate a new CacheResult, even if one shouldn't be made. The
|
||||
/// way in which this gets invoked should always be through
|
||||
/// [get_changed_program]. This is purely to contain the logic on
|
||||
/// how we construct a new [CacheResult].
|
||||
fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>) -> (bool, Node<Program>) {
|
||||
if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
|
||||
let old_node: WalkNode = old.into();
|
||||
let new_node: WalkNode = new.into();
|
||||
old_node.digest() == new_node.digest()
|
||||
}) {
|
||||
// If any of the nodes are different in the stretch of body that
|
||||
// overlaps, we have to bust cache and rebuild the scene. This
|
||||
// means a single insertion or deletion will result in a cache
|
||||
// bust.
|
||||
|
||||
return (true, new_ast);
|
||||
}
|
||||
|
||||
// otherwise the overlapping section of the ast bodies matches.
|
||||
// Let's see what the rest of the slice looks like.
|
||||
|
||||
match new_ast.body.len().cmp(&old_ast.body.len()) {
|
||||
std::cmp::Ordering::Less => {
|
||||
// the new AST is shorter than the old AST -- statements
|
||||
// were removed from the "current" code in the "new" code.
|
||||
//
|
||||
// Statements up until now match which means this is a
|
||||
// "pure delete" of the remaining slice, when we get to
|
||||
// supporting that.
|
||||
|
||||
// Cache bust time.
|
||||
(true, new_ast)
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
// the new AST is longer than the old AST, which means
|
||||
// statements were added to the new code we haven't previously
|
||||
// seen.
|
||||
//
|
||||
// Statements up until now are the same, which means this
|
||||
// is a "pure addition" of the remaining slice.
|
||||
|
||||
new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
|
||||
|
||||
(false, new_ast)
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
// currently unreachable, but let's pretend like the code
|
||||
// above can do something meaningful here for when we get
|
||||
// to diffing and yanking chunks of the program apart.
|
||||
|
||||
// We don't actually want to do anything here; so we're going
|
||||
// to not clear and do nothing. Is this wrong? I don't think
|
||||
// so but i think many things. This def needs to change
|
||||
// when the code above changes.
|
||||
|
||||
new_ast.body = vec![];
|
||||
|
||||
(false, new_ast)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a cache check.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct CacheResult {
|
||||
/// Should we clear the scene and start over?
|
||||
pub clear_scene: bool,
|
||||
/// The program that needs to be executed.
|
||||
pub program: Node<Program>,
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::execution::parse_execute;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code() {
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line(end = [24, 0])
|
||||
|> line(end = [0, -24])
|
||||
|> line(end = [-24, 0])
|
||||
|> close()
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program, ctx, _) = parse_execute(new).await.unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(result, CacheResult::NoAction(false));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_changed_whitespace() {
|
||||
let old = r#" // Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line(end = [24, 0])
|
||||
|> line(end = [0, -24])
|
||||
|> line(end = [-24, 0])
|
||||
|> close()
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line(end = [24, 0])
|
||||
|> line(end = [0, -24])
|
||||
|> line(end = [-24, 0])
|
||||
|> close()
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program_old, ctx, _) = parse_execute(old).await.unwrap();
|
||||
|
||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
ast: &program_old.ast,
|
||||
settings: &ctx.settings,
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program_new.ast,
|
||||
settings: &ctx.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(result, CacheResult::NoAction(false));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
|
||||
let old = r#" // Removed the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line(end = [24, 0])
|
||||
|> line(end = [0, -24])
|
||||
|> line(end = [-24, 0])
|
||||
|> close()
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line(end = [24, 0])
|
||||
|> line(end = [0, -24])
|
||||
|> line(end = [-24, 0])
|
||||
|> close()
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program, ctx, _) = parse_execute(old).await.unwrap();
|
||||
|
||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program_new.ast,
|
||||
settings: &ctx.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(result, CacheResult::NoAction(false));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_changed_code_comments() {
|
||||
let old = r#" // Removed the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line(end = [24, 0])
|
||||
|> line(end = [0, -24])
|
||||
|> line(end = [-24, 0]) // my thing
|
||||
|> close()
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line(end = [24, 0])
|
||||
|> line(end = [0, -24])
|
||||
|> line(end = [-24, 0])
|
||||
|> close()
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program, ctx, _) = parse_execute(old).await.unwrap();
|
||||
|
||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program_new.ast,
|
||||
settings: &ctx.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(result, CacheResult::NoAction(false));
|
||||
}
|
||||
|
||||
// Changing the units with the exact same file should bust the cache.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_but_different_units() {
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line(end = [24, 0])
|
||||
|> line(end = [0, -24])
|
||||
|> line(end = [-24, 0])
|
||||
|> close()
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program, mut ctx, _) = parse_execute(new).await.unwrap();
|
||||
|
||||
// Change the settings to cm.
|
||||
ctx.settings.units = crate::UnitLength::Cm;
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &Default::default(),
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
CacheResult::ReExecute {
|
||||
clear_scene: true,
|
||||
reapply_settings: true,
|
||||
program: program.ast
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Changing the grid settings with the exact same file should NOT bust the cache.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_but_different_grid_setting() {
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line(end = [24, 0])
|
||||
|> line(end = [0, -24])
|
||||
|> line(end = [-24, 0])
|
||||
|> close()
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program, mut ctx, _) = parse_execute(new).await.unwrap();
|
||||
|
||||
// Change the settings.
|
||||
ctx.settings.show_grid = !ctx.settings.show_grid;
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &Default::default(),
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(result, CacheResult::NoAction(true));
|
||||
}
|
||||
|
||||
// Changing the edge visibility settings with the exact same file should NOT bust the cache.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_but_different_edge_visiblity_setting() {
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line(end = [24, 0])
|
||||
|> line(end = [0, -24])
|
||||
|> line(end = [-24, 0])
|
||||
|> close()
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program, mut ctx, _) = parse_execute(new).await.unwrap();
|
||||
|
||||
// Change the settings.
|
||||
ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &Default::default(),
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(result, CacheResult::NoAction(true));
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,460 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use async_recursion::async_recursion;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use super::cad_op::{OpArg, Operation};
|
||||
use crate::{
|
||||
engine::ExecutionKind,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
execution::{
|
||||
BodyType, ExecState, ExecutorContext, KclValue, Metadata, StatementKind, TagEngineInfo, TagIdentifier,
|
||||
annotations,
|
||||
cad_op::{OpArg, Operation},
|
||||
state::ModuleState,
|
||||
BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ModuleRepr, ProgramMemory,
|
||||
TagEngineInfo, TagIdentifier,
|
||||
},
|
||||
fs::FileSystem,
|
||||
parsing::ast::types::{
|
||||
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, CallExpression,
|
||||
CallExpressionKw, Expr, IfExpression, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node,
|
||||
ObjectExpression, PipeExpression, TagDeclarator, UnaryExpression, UnaryOperator,
|
||||
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
|
||||
CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility,
|
||||
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, NodeRef, NonCodeValue, ObjectExpression,
|
||||
PipeExpression, TagDeclarator, UnaryExpression, UnaryOperator,
|
||||
},
|
||||
source_range::SourceRange,
|
||||
source_range::{ModuleId, SourceRange},
|
||||
std::{
|
||||
args::{Arg, KwArgs},
|
||||
FunctionKind,
|
||||
},
|
||||
};
|
||||
|
||||
enum StatementKind<'a> {
|
||||
Declaration { name: &'a str },
|
||||
Expression,
|
||||
}
|
||||
|
||||
impl ExecutorContext {
|
||||
async fn handle_annotations(
|
||||
&self,
|
||||
annotations: impl Iterator<Item = (&NonCodeValue, SourceRange)>,
|
||||
scope: annotations::AnnotationScope,
|
||||
exec_state: &mut ExecState,
|
||||
) -> Result<(), KclError> {
|
||||
for (annotation, source_range) in annotations {
|
||||
if annotation.annotation_name() == Some(annotations::SETTINGS) {
|
||||
if scope == annotations::AnnotationScope::Module {
|
||||
let old_units = exec_state.length_unit();
|
||||
exec_state
|
||||
.mod_local
|
||||
.settings
|
||||
.update_from_annotation(annotation, source_range)?;
|
||||
let new_units = exec_state.length_unit();
|
||||
if old_units != new_units {
|
||||
self.engine.set_units(new_units.into(), source_range).await?;
|
||||
}
|
||||
} else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Settings can only be modified at the top level scope of a file".to_owned(),
|
||||
source_ranges: vec![source_range],
|
||||
}));
|
||||
}
|
||||
}
|
||||
// TODO warn on unknown annotations
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute an AST's program.
|
||||
#[async_recursion]
|
||||
pub(super) async fn exec_program<'a>(
|
||||
&'a self,
|
||||
program: NodeRef<'a, crate::parsing::ast::types::Program>,
|
||||
exec_state: &mut ExecState,
|
||||
body_type: BodyType,
|
||||
) -> Result<Option<KclValue>, KclError> {
|
||||
self.handle_annotations(
|
||||
program
|
||||
.non_code_meta
|
||||
.start_nodes
|
||||
.iter()
|
||||
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))),
|
||||
annotations::AnnotationScope::Module,
|
||||
exec_state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut last_expr = None;
|
||||
// Iterate over the body of the program.
|
||||
for statement in &program.body {
|
||||
match statement {
|
||||
BodyItem::ImportStatement(import_stmt) => {
|
||||
let source_range = SourceRange::from(import_stmt);
|
||||
let module_id = self.open_module(&import_stmt.path, exec_state, source_range).await?;
|
||||
|
||||
match &import_stmt.selector {
|
||||
ImportSelector::List { items } => {
|
||||
let (_, module_memory, module_exports) = self
|
||||
.exec_module(module_id, exec_state, ExecutionKind::Isolated, source_range)
|
||||
.await?;
|
||||
for import_item in items {
|
||||
// Extract the item from the module.
|
||||
let item =
|
||||
module_memory
|
||||
.get(&import_item.name.name, import_item.into())
|
||||
.map_err(|_err| {
|
||||
KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("{} is not defined in module", import_item.name.name),
|
||||
source_ranges: vec![SourceRange::from(&import_item.name)],
|
||||
})
|
||||
})?;
|
||||
// Check that the item is allowed to be imported.
|
||||
if !module_exports.contains(&import_item.name.name) {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
|
||||
import_item.name.name
|
||||
),
|
||||
source_ranges: vec![SourceRange::from(&import_item.name)],
|
||||
}));
|
||||
}
|
||||
|
||||
// Add the item to the current module.
|
||||
exec_state.mut_memory().add(
|
||||
import_item.identifier(),
|
||||
item.clone(),
|
||||
SourceRange::from(&import_item.name),
|
||||
)?;
|
||||
|
||||
if let ItemVisibility::Export = import_stmt.visibility {
|
||||
exec_state
|
||||
.mod_local
|
||||
.module_exports
|
||||
.push(import_item.identifier().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
ImportSelector::Glob(_) => {
|
||||
let (_, module_memory, module_exports) = self
|
||||
.exec_module(module_id, exec_state, ExecutionKind::Isolated, source_range)
|
||||
.await?;
|
||||
for name in module_exports.iter() {
|
||||
let item = module_memory.get(name, source_range).map_err(|_err| {
|
||||
KclError::Internal(KclErrorDetails {
|
||||
message: format!("{} is not defined in module (but was exported?)", name),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
})?;
|
||||
exec_state.mut_memory().add(name, item.clone(), source_range)?;
|
||||
|
||||
if let ItemVisibility::Export = import_stmt.visibility {
|
||||
exec_state.mod_local.module_exports.push(name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
ImportSelector::None { .. } => {
|
||||
let name = import_stmt.module_name().unwrap();
|
||||
let item = KclValue::Module {
|
||||
value: module_id,
|
||||
meta: vec![source_range.into()],
|
||||
};
|
||||
exec_state.mut_memory().add(&name, item, source_range)?;
|
||||
}
|
||||
}
|
||||
last_expr = None;
|
||||
}
|
||||
BodyItem::ExpressionStatement(expression_statement) => {
|
||||
let metadata = Metadata::from(expression_statement);
|
||||
last_expr = Some(
|
||||
self.execute_expr(
|
||||
&expression_statement.expression,
|
||||
exec_state,
|
||||
&metadata,
|
||||
StatementKind::Expression,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
BodyItem::VariableDeclaration(variable_declaration) => {
|
||||
let var_name = variable_declaration.declaration.id.name.to_string();
|
||||
let source_range = SourceRange::from(&variable_declaration.declaration.init);
|
||||
let metadata = Metadata { source_range };
|
||||
|
||||
let memory_item = self
|
||||
.execute_expr(
|
||||
&variable_declaration.declaration.init,
|
||||
exec_state,
|
||||
&metadata,
|
||||
StatementKind::Declaration { name: &var_name },
|
||||
)
|
||||
.await?;
|
||||
exec_state.mut_memory().add(&var_name, memory_item, source_range)?;
|
||||
|
||||
// Track exports.
|
||||
if let ItemVisibility::Export = variable_declaration.visibility {
|
||||
exec_state.mod_local.module_exports.push(var_name);
|
||||
}
|
||||
last_expr = None;
|
||||
}
|
||||
BodyItem::ReturnStatement(return_statement) => {
|
||||
let metadata = Metadata::from(return_statement);
|
||||
let value = self
|
||||
.execute_expr(
|
||||
&return_statement.argument,
|
||||
exec_state,
|
||||
&metadata,
|
||||
StatementKind::Expression,
|
||||
)
|
||||
.await?;
|
||||
exec_state.mut_memory().return_ = Some(value);
|
||||
last_expr = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if BodyType::Root == body_type {
|
||||
// Flush the batch queue.
|
||||
self.engine
|
||||
.flush_batch(
|
||||
// True here tells the engine to flush all the end commands as well like fillets
|
||||
// and chamfers where the engine would otherwise eat the ID of the segments.
|
||||
true,
|
||||
SourceRange::new(program.end, program.end, program.module_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(last_expr)
|
||||
}
|
||||
|
||||
async fn open_module(
|
||||
&self,
|
||||
path: &ImportPath,
|
||||
exec_state: &mut ExecState,
|
||||
source_range: SourceRange,
|
||||
) -> Result<ModuleId, KclError> {
|
||||
match path {
|
||||
ImportPath::Kcl { filename } => {
|
||||
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
|
||||
project_dir.join(filename)
|
||||
} else {
|
||||
std::path::PathBuf::from(filename)
|
||||
};
|
||||
|
||||
if exec_state.mod_local.import_stack.contains(&resolved_path) {
|
||||
return Err(KclError::ImportCycle(KclErrorDetails {
|
||||
message: format!(
|
||||
"circular import of modules is not allowed: {} -> {}",
|
||||
exec_state
|
||||
.mod_local
|
||||
.import_stack
|
||||
.iter()
|
||||
.map(|p| p.as_path().to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" -> "),
|
||||
resolved_path.to_string_lossy()
|
||||
),
|
||||
source_ranges: vec![source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
|
||||
return Ok(*id);
|
||||
}
|
||||
|
||||
let source = self.fs.read_to_string(&resolved_path, source_range).await?;
|
||||
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
|
||||
// TODO handle parsing errors properly
|
||||
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?;
|
||||
let repr = ModuleRepr::Kcl(parsed);
|
||||
|
||||
Ok(exec_state.add_module(id, resolved_path, repr))
|
||||
}
|
||||
ImportPath::Foreign { path } => {
|
||||
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
|
||||
project_dir.join(path)
|
||||
} else {
|
||||
std::path::PathBuf::from(path)
|
||||
};
|
||||
|
||||
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
|
||||
return Ok(*id);
|
||||
}
|
||||
|
||||
let geom = super::import::import_foreign(&resolved_path, None, exec_state, self, source_range).await?;
|
||||
let repr = ModuleRepr::Foreign(geom);
|
||||
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
|
||||
Ok(exec_state.add_module(id, resolved_path, repr))
|
||||
}
|
||||
i => Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Unsupported import: `{i}`"),
|
||||
source_ranges: vec![source_range],
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
async fn exec_module(
|
||||
&self,
|
||||
module_id: ModuleId,
|
||||
exec_state: &mut ExecState,
|
||||
exec_kind: ExecutionKind,
|
||||
source_range: SourceRange,
|
||||
) -> Result<(Option<KclValue>, ProgramMemory, Vec<String>), KclError> {
|
||||
let old_units = exec_state.length_unit();
|
||||
// TODO It sucks that we have to clone the whole module AST here
|
||||
let info = exec_state.global.module_infos[&module_id].clone();
|
||||
|
||||
match &info.repr {
|
||||
ModuleRepr::Root => Err(KclError::ImportCycle(KclErrorDetails {
|
||||
message: format!(
|
||||
"circular import of modules is not allowed: {} -> {}",
|
||||
exec_state
|
||||
.mod_local
|
||||
.import_stack
|
||||
.iter()
|
||||
.map(|p| p.as_path().to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" -> "),
|
||||
info.path.display()
|
||||
),
|
||||
source_ranges: vec![source_range],
|
||||
})),
|
||||
ModuleRepr::Kcl(program) => {
|
||||
let mut local_state = ModuleState {
|
||||
import_stack: exec_state.mod_local.import_stack.clone(),
|
||||
..ModuleState::new(&self.settings)
|
||||
};
|
||||
local_state.import_stack.push(info.path.clone());
|
||||
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
||||
let original_execution = self.engine.replace_execution_kind(exec_kind);
|
||||
|
||||
let result = self
|
||||
.exec_program(program, exec_state, crate::execution::BodyType::Root)
|
||||
.await;
|
||||
|
||||
let new_units = exec_state.length_unit();
|
||||
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
||||
if new_units != old_units {
|
||||
self.engine.set_units(old_units.into(), Default::default()).await?;
|
||||
}
|
||||
self.engine.replace_execution_kind(original_execution);
|
||||
|
||||
let result = result.map_err(|err| {
|
||||
if let KclError::ImportCycle(_) = err {
|
||||
// It was an import cycle. Keep the original message.
|
||||
err.override_source_ranges(vec![source_range])
|
||||
} else {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"Error loading imported file. Open it to view more details. {}: {}",
|
||||
info.path.display(),
|
||||
err.message()
|
||||
),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok((result, local_state.memory, local_state.module_exports))
|
||||
}
|
||||
ModuleRepr::Foreign(geom) => {
|
||||
let geom = super::import::send_to_engine(geom.clone(), self).await?;
|
||||
Ok((Some(KclValue::ImportedGeometry(geom)), ProgramMemory::new(), Vec::new()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
async fn execute_expr<'a: 'async_recursion>(
|
||||
&self,
|
||||
init: &Expr,
|
||||
exec_state: &mut ExecState,
|
||||
metadata: &Metadata,
|
||||
statement_kind: StatementKind<'a>,
|
||||
) -> Result<KclValue, KclError> {
|
||||
let item = match init {
|
||||
Expr::None(none) => KclValue::from(none),
|
||||
Expr::Literal(literal) => KclValue::from(literal),
|
||||
Expr::TagDeclarator(tag) => tag.execute(exec_state).await?,
|
||||
Expr::Identifier(identifier) => {
|
||||
let value = exec_state.memory().get(&identifier.name, identifier.into())?.clone();
|
||||
if let KclValue::Module { value: module_id, meta } = value {
|
||||
let (result, _, _) = self
|
||||
.exec_module(module_id, exec_state, ExecutionKind::Normal, metadata.source_range)
|
||||
.await?;
|
||||
result.unwrap_or_else(|| {
|
||||
// The module didn't have a return value. Currently,
|
||||
// the only way to have a return value is with the final
|
||||
// statement being an expression statement.
|
||||
//
|
||||
// TODO: Make a warning when we support them in the
|
||||
// execution phase.
|
||||
let mut new_meta = vec![metadata.to_owned()];
|
||||
new_meta.extend(meta);
|
||||
KclValue::KclNone {
|
||||
value: Default::default(),
|
||||
meta: new_meta,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
Expr::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, self).await?,
|
||||
Expr::FunctionExpression(function_expression) => {
|
||||
// Cloning memory here is crucial for semantics so that we close
|
||||
// over variables. Variables defined lexically later shouldn't
|
||||
// be available to the function body.
|
||||
KclValue::Function {
|
||||
expression: function_expression.clone(),
|
||||
meta: vec![metadata.to_owned()],
|
||||
func: None,
|
||||
memory: Box::new(exec_state.memory().clone()),
|
||||
}
|
||||
}
|
||||
Expr::CallExpression(call_expression) => call_expression.execute(exec_state, self).await?,
|
||||
Expr::CallExpressionKw(call_expression) => call_expression.execute(exec_state, self).await?,
|
||||
Expr::PipeExpression(pipe_expression) => pipe_expression.get_result(exec_state, self).await?,
|
||||
Expr::PipeSubstitution(pipe_substitution) => match statement_kind {
|
||||
StatementKind::Declaration { name } => {
|
||||
let message = format!(
|
||||
"you cannot declare variable {name} as %, because % can only be used in function calls"
|
||||
);
|
||||
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message,
|
||||
source_ranges: vec![pipe_substitution.into()],
|
||||
}));
|
||||
}
|
||||
StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "cannot use % outside a pipe expression".to_owned(),
|
||||
source_ranges: vec![pipe_substitution.into()],
|
||||
}));
|
||||
}
|
||||
},
|
||||
},
|
||||
Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?,
|
||||
Expr::ArrayRangeExpression(range_expression) => range_expression.execute(exec_state, self).await?,
|
||||
Expr::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?,
|
||||
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
|
||||
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
|
||||
Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
|
||||
Expr::LabelledExpression(expr) => {
|
||||
let result = self
|
||||
.execute_expr(&expr.expr, exec_state, metadata, statement_kind)
|
||||
.await?;
|
||||
exec_state
|
||||
.mut_memory()
|
||||
.add(&expr.label.name, result.clone(), init.into())?;
|
||||
// TODO this lets us use the label as a variable name, but not as a tag in most cases
|
||||
result
|
||||
}
|
||||
};
|
||||
Ok(item)
|
||||
}
|
||||
}
|
||||
|
||||
impl BinaryPart {
|
||||
#[async_recursion]
|
||||
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||
@ -862,7 +1297,7 @@ impl Node<IfExpression> {
|
||||
.await?
|
||||
.get_bool()?;
|
||||
if cond {
|
||||
let block_result = ctx.inner_execute(&self.then_val, exec_state, BodyType::Block).await?;
|
||||
let block_result = ctx.exec_program(&self.then_val, exec_state, BodyType::Block).await?;
|
||||
// Block must end in an expression, so this has to be Some.
|
||||
// Enforced by the parser.
|
||||
// See https://github.com/KittyCAD/modeling-app/issues/4015
|
||||
@ -881,9 +1316,7 @@ impl Node<IfExpression> {
|
||||
.await?
|
||||
.get_bool()?;
|
||||
if cond {
|
||||
let block_result = ctx
|
||||
.inner_execute(&else_if.then_val, exec_state, BodyType::Block)
|
||||
.await?;
|
||||
let block_result = ctx.exec_program(&else_if.then_val, exec_state, BodyType::Block).await?;
|
||||
// Block must end in an expression, so this has to be Some.
|
||||
// Enforced by the parser.
|
||||
// See https://github.com/KittyCAD/modeling-app/issues/4015
|
||||
@ -892,7 +1325,7 @@ impl Node<IfExpression> {
|
||||
}
|
||||
|
||||
// Run the final `else` branch.
|
||||
ctx.inner_execute(&self.final_else, exec_state, BodyType::Block)
|
||||
ctx.exec_program(&self.final_else, exec_state, BodyType::Block)
|
||||
.await
|
||||
.map(|expr| expr.unwrap())
|
||||
}
|
||||
@ -1000,3 +1433,343 @@ impl Node<PipeExpression> {
|
||||
execute_pipe_body(exec_state, &self.body, self.into(), ctx).await
|
||||
}
|
||||
}
|
||||
|
||||
/// For each argument given,
|
||||
/// assign it to a parameter of the function, in the given block of function memory.
|
||||
/// Returns Err if too few/too many arguments were given for the function.
|
||||
fn assign_args_to_params(
|
||||
function_expression: NodeRef<'_, FunctionExpression>,
|
||||
args: Vec<Arg>,
|
||||
mut fn_memory: ProgramMemory,
|
||||
) -> Result<ProgramMemory, KclError> {
|
||||
let num_args = function_expression.number_of_args();
|
||||
let (min_params, max_params) = num_args.into_inner();
|
||||
let n = args.len();
|
||||
|
||||
// Check if the user supplied too many arguments
|
||||
// (we'll check for too few arguments below).
|
||||
let err_wrong_number_args = KclError::Semantic(KclErrorDetails {
|
||||
message: if min_params == max_params {
|
||||
format!("Expected {min_params} arguments, got {n}")
|
||||
} else {
|
||||
format!("Expected {min_params}-{max_params} arguments, got {n}")
|
||||
},
|
||||
source_ranges: vec![function_expression.into()],
|
||||
});
|
||||
if n > max_params {
|
||||
return Err(err_wrong_number_args);
|
||||
}
|
||||
|
||||
// Add the arguments to the memory. A new call frame should have already
|
||||
// been created.
|
||||
for (index, param) in function_expression.params.iter().enumerate() {
|
||||
if let Some(arg) = args.get(index) {
|
||||
// Argument was provided.
|
||||
fn_memory.add(¶m.identifier.name, arg.value.clone(), (¶m.identifier).into())?;
|
||||
} else {
|
||||
// Argument was not provided.
|
||||
if let Some(ref default_val) = param.default_value {
|
||||
// If the corresponding parameter is optional,
|
||||
// then it's fine, the user doesn't need to supply it.
|
||||
fn_memory.add(
|
||||
¶m.identifier.name,
|
||||
default_val.clone().into(),
|
||||
(¶m.identifier).into(),
|
||||
)?;
|
||||
} else {
|
||||
// But if the corresponding parameter was required,
|
||||
// then the user has called with too few arguments.
|
||||
return Err(err_wrong_number_args);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(fn_memory)
|
||||
}
|
||||
|
||||
fn assign_args_to_params_kw(
|
||||
function_expression: NodeRef<'_, FunctionExpression>,
|
||||
mut args: crate::std::args::KwArgs,
|
||||
mut fn_memory: ProgramMemory,
|
||||
) -> Result<ProgramMemory, KclError> {
|
||||
// Add the arguments to the memory. A new call frame should have already
|
||||
// been created.
|
||||
let source_ranges = vec![function_expression.into()];
|
||||
for param in function_expression.params.iter() {
|
||||
if param.labeled {
|
||||
let arg = args.labeled.get(¶m.identifier.name);
|
||||
let arg_val = match arg {
|
||||
Some(arg) => arg.value.clone(),
|
||||
None => match param.default_value {
|
||||
Some(ref default_val) => KclValue::from(default_val.clone()),
|
||||
None => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges,
|
||||
message: format!(
|
||||
"This function requires a parameter {}, but you haven't passed it one.",
|
||||
param.identifier.name
|
||||
),
|
||||
}));
|
||||
}
|
||||
},
|
||||
};
|
||||
fn_memory.add(¶m.identifier.name, arg_val, (¶m.identifier).into())?;
|
||||
} else {
|
||||
let Some(unlabeled) = args.unlabeled.take() else {
|
||||
let param_name = ¶m.identifier.name;
|
||||
return Err(if args.labeled.contains_key(param_name) {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
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 {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
source_ranges,
|
||||
message: "This function expects an unlabeled first parameter, but you haven't passed it one."
|
||||
.to_owned(),
|
||||
})
|
||||
});
|
||||
};
|
||||
fn_memory.add(
|
||||
¶m.identifier.name,
|
||||
unlabeled.value.clone(),
|
||||
(¶m.identifier).into(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(fn_memory)
|
||||
}
|
||||
|
||||
pub(crate) async fn call_user_defined_function(
|
||||
args: Vec<Arg>,
|
||||
memory: &ProgramMemory,
|
||||
function_expression: NodeRef<'_, FunctionExpression>,
|
||||
exec_state: &mut ExecState,
|
||||
ctx: &ExecutorContext,
|
||||
) -> Result<Option<KclValue>, KclError> {
|
||||
// Create a new environment to execute the function body in so that local
|
||||
// variables shadow variables in the parent scope. The new environment's
|
||||
// parent should be the environment of the closure.
|
||||
let mut body_memory = memory.clone();
|
||||
let body_env = body_memory.new_env_for_call(memory.current_env);
|
||||
body_memory.current_env = body_env;
|
||||
let fn_memory = assign_args_to_params(function_expression, args, body_memory)?;
|
||||
|
||||
// Execute the function body using the memory we just created.
|
||||
let (result, fn_memory) = {
|
||||
let previous_memory = std::mem::replace(&mut exec_state.mod_local.memory, fn_memory);
|
||||
let result = ctx
|
||||
.exec_program(&function_expression.body, exec_state, BodyType::Block)
|
||||
.await;
|
||||
// Restore the previous memory.
|
||||
let fn_memory = std::mem::replace(&mut exec_state.mod_local.memory, previous_memory);
|
||||
|
||||
(result, fn_memory)
|
||||
};
|
||||
|
||||
result.map(|_| fn_memory.return_)
|
||||
}
|
||||
|
||||
pub(crate) async fn call_user_defined_function_kw(
|
||||
args: crate::std::args::KwArgs,
|
||||
memory: &ProgramMemory,
|
||||
function_expression: NodeRef<'_, FunctionExpression>,
|
||||
exec_state: &mut ExecState,
|
||||
ctx: &ExecutorContext,
|
||||
) -> Result<Option<KclValue>, KclError> {
|
||||
// Create a new environment to execute the function body in so that local
|
||||
// variables shadow variables in the parent scope. The new environment's
|
||||
// parent should be the environment of the closure.
|
||||
let mut body_memory = memory.clone();
|
||||
let body_env = body_memory.new_env_for_call(memory.current_env);
|
||||
body_memory.current_env = body_env;
|
||||
let fn_memory = assign_args_to_params_kw(function_expression, args, body_memory)?;
|
||||
|
||||
// Execute the function body using the memory we just created.
|
||||
let (result, fn_memory) = {
|
||||
let previous_memory = std::mem::replace(&mut exec_state.mod_local.memory, fn_memory);
|
||||
let result = ctx
|
||||
.exec_program(&function_expression.body, exec_state, BodyType::Block)
|
||||
.await;
|
||||
// Restore the previous memory.
|
||||
let fn_memory = std::mem::replace(&mut exec_state.mod_local.memory, previous_memory);
|
||||
|
||||
(result, fn_memory)
|
||||
};
|
||||
|
||||
result.map(|_| fn_memory.return_)
|
||||
}
|
||||
|
||||
/// A function being used as a parameter into a stdlib function. This is a
|
||||
/// closure, plus everything needed to execute it.
|
||||
pub struct FunctionParam<'a> {
|
||||
pub inner: Option<&'a MemoryFunction>,
|
||||
pub memory: ProgramMemory,
|
||||
pub fn_expr: crate::parsing::ast::types::BoxNode<FunctionExpression>,
|
||||
pub meta: Vec<Metadata>,
|
||||
pub ctx: ExecutorContext,
|
||||
}
|
||||
|
||||
impl<'a> FunctionParam<'a> {
|
||||
pub async fn call(&self, exec_state: &mut ExecState, args: Vec<Arg>) -> Result<Option<KclValue>, KclError> {
|
||||
if let Some(inner) = self.inner {
|
||||
inner(
|
||||
args,
|
||||
self.memory.clone(),
|
||||
self.fn_expr.clone(),
|
||||
self.meta.clone(),
|
||||
exec_state,
|
||||
self.ctx.clone(),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
call_user_defined_function(args, &self.memory, self.fn_expr.as_ref(), exec_state, &self.ctx).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for FunctionParam<'_> {
|
||||
fn schema_name() -> String {
|
||||
"FunctionParam".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
// TODO: Actually generate a reasonable schema.
|
||||
gen.subschema_for::<()>()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::parsing::ast::types::{DefaultParamVal, Identifier, Parameter};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_assign_args_to_params() {
|
||||
// Set up a little framework for this test.
|
||||
fn mem(number: usize) -> KclValue {
|
||||
KclValue::Int {
|
||||
value: number as i64,
|
||||
meta: Default::default(),
|
||||
}
|
||||
}
|
||||
fn ident(s: &'static str) -> Node<Identifier> {
|
||||
Node::no_src(Identifier {
|
||||
name: s.to_owned(),
|
||||
digest: None,
|
||||
})
|
||||
}
|
||||
fn opt_param(s: &'static str) -> Parameter {
|
||||
Parameter {
|
||||
identifier: ident(s),
|
||||
type_: None,
|
||||
default_value: Some(DefaultParamVal::none()),
|
||||
labeled: true,
|
||||
digest: None,
|
||||
}
|
||||
}
|
||||
fn req_param(s: &'static str) -> Parameter {
|
||||
Parameter {
|
||||
identifier: ident(s),
|
||||
type_: None,
|
||||
default_value: None,
|
||||
labeled: true,
|
||||
digest: None,
|
||||
}
|
||||
}
|
||||
fn additional_program_memory(items: &[(String, KclValue)]) -> ProgramMemory {
|
||||
let mut program_memory = ProgramMemory::new();
|
||||
for (name, item) in items {
|
||||
program_memory
|
||||
.add(name.as_str(), item.clone(), SourceRange::default())
|
||||
.unwrap();
|
||||
}
|
||||
program_memory
|
||||
}
|
||||
// Declare the test cases.
|
||||
for (test_name, params, args, expected) in [
|
||||
("empty", Vec::new(), Vec::new(), Ok(ProgramMemory::new())),
|
||||
(
|
||||
"all params required, and all given, should be OK",
|
||||
vec![req_param("x")],
|
||||
vec![mem(1)],
|
||||
Ok(additional_program_memory(&[("x".to_owned(), mem(1))])),
|
||||
),
|
||||
(
|
||||
"all params required, none given, should error",
|
||||
vec![req_param("x")],
|
||||
vec![],
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: vec![SourceRange::default()],
|
||||
message: "Expected 1 arguments, got 0".to_owned(),
|
||||
})),
|
||||
),
|
||||
(
|
||||
"all params optional, none given, should be OK",
|
||||
vec![opt_param("x")],
|
||||
vec![],
|
||||
Ok(additional_program_memory(&[("x".to_owned(), KclValue::none())])),
|
||||
),
|
||||
(
|
||||
"mixed params, too few given",
|
||||
vec![req_param("x"), opt_param("y")],
|
||||
vec![],
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: vec![SourceRange::default()],
|
||||
message: "Expected 1-2 arguments, got 0".to_owned(),
|
||||
})),
|
||||
),
|
||||
(
|
||||
"mixed params, minimum given, should be OK",
|
||||
vec![req_param("x"), opt_param("y")],
|
||||
vec![mem(1)],
|
||||
Ok(additional_program_memory(&[
|
||||
("x".to_owned(), mem(1)),
|
||||
("y".to_owned(), KclValue::none()),
|
||||
])),
|
||||
),
|
||||
(
|
||||
"mixed params, maximum given, should be OK",
|
||||
vec![req_param("x"), opt_param("y")],
|
||||
vec![mem(1), mem(2)],
|
||||
Ok(additional_program_memory(&[
|
||||
("x".to_owned(), mem(1)),
|
||||
("y".to_owned(), mem(2)),
|
||||
])),
|
||||
),
|
||||
(
|
||||
"mixed params, too many given",
|
||||
vec![req_param("x"), opt_param("y")],
|
||||
vec![mem(1), mem(2), mem(3)],
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: vec![SourceRange::default()],
|
||||
message: "Expected 1-2 arguments, got 3".to_owned(),
|
||||
})),
|
||||
),
|
||||
] {
|
||||
// Run each test.
|
||||
let func_expr = &Node::no_src(FunctionExpression {
|
||||
params,
|
||||
body: Node {
|
||||
inner: crate::parsing::ast::types::Program {
|
||||
body: Vec::new(),
|
||||
non_code_meta: Default::default(),
|
||||
shebang: None,
|
||||
digest: None,
|
||||
},
|
||||
start: 0,
|
||||
end: 0,
|
||||
module_id: ModuleId::default(),
|
||||
},
|
||||
return_type: None,
|
||||
digest: None,
|
||||
});
|
||||
let args = args.into_iter().map(Arg::synthetic).collect();
|
||||
let actual = assign_args_to_params(func_expr, args, ProgramMemory::new());
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"failed test '{test_name}':\ngot {actual:?}\nbut expected\n{expected:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,49 +0,0 @@
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use crate::{
|
||||
errors::KclError,
|
||||
execution::{
|
||||
call_user_defined_function, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ProgramMemory,
|
||||
},
|
||||
parsing::ast::types::FunctionExpression,
|
||||
std::args::Arg,
|
||||
};
|
||||
|
||||
/// A function being used as a parameter into a stdlib function. This is a
|
||||
/// closure, plus everything needed to execute it.
|
||||
pub struct FunctionParam<'a> {
|
||||
pub inner: Option<&'a MemoryFunction>,
|
||||
pub memory: ProgramMemory,
|
||||
pub fn_expr: crate::parsing::ast::types::BoxNode<FunctionExpression>,
|
||||
pub meta: Vec<Metadata>,
|
||||
pub ctx: ExecutorContext,
|
||||
}
|
||||
|
||||
impl<'a> FunctionParam<'a> {
|
||||
pub async fn call(&self, exec_state: &mut ExecState, args: Vec<Arg>) -> Result<Option<KclValue>, KclError> {
|
||||
if let Some(inner) = self.inner {
|
||||
inner(
|
||||
args,
|
||||
self.memory.clone(),
|
||||
self.fn_expr.clone(),
|
||||
self.meta.clone(),
|
||||
exec_state,
|
||||
self.ctx.clone(),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
call_user_defined_function(args, &self.memory, self.fn_expr.as_ref(), exec_state, &self.ctx).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for FunctionParam<'_> {
|
||||
fn schema_name() -> String {
|
||||
"FunctionParam".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
// TODO: Actually generate a reasonable schema.
|
||||
gen.subschema_for::<()>()
|
||||
}
|
||||
}
|
1036
src/wasm-lib/kcl/src/execution/geometry.rs
Normal file
@ -533,7 +533,7 @@ impl KclValue {
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
crate::execution::call_user_defined_function(
|
||||
crate::execution::exec_ast::call_user_defined_function(
|
||||
args,
|
||||
closure_memory.as_ref(),
|
||||
expression.as_ref(),
|
||||
@ -568,7 +568,7 @@ impl KclValue {
|
||||
if let Some(_func) = func {
|
||||
todo!("Implement calling KCL stdlib fns that are aliased. Part of https://github.com/KittyCAD/modeling-app/issues/4600");
|
||||
} else {
|
||||
crate::execution::call_user_defined_function_kw(
|
||||
crate::execution::exec_ast::call_user_defined_function_kw(
|
||||
args.kw_args,
|
||||
closure_memory.as_ref(),
|
||||
expression.as_ref(),
|
||||
|
191
src/wasm-lib/kcl/src/execution/memory.rs
Normal file
@ -0,0 +1,191 @@
|
||||
use anyhow::Result;
|
||||
use indexmap::IndexMap;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
execution::{KclValue, Metadata, Sketch, Solid, TagIdentifier},
|
||||
source_range::SourceRange,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProgramMemory {
|
||||
pub environments: Vec<Environment>,
|
||||
pub current_env: EnvironmentRef,
|
||||
#[serde(rename = "return")]
|
||||
pub return_: Option<KclValue>,
|
||||
}
|
||||
|
||||
impl ProgramMemory {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
environments: vec![Environment::root()],
|
||||
current_env: EnvironmentRef::root(),
|
||||
return_: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_env_for_call(&mut self, parent: EnvironmentRef) -> EnvironmentRef {
|
||||
let new_env_ref = EnvironmentRef(self.environments.len());
|
||||
let new_env = Environment::new(parent);
|
||||
self.environments.push(new_env);
|
||||
new_env_ref
|
||||
}
|
||||
|
||||
/// Add to the program memory in the current scope.
|
||||
pub fn add(&mut self, key: &str, value: KclValue, source_range: SourceRange) -> Result<(), KclError> {
|
||||
if self.environments[self.current_env.index()].contains_key(key) {
|
||||
return Err(KclError::ValueAlreadyDefined(KclErrorDetails {
|
||||
message: format!("Cannot redefine `{}`", key),
|
||||
source_ranges: vec![source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
self.environments[self.current_env.index()].insert(key.to_string(), value);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_tag(&mut self, tag: &str, value: TagIdentifier) -> Result<(), KclError> {
|
||||
self.environments[self.current_env.index()].insert(tag.to_string(), KclValue::TagIdentifier(Box::new(value)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a value from the program memory.
|
||||
/// Return Err if not found.
|
||||
pub fn get(&self, var: &str, source_range: SourceRange) -> Result<&KclValue, KclError> {
|
||||
let mut env_ref = self.current_env;
|
||||
loop {
|
||||
let env = &self.environments[env_ref.index()];
|
||||
if let Some(item) = env.bindings.get(var) {
|
||||
return Ok(item);
|
||||
}
|
||||
if let Some(parent) = env.parent {
|
||||
env_ref = parent;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("memory item key `{}` is not defined", var),
|
||||
source_ranges: vec![source_range],
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns all bindings in the current scope.
|
||||
#[allow(dead_code)]
|
||||
fn get_all_cur_scope(&self) -> IndexMap<String, KclValue> {
|
||||
let env = &self.environments[self.current_env.index()];
|
||||
env.bindings.clone()
|
||||
}
|
||||
|
||||
/// Find all solids in the memory that are on a specific sketch id.
|
||||
/// This does not look inside closures. But as long as we do not allow
|
||||
/// mutation of variables in KCL, closure memory should be a subset of this.
|
||||
#[allow(clippy::vec_box)]
|
||||
pub fn find_solids_on_sketch(&self, sketch_id: uuid::Uuid) -> Vec<Box<Solid>> {
|
||||
self.environments
|
||||
.iter()
|
||||
.flat_map(|env| {
|
||||
env.bindings
|
||||
.values()
|
||||
.filter_map(|item| match item {
|
||||
KclValue::Solid { value } if value.sketch.id == sketch_id => Some(value.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProgramMemory {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// An index pointing to an environment.
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[schemars(transparent)]
|
||||
pub struct EnvironmentRef(usize);
|
||||
|
||||
impl EnvironmentRef {
|
||||
pub fn root() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
pub fn index(&self) -> usize {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
pub struct Environment {
|
||||
pub(super) bindings: IndexMap<String, KclValue>,
|
||||
parent: Option<EnvironmentRef>,
|
||||
}
|
||||
|
||||
const NO_META: Vec<Metadata> = Vec::new();
|
||||
|
||||
impl Environment {
|
||||
pub fn root() -> Self {
|
||||
Self {
|
||||
// Prelude
|
||||
bindings: IndexMap::from([
|
||||
("ZERO".to_string(), KclValue::from_number(0.0, NO_META)),
|
||||
("QUARTER_TURN".to_string(), KclValue::from_number(90.0, NO_META)),
|
||||
("HALF_TURN".to_string(), KclValue::from_number(180.0, NO_META)),
|
||||
("THREE_QUARTER_TURN".to_string(), KclValue::from_number(270.0, NO_META)),
|
||||
]),
|
||||
parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(parent: EnvironmentRef) -> Self {
|
||||
Self {
|
||||
bindings: IndexMap::new(),
|
||||
parent: Some(parent),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str, source_range: SourceRange) -> Result<&KclValue, KclError> {
|
||||
self.bindings.get(key).ok_or_else(|| {
|
||||
KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("memory item key `{}` is not defined", key),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: String, value: KclValue) {
|
||||
self.bindings.insert(key, value);
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, key: &str) -> bool {
|
||||
self.bindings.contains_key(key)
|
||||
}
|
||||
|
||||
pub fn update_sketch_tags(&mut self, sg: &Sketch) {
|
||||
if sg.tags.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (_, val) in self.bindings.iter_mut() {
|
||||
let KclValue::Sketch { value } = val else { continue };
|
||||
let mut sketch = value.to_owned();
|
||||
|
||||
if sketch.original_id == sg.original_id {
|
||||
for tag in sg.tags.iter() {
|
||||
sketch.tags.insert(tag.0.clone(), tag.1.clone());
|
||||
}
|
||||
}
|
||||
*val = KclValue::Sketch { value: sketch };
|
||||
}
|
||||
}
|
||||
}
|
300
src/wasm-lib/kcl/src/execution/state.rs
Normal file
@ -0,0 +1,300 @@
|
||||
use anyhow::Result;
|
||||
use indexmap::IndexMap;
|
||||
use kittycad_modeling_cmds::websocket::WebSocketResponse;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
execution::{
|
||||
annotations, kcl_value, Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, ExecOutcome, ExecutorSettings,
|
||||
KclValue, ModuleInfo, ModuleRepr, Operation, ProgramMemory, SolidLazyIds, UnitAngle, UnitLen,
|
||||
},
|
||||
parsing::ast::types::NonCodeValue,
|
||||
source_range::{ModuleId, SourceRange},
|
||||
};
|
||||
|
||||
/// State for executing a program.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecState {
|
||||
pub global: GlobalState,
|
||||
pub mod_local: ModuleState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GlobalState {
|
||||
/// The stable artifact ID generator.
|
||||
pub id_generator: IdGenerator,
|
||||
/// Map from source file absolute path to module ID.
|
||||
pub path_to_source_id: IndexMap<std::path::PathBuf, ModuleId>,
|
||||
/// Map from module ID to module info.
|
||||
pub module_infos: IndexMap<ModuleId, ModuleInfo>,
|
||||
/// Output map of UUIDs to artifacts.
|
||||
pub artifacts: IndexMap<ArtifactId, Artifact>,
|
||||
/// Output commands to allow building the artifact graph by the caller.
|
||||
/// These are accumulated in the [`ExecutorContext`] but moved here for
|
||||
/// convenience of the execution cache.
|
||||
pub artifact_commands: Vec<ArtifactCommand>,
|
||||
/// Responses from the engine for `artifact_commands`. We need to cache
|
||||
/// this so that we can build the artifact graph. These are accumulated in
|
||||
/// the [`ExecutorContext`] but moved here for convenience of the execution
|
||||
/// cache.
|
||||
pub artifact_responses: IndexMap<Uuid, WebSocketResponse>,
|
||||
/// Output artifact graph.
|
||||
pub artifact_graph: ArtifactGraph,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ModuleState {
|
||||
/// Program variable bindings.
|
||||
pub memory: ProgramMemory,
|
||||
/// Dynamic state that follows dynamic flow of the program.
|
||||
pub dynamic_state: DynamicState,
|
||||
/// The current value of the pipe operator returned from the previous
|
||||
/// expression. If we're not currently in a pipeline, this will be None.
|
||||
pub pipe_value: Option<KclValue>,
|
||||
/// Identifiers that have been exported from the current module.
|
||||
pub module_exports: Vec<String>,
|
||||
/// The stack of import statements for detecting circular module imports.
|
||||
/// If this is empty, we're not currently executing an import statement.
|
||||
pub import_stack: Vec<std::path::PathBuf>,
|
||||
/// Operations that have been performed in execution order, for display in
|
||||
/// the Feature Tree.
|
||||
pub operations: Vec<Operation>,
|
||||
/// Settings specified from annotations.
|
||||
pub settings: MetaSettings,
|
||||
}
|
||||
|
||||
impl ExecState {
|
||||
pub fn new(exec_settings: &ExecutorSettings) -> Self {
|
||||
ExecState {
|
||||
global: GlobalState::new(exec_settings),
|
||||
mod_local: ModuleState::new(exec_settings),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn reset(&mut self, exec_settings: &ExecutorSettings) {
|
||||
let mut id_generator = self.global.id_generator.clone();
|
||||
// We do not pop the ids, since we want to keep the same id generator.
|
||||
// This is for the front end to keep track of the ids.
|
||||
id_generator.next_id = 0;
|
||||
|
||||
let mut global = GlobalState::new(exec_settings);
|
||||
global.id_generator = id_generator;
|
||||
|
||||
*self = ExecState {
|
||||
global,
|
||||
mod_local: ModuleState::new(exec_settings),
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to execution outcome when running in WebAssembly. We want to
|
||||
/// reduce the amount of data that crosses the WASM boundary as much as
|
||||
/// possible.
|
||||
pub fn to_wasm_outcome(self) -> ExecOutcome {
|
||||
// Fields are opt-in so that we don't accidentally leak private internal
|
||||
// state when we add more to ExecState.
|
||||
ExecOutcome {
|
||||
memory: self.mod_local.memory,
|
||||
operations: self.mod_local.operations,
|
||||
artifacts: self.global.artifacts,
|
||||
artifact_commands: self.global.artifact_commands,
|
||||
artifact_graph: self.global.artifact_graph,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn memory(&self) -> &ProgramMemory {
|
||||
&self.mod_local.memory
|
||||
}
|
||||
|
||||
pub fn mut_memory(&mut self) -> &mut ProgramMemory {
|
||||
&mut self.mod_local.memory
|
||||
}
|
||||
|
||||
pub fn next_uuid(&mut self) -> Uuid {
|
||||
self.global.id_generator.next_uuid()
|
||||
}
|
||||
|
||||
pub fn add_artifact(&mut self, artifact: Artifact) {
|
||||
let id = artifact.id();
|
||||
self.global.artifacts.insert(id, artifact);
|
||||
}
|
||||
|
||||
pub(super) fn add_module(&mut self, id: ModuleId, path: std::path::PathBuf, repr: ModuleRepr) -> ModuleId {
|
||||
debug_assert!(!self.global.path_to_source_id.contains_key(&path));
|
||||
|
||||
self.global.path_to_source_id.insert(path.clone(), id);
|
||||
|
||||
let module_info = ModuleInfo { id, repr, path };
|
||||
self.global.module_infos.insert(id, module_info);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
pub fn length_unit(&self) -> UnitLen {
|
||||
self.mod_local.settings.default_length_units
|
||||
}
|
||||
|
||||
pub fn angle_unit(&self) -> UnitAngle {
|
||||
self.mod_local.settings.default_angle_units
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalState {
|
||||
fn new(settings: &ExecutorSettings) -> Self {
|
||||
let mut global = GlobalState {
|
||||
id_generator: Default::default(),
|
||||
path_to_source_id: Default::default(),
|
||||
module_infos: Default::default(),
|
||||
artifacts: Default::default(),
|
||||
artifact_commands: Default::default(),
|
||||
artifact_responses: Default::default(),
|
||||
artifact_graph: Default::default(),
|
||||
};
|
||||
|
||||
let root_id = ModuleId::default();
|
||||
let root_path = settings.current_file.clone().unwrap_or_default();
|
||||
global.module_infos.insert(
|
||||
root_id,
|
||||
ModuleInfo {
|
||||
id: root_id,
|
||||
path: root_path.clone(),
|
||||
repr: ModuleRepr::Root,
|
||||
},
|
||||
);
|
||||
global.path_to_source_id.insert(root_path, root_id);
|
||||
global
|
||||
}
|
||||
}
|
||||
|
||||
impl ModuleState {
|
||||
pub(super) fn new(exec_settings: &ExecutorSettings) -> Self {
|
||||
ModuleState {
|
||||
memory: Default::default(),
|
||||
dynamic_state: Default::default(),
|
||||
pipe_value: Default::default(),
|
||||
module_exports: Default::default(),
|
||||
import_stack: Default::default(),
|
||||
operations: Default::default(),
|
||||
settings: MetaSettings {
|
||||
default_length_units: exec_settings.units.into(),
|
||||
default_angle_units: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MetaSettings {
|
||||
pub default_length_units: kcl_value::UnitLen,
|
||||
pub default_angle_units: kcl_value::UnitAngle,
|
||||
}
|
||||
|
||||
impl MetaSettings {
|
||||
pub(crate) fn update_from_annotation(
|
||||
&mut self,
|
||||
annotation: &NonCodeValue,
|
||||
source_range: SourceRange,
|
||||
) -> Result<(), KclError> {
|
||||
let properties = annotations::expect_properties(annotations::SETTINGS, annotation, source_range)?;
|
||||
|
||||
for p in properties {
|
||||
match &*p.inner.key.name {
|
||||
annotations::SETTINGS_UNIT_LENGTH => {
|
||||
let value = annotations::expect_ident(&p.inner.value)?;
|
||||
let value = kcl_value::UnitLen::from_str(value, source_range)?;
|
||||
self.default_length_units = value;
|
||||
}
|
||||
annotations::SETTINGS_UNIT_ANGLE => {
|
||||
let value = annotations::expect_ident(&p.inner.value)?;
|
||||
let value = kcl_value::UnitAngle::from_str(value, source_range)?;
|
||||
self.default_angle_units = value;
|
||||
}
|
||||
name => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"Unexpected settings key: `{name}`; expected one of `{}`, `{}`",
|
||||
annotations::SETTINGS_UNIT_LENGTH,
|
||||
annotations::SETTINGS_UNIT_ANGLE
|
||||
),
|
||||
source_ranges: vec![source_range],
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Dynamic state that depends on the dynamic flow of the program, like the call
|
||||
/// stack. If the language had exceptions, for example, you could store the
|
||||
/// stack of exception handlers here.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct DynamicState {
|
||||
pub solid_ids: Vec<SolidLazyIds>,
|
||||
}
|
||||
|
||||
impl DynamicState {
|
||||
#[must_use]
|
||||
pub(super) fn merge(&self, memory: &ProgramMemory) -> Self {
|
||||
let mut merged = self.clone();
|
||||
merged.append(memory);
|
||||
merged
|
||||
}
|
||||
|
||||
fn append(&mut self, memory: &ProgramMemory) {
|
||||
for env in &memory.environments {
|
||||
for item in env.bindings.values() {
|
||||
if let KclValue::Solid { value } = item {
|
||||
self.solid_ids.push(SolidLazyIds::from(value.as_ref()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn edge_cut_ids_on_sketch(&self, sketch_id: uuid::Uuid) -> Vec<uuid::Uuid> {
|
||||
self.solid_ids
|
||||
.iter()
|
||||
.flat_map(|eg| {
|
||||
if eg.sketch_id == sketch_id {
|
||||
eg.edge_cuts.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
/// A generator for ArtifactIds that can be stable across executions.
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IdGenerator {
|
||||
pub(super) next_id: usize,
|
||||
ids: Vec<uuid::Uuid>,
|
||||
}
|
||||
|
||||
impl IdGenerator {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn next_uuid(&mut self) -> uuid::Uuid {
|
||||
if let Some(id) = self.ids.get(self.next_id) {
|
||||
self.next_id += 1;
|
||||
*id
|
||||
} else {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
self.ids.push(id);
|
||||
self.next_id += 1;
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
@ -183,6 +183,6 @@ impl FileSystem for FileManager {
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(files.into_iter().map(|s| std::path::PathBuf::from(s)).collect())
|
||||
Ok(files.into_iter().map(std::path::PathBuf::from).collect())
|
||||
}
|
||||
}
|
||||
|
@ -82,10 +82,7 @@ mod wasm;
|
||||
pub use coredump::CoreDump;
|
||||
pub use engine::{EngineManager, ExecutionKind};
|
||||
pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs};
|
||||
pub use execution::{
|
||||
cache::{CacheInformation, OldAstState},
|
||||
ExecState, ExecutorContext, ExecutorSettings, MetaSettings, Point2d,
|
||||
};
|
||||
pub use execution::{bust_cache, ExecOutcome, ExecState, ExecutorContext, ExecutorSettings, MetaSettings, Point2d};
|
||||
pub use lsp::{
|
||||
copilot::Backend as CopilotLspBackend,
|
||||
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
|
||||
|
@ -36,19 +36,19 @@ macro_rules! logln {
|
||||
}
|
||||
pub(crate) use logln;
|
||||
|
||||
#[cfg(not(feature = "disable-println"))]
|
||||
#[cfg(all(not(feature = "disable-println"), not(target_arch = "wasm32")))]
|
||||
#[inline]
|
||||
fn log_inner(msg: String) {
|
||||
eprintln!("{msg}");
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "disable-println", target_arch = "wasm32"))]
|
||||
#[cfg(all(not(feature = "disable-println"), target_arch = "wasm32"))]
|
||||
#[inline]
|
||||
fn log_inner(msg: String) {
|
||||
web_sys::console::log_1(&msg.into());
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "disable-println", not(target_arch = "wasm32")))]
|
||||
#[cfg(feature = "disable-println")]
|
||||
#[inline]
|
||||
fn log_inner(_msg: String) {}
|
||||
|
||||
|
@ -49,7 +49,7 @@ use crate::{
|
||||
token::TokenStream,
|
||||
PIPE_OPERATOR,
|
||||
},
|
||||
CacheInformation, ExecState, ModuleId, OldAstState, Program, SourceRange,
|
||||
ModuleId, Program, SourceRange,
|
||||
};
|
||||
const SEMANTIC_TOKEN_TYPES: [SemanticTokenType; 10] = [
|
||||
SemanticTokenType::NUMBER,
|
||||
@ -102,12 +102,6 @@ pub struct Backend {
|
||||
pub(super) token_map: DashMap<String, TokenStream>,
|
||||
/// AST maps.
|
||||
pub ast_map: DashMap<String, Node<crate::parsing::ast::types::Program>>,
|
||||
/// Last successful execution.
|
||||
/// This gets set to None when execution errors, or we want to bust the cache on purpose to
|
||||
/// force a re-execution.
|
||||
/// We do not need to manually bust the cache for changed units, that's handled by the cache
|
||||
/// information.
|
||||
pub last_successful_ast_state: Arc<RwLock<Option<OldAstState>>>,
|
||||
/// Memory maps.
|
||||
pub memory_map: DashMap<String, crate::execution::ProgramMemory>,
|
||||
/// Current code.
|
||||
@ -192,7 +186,6 @@ impl Backend {
|
||||
diagnostics_map: Default::default(),
|
||||
symbols_map: Default::default(),
|
||||
semantic_tokens_map: Default::default(),
|
||||
last_successful_ast_state: Default::default(),
|
||||
is_initialized: Default::default(),
|
||||
})
|
||||
}
|
||||
@ -267,10 +260,7 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
|
||||
async fn inner_on_change(&self, params: TextDocumentItem, force: bool) {
|
||||
if force {
|
||||
// Bust the execution cache.
|
||||
let mut old_ast_state = self.last_successful_ast_state.write().await;
|
||||
*old_ast_state = None;
|
||||
drop(old_ast_state);
|
||||
crate::bust_cache().await;
|
||||
}
|
||||
|
||||
let filename = params.uri.to_string();
|
||||
@ -688,52 +678,27 @@ impl Backend {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut last_successful_ast_state = self.last_successful_ast_state.write().await;
|
||||
match executor_ctx.run_with_caching(ast.clone()).await {
|
||||
Err(err) => {
|
||||
self.memory_map.remove(params.uri.as_str());
|
||||
self.add_to_diagnostics(params, &[err.error], false).await;
|
||||
|
||||
let mut exec_state = if let Some(last_successful_ast_state) = last_successful_ast_state.clone() {
|
||||
last_successful_ast_state.exec_state
|
||||
} else {
|
||||
ExecState::new(&executor_ctx.settings)
|
||||
};
|
||||
// Since we already published the diagnostics we don't really care about the error
|
||||
// string.
|
||||
Err(anyhow::anyhow!("failed to execute code"))
|
||||
}
|
||||
Ok(outcome) => {
|
||||
let memory = outcome.memory;
|
||||
self.memory_map.insert(params.uri.to_string(), memory.clone());
|
||||
|
||||
if let Err(err) = executor_ctx
|
||||
.run(
|
||||
CacheInformation {
|
||||
old: last_successful_ast_state.clone(),
|
||||
new_ast: ast.ast.clone(),
|
||||
},
|
||||
&mut exec_state,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.memory_map.remove(params.uri.as_str());
|
||||
self.add_to_diagnostics(params, &[err], false).await;
|
||||
// Send the notification to the client that the memory was updated.
|
||||
self.client
|
||||
.send_notification::<custom_notifications::MemoryUpdated>(memory)
|
||||
.await;
|
||||
|
||||
// Update the last successful ast state to be None.
|
||||
*last_successful_ast_state = None;
|
||||
|
||||
// Since we already published the diagnostics we don't really care about the error
|
||||
// string.
|
||||
return Err(anyhow::anyhow!("failed to execute code"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Update the last successful ast state.
|
||||
*last_successful_ast_state = Some(OldAstState {
|
||||
ast: ast.ast.clone(),
|
||||
exec_state: exec_state.clone(),
|
||||
settings: executor_ctx.settings.clone(),
|
||||
});
|
||||
drop(last_successful_ast_state);
|
||||
|
||||
self.memory_map
|
||||
.insert(params.uri.to_string(), exec_state.memory().clone());
|
||||
|
||||
// Send the notification to the client that the memory was updated.
|
||||
self.client
|
||||
.send_notification::<custom_notifications::MemoryUpdated>(exec_state.mod_local.memory)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_semantic_token_type_index(&self, token_type: &SemanticTokenType) -> Option<u32> {
|
||||
|
@ -9,7 +9,7 @@ pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
|
||||
let stdlib_completions = crate::lsp::kcl::get_completions_from_stdlib(&stdlib)?;
|
||||
let stdlib_signatures = crate::lsp::kcl::get_signatures_from_stdlib(&stdlib)?;
|
||||
|
||||
let zoo_client = crate::execution::new_zoo_client(None, None)?;
|
||||
let zoo_client = crate::engine::new_zoo_client(None, None)?;
|
||||
|
||||
let executor_ctx = if execute {
|
||||
Some(crate::execution::ExecutorContext::new(&zoo_client, Default::default()).await?)
|
||||
@ -37,7 +37,6 @@ pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
|
||||
can_send_telemetry: true,
|
||||
executor_ctx: Arc::new(tokio::sync::RwLock::new(executor_ctx)),
|
||||
can_execute: Arc::new(tokio::sync::RwLock::new(can_execute)),
|
||||
last_successful_ast_state: Default::default(),
|
||||
is_initialized: Default::default(),
|
||||
})
|
||||
.custom_method("kcl/updateUnits", crate::lsp::kcl::Backend::update_units)
|
||||
|
@ -7,7 +7,7 @@ use winnow::{
|
||||
prelude::*,
|
||||
stream::{Location, Stream},
|
||||
token::{any, none_of, one_of, take_till, take_until},
|
||||
Located, Stateful,
|
||||
LocatingSlice, Stateful,
|
||||
};
|
||||
|
||||
use super::TokenStream;
|
||||
@ -65,13 +65,13 @@ lazy_static! {
|
||||
pub(super) fn lex(i: &str, module_id: ModuleId) -> Result<TokenStream, ParseError<Input<'_>, ContextError>> {
|
||||
let state = State::new(module_id);
|
||||
let input = Input {
|
||||
input: Located::new(i),
|
||||
input: LocatingSlice::new(i),
|
||||
state,
|
||||
};
|
||||
Ok(TokenStream::new(repeat(0.., token).parse(input)?))
|
||||
}
|
||||
|
||||
pub(super) type Input<'a> = Stateful<Located<&'a str>, State>;
|
||||
pub(super) type Input<'a> = Stateful<LocatingSlice<&'a str>, State>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct State {
|
||||
@ -361,7 +361,7 @@ fn keyword_type_or_word(i: &mut Input<'_>) -> PResult<Token> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use winnow::Located;
|
||||
use winnow::LocatingSlice;
|
||||
|
||||
use super::*;
|
||||
use crate::parsing::token::TokenSlice;
|
||||
@ -373,7 +373,7 @@ mod tests {
|
||||
{
|
||||
let state = State::new(ModuleId::default());
|
||||
let mut input = Input {
|
||||
input: Located::new(s),
|
||||
input: LocatingSlice::new(s),
|
||||
state,
|
||||
};
|
||||
assert!(p.parse_next(&mut input).is_err(), "parsed {s} but should have failed");
|
||||
@ -388,7 +388,7 @@ mod tests {
|
||||
{
|
||||
let state = State::new(ModuleId::default());
|
||||
let mut input = Input {
|
||||
input: Located::new(s),
|
||||
input: LocatingSlice::new(s),
|
||||
state,
|
||||
};
|
||||
let res = p.parse_next(&mut input);
|
||||
@ -422,7 +422,7 @@ mod tests {
|
||||
|
||||
let module_id = ModuleId::from_usize(1);
|
||||
let input = Input {
|
||||
input: Located::new("0.0000000000"),
|
||||
input: LocatingSlice::new("0.0000000000"),
|
||||
state: State::new(module_id),
|
||||
};
|
||||
|
||||
|
@ -3,8 +3,9 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
engine::new_zoo_client,
|
||||
errors::ExecErrorWithState,
|
||||
execution::{new_zoo_client, ExecutorContext, ExecutorSettings},
|
||||
execution::{ExecutorContext, ExecutorSettings},
|
||||
settings::types::UnitLength,
|
||||
ConnectionError, ExecError, ExecState, KclErrorWithOutputs, Program,
|
||||
};
|
||||
@ -66,8 +67,11 @@ async fn do_execute_and_snapshot(
|
||||
program: Program,
|
||||
) -> Result<(ExecState, image::DynamicImage), ExecErrorWithState> {
|
||||
let mut exec_state = ExecState::new(&ctx.settings);
|
||||
ctx.run_with_ui_outputs(&program, &mut exec_state)
|
||||
.await
|
||||
.map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone()))?;
|
||||
let snapshot_png_bytes = ctx
|
||||
.execute_and_prepare_snapshot(&program, &mut exec_state)
|
||||
.prepare_snapshot()
|
||||
.await
|
||||
.map_err(|err| ExecErrorWithState::new(err, exec_state.clone()))?
|
||||
.contents
|
||||
|
@ -1,4 +1,4 @@
|
||||
///! Web assembly utils.
|
||||
//! Web assembly utils.
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
|
@ -1,12 +1,11 @@
|
||||
---
|
||||
source: kcl/src/simulation_tests.rs
|
||||
description: Error from executing kw_fn_too_few_args.kcl
|
||||
snapshot_kind: text
|
||||
---
|
||||
KCL Semantic error
|
||||
|
||||
× semantic: This function requires a parameter y, but you haven't passed
|
||||
│ it one.
|
||||
× semantic: This function requires a parameter y, but you haven't passed it
|
||||
│ one.
|
||||
╭─[1:7]
|
||||
1 │ ╭─▶ fn add(x, y) {
|
||||
2 │ │ return x + y
|
||||
|
@ -5,33 +5,11 @@ use std::sync::Arc;
|
||||
use futures::stream::TryStreamExt;
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
use kcl_lib::{
|
||||
exec::IdGenerator, pretty::NumericSuffix, CacheInformation, CoreDump, EngineManager, ExecState, ModuleId,
|
||||
OldAstState, Point2d, Program,
|
||||
bust_cache, exec::IdGenerator, pretty::NumericSuffix, CoreDump, EngineManager, ModuleId, Point2d, Program,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tower_lsp::{LspService, Server};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// A static mutable lock for updating the last successful execution state for the cache.
|
||||
static ref OLD_AST_MEMORY: Arc<RwLock<Option<OldAstState>>> = Default::default();
|
||||
}
|
||||
|
||||
// Read the old ast memory from the lock, this should never fail since
|
||||
// in failure scenarios we should just bust the cache and send back None as the previous
|
||||
// state.
|
||||
async fn read_old_ast_memory() -> Option<OldAstState> {
|
||||
let lock = OLD_AST_MEMORY.read().await;
|
||||
lock.clone()
|
||||
}
|
||||
|
||||
async fn bust_cache() {
|
||||
// We don't use the cache in mock mode.
|
||||
let mut current_cache = OLD_AST_MEMORY.write().await;
|
||||
// Set the cache to None.
|
||||
*current_cache = None;
|
||||
}
|
||||
|
||||
// wasm_bindgen wrapper for clearing the scene and busting the cache.
|
||||
#[wasm_bindgen]
|
||||
pub async fn clear_scene_and_bust_cache(
|
||||
@ -39,7 +17,6 @@ pub async fn clear_scene_and_bust_cache(
|
||||
) -> Result<(), String> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
// Bust the cache.
|
||||
bust_cache().await;
|
||||
|
||||
let engine = kcl_lib::wasm_engine::EngineConnection::new(engine_manager)
|
||||
@ -70,74 +47,31 @@ pub async fn execute(
|
||||
let program: Program = serde_json::from_str(program_ast_json).map_err(|e| e.to_string())?;
|
||||
let program_memory_override: Option<kcl_lib::exec::ProgramMemory> =
|
||||
serde_json::from_str(program_memory_override_str).map_err(|e| e.to_string())?;
|
||||
|
||||
// If we have a program memory override, assume we are in mock mode.
|
||||
// You cannot override the memory in non-mock mode.
|
||||
let is_mock = program_memory_override.is_some();
|
||||
|
||||
let config: kcl_lib::Configuration = serde_json::from_str(settings).map_err(|e| e.to_string())?;
|
||||
let mut settings: kcl_lib::ExecutorSettings = config.into();
|
||||
if let Some(path) = path {
|
||||
settings.with_current_file(std::path::PathBuf::from(path));
|
||||
}
|
||||
|
||||
let ctx = if is_mock {
|
||||
kcl_lib::ExecutorContext::new_mock(fs_manager, settings).await?
|
||||
// If we have a program memory override, assume we are in mock mode.
|
||||
// You cannot override the memory in non-mock mode.
|
||||
if program_memory_override.is_some() {
|
||||
let ctx = kcl_lib::ExecutorContext::new_mock(fs_manager, settings.into()).await?;
|
||||
match ctx.run_mock(program, program_memory_override).await {
|
||||
// The serde-wasm-bindgen does not work here because of weird HashMap issues.
|
||||
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend.
|
||||
Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()),
|
||||
Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?),
|
||||
}
|
||||
} else {
|
||||
kcl_lib::ExecutorContext::new(engine_manager, fs_manager, settings).await?
|
||||
};
|
||||
|
||||
let mut exec_state = ExecState::new(&ctx.settings);
|
||||
let mut old_ast_memory = None;
|
||||
|
||||
// Populate from the old exec state if it exists.
|
||||
if let Some(program_memory_override) = program_memory_override {
|
||||
// We are in mock mode, so don't use any cache.
|
||||
exec_state.mod_local.memory = program_memory_override;
|
||||
} else {
|
||||
// If we are in mock mode, we don't want to use any cache.
|
||||
if let Some(old) = read_old_ast_memory().await {
|
||||
exec_state = old.exec_state.clone();
|
||||
old_ast_memory = Some(old);
|
||||
let ctx = kcl_lib::ExecutorContext::new(engine_manager, fs_manager, settings.into()).await?;
|
||||
match ctx.run_with_caching(program).await {
|
||||
// The serde-wasm-bindgen does not work here because of weird HashMap issues.
|
||||
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend.
|
||||
Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()),
|
||||
Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?),
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = ctx
|
||||
.run_with_ui_outputs(
|
||||
CacheInformation {
|
||||
old: old_ast_memory,
|
||||
new_ast: program.ast.clone(),
|
||||
},
|
||||
&mut exec_state,
|
||||
)
|
||||
.await
|
||||
{
|
||||
if !is_mock {
|
||||
bust_cache().await;
|
||||
}
|
||||
|
||||
// Throw the error.
|
||||
return Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?);
|
||||
}
|
||||
|
||||
if !is_mock {
|
||||
// We don't use the cache in mock mode.
|
||||
let mut current_cache = OLD_AST_MEMORY.write().await;
|
||||
|
||||
// If we aren't in mock mode, save this as the last successful execution to the cache.
|
||||
*current_cache = Some(OldAstState {
|
||||
ast: program.ast.clone(),
|
||||
exec_state: exec_state.clone(),
|
||||
settings: ctx.settings.clone(),
|
||||
});
|
||||
drop(current_cache);
|
||||
}
|
||||
|
||||
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
|
||||
// gloo-serialize crate instead.
|
||||
// DO NOT USE serde_wasm_bindgen::to_value(&exec_state).map_err(|e| e.to_string())
|
||||
// it will break the frontend.
|
||||
JsValue::from_serde(&exec_state.to_wasm_outcome()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// wasm_bindgen wrapper for execute
|
||||
@ -160,7 +94,6 @@ pub async fn make_default_planes(
|
||||
engine_manager: kcl_lib::wasm_engine::EngineCommandManager,
|
||||
) -> Result<JsValue, String> {
|
||||
console_error_panic_hook::set_once();
|
||||
// deserialize the ast from a stringified json
|
||||
|
||||
let engine = kcl_lib::wasm_engine::EngineConnection::new(engine_manager)
|
||||
.await
|
||||
@ -170,12 +103,9 @@ pub async fn make_default_planes(
|
||||
.await
|
||||
.map_err(String::from)?;
|
||||
|
||||
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
|
||||
// gloo-serialize crate instead.
|
||||
JsValue::from_serde(&default_planes).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// wasm_bindgen wrapper for execute
|
||||
#[wasm_bindgen]
|
||||
pub async fn modify_ast_for_sketch_wasm(
|
||||
manager: kcl_lib::wasm_engine::EngineCommandManager,
|
||||
@ -208,8 +138,6 @@ pub async fn modify_ast_for_sketch_wasm(
|
||||
.await
|
||||
.map_err(String::from)?;
|
||||
|
||||
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
|
||||
// gloo-serialize crate instead.
|
||||
JsValue::from_serde(&program).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@ -384,11 +312,7 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, baseurl: Strin
|
||||
let mut zoo_client = kittycad::Client::new(token);
|
||||
zoo_client.set_base_url(baseurl.as_str());
|
||||
|
||||
let dev_mode = if baseurl == "https://api.dev.zoo.dev" {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let dev_mode = baseurl == "https://api.dev.zoo.dev";
|
||||
|
||||
let (service, socket) =
|
||||
LspService::build(|client| kcl_lib::CopilotLspBackend::new_wasm(client, fs, zoo_client, dev_mode))
|
||||
@ -523,7 +447,7 @@ pub fn default_app_settings() -> Result<JsValue, String> {
|
||||
pub fn parse_app_settings(toml_str: &str) -> Result<JsValue, String> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let settings = kcl_lib::Configuration::backwards_compatible_toml_parse(&toml_str).map_err(|e| e.to_string())?;
|
||||
let settings = kcl_lib::Configuration::backwards_compatible_toml_parse(toml_str).map_err(|e| e.to_string())?;
|
||||
|
||||
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
|
||||
// gloo-serialize crate instead.
|
||||
@ -548,7 +472,7 @@ pub fn parse_project_settings(toml_str: &str) -> Result<JsValue, String> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let settings =
|
||||
kcl_lib::ProjectConfiguration::backwards_compatible_toml_parse(&toml_str).map_err(|e| e.to_string())?;
|
||||
kcl_lib::ProjectConfiguration::backwards_compatible_toml_parse(toml_str).map_err(|e| e.to_string())?;
|
||||
|
||||
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
|
||||
// gloo-serialize crate instead.
|
||||
|
@ -1,7 +1,7 @@
|
||||
//! Cache testing framework.
|
||||
|
||||
use anyhow::Result;
|
||||
use kcl_lib::{ExecError, ExecState};
|
||||
use kcl_lib::{bust_cache, ExecError, ExecOutcome};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Variation<'a> {
|
||||
@ -12,15 +12,14 @@ struct Variation<'a> {
|
||||
async fn cache_test(
|
||||
test_name: &str,
|
||||
variations: Vec<Variation<'_>>,
|
||||
) -> Result<Vec<(String, image::DynamicImage, ExecState)>> {
|
||||
) -> Result<Vec<(String, image::DynamicImage, ExecOutcome)>> {
|
||||
let first = variations
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("No variations provided for test '{}'", test_name))?;
|
||||
|
||||
let mut ctx = kcl_lib::ExecutorContext::new_with_client(first.settings.clone(), None, None).await?;
|
||||
let mut exec_state = kcl_lib::ExecState::new(&ctx.settings);
|
||||
|
||||
let mut old_ast_state = None;
|
||||
bust_cache().await;
|
||||
let mut img_results = Vec::new();
|
||||
for (index, variation) in variations.iter().enumerate() {
|
||||
let program = kcl_lib::Program::parse_no_errs(variation.code)?;
|
||||
@ -28,14 +27,7 @@ async fn cache_test(
|
||||
// set the new settings.
|
||||
ctx.settings = variation.settings.clone();
|
||||
|
||||
ctx.run(
|
||||
kcl_lib::CacheInformation {
|
||||
old: old_ast_state,
|
||||
new_ast: program.ast.clone(),
|
||||
},
|
||||
&mut exec_state,
|
||||
)
|
||||
.await?;
|
||||
let outcome = ctx.run_with_caching(program).await?;
|
||||
let snapshot_png_bytes = ctx.prepare_snapshot().await?.contents.0;
|
||||
|
||||
// Decode the snapshot, return it.
|
||||
@ -46,14 +38,7 @@ async fn cache_test(
|
||||
// Save the snapshot.
|
||||
let path = crate::assert_out(&format!("cache_{}_{}", test_name, index), &img);
|
||||
|
||||
img_results.push((path, img, exec_state.clone()));
|
||||
|
||||
// Prepare the last state.
|
||||
old_ast_state = Some(kcl_lib::OldAstState {
|
||||
ast: program.ast,
|
||||
exec_state: exec_state.clone(),
|
||||
settings: variation.settings.clone(),
|
||||
});
|
||||
img_results.push((path, img, outcome));
|
||||
}
|
||||
|
||||
ctx.close().await;
|
||||
@ -254,19 +239,19 @@ extrude(sketch001, length = 4)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let first = result.first().unwrap();
|
||||
let second = result.last().unwrap();
|
||||
let first = &result.first().unwrap().2;
|
||||
let second = &result.last().unwrap().2;
|
||||
|
||||
assert!(
|
||||
first.2.global.artifact_commands.len() < second.2.global.artifact_commands.len(),
|
||||
first.artifact_commands.len() < second.artifact_commands.len(),
|
||||
"Second should have all the artifact commands of the first, plus more. first={:?}, second={:?}",
|
||||
first.2.global.artifact_commands.len(),
|
||||
second.2.global.artifact_commands.len()
|
||||
first.artifact_commands.len(),
|
||||
second.artifact_commands.len()
|
||||
);
|
||||
assert!(
|
||||
first.2.global.artifact_responses.len() < second.2.global.artifact_responses.len(),
|
||||
"Second should have all the artifact responses of the first, plus more. first={:?}, second={:?}",
|
||||
first.2.global.artifact_responses.len(),
|
||||
second.2.global.artifact_responses.len()
|
||||
first.artifact_graph.len() < second.artifact_graph.len(),
|
||||
"Second should have all the artifacts of the first, plus more. first={:?}, second={:?}",
|
||||
first.artifact_graph.len(),
|
||||
second.artifact_graph.len()
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, Modu
|
||||
let program = Program::parse_no_errs(code)?;
|
||||
let ctx = kcl_lib::ExecutorContext::new_with_default_client(Default::default()).await?;
|
||||
let mut exec_state = ExecState::new(&ctx.settings);
|
||||
ctx.run(program.clone().into(), &mut exec_state).await?;
|
||||
ctx.run(&program, &mut exec_state).await?;
|
||||
|
||||
// We need to get the sketch ID.
|
||||
// Get the sketch ID from memory.
|
||||
|