Compare commits

...

17 Commits

Author SHA1 Message Date
7c935741e4 Update snapshots from the upload artifact run 2025-02-05 16:36:14 -05:00
87e299e0bb Merge branch 'main' into pierremtb/make-snapshot-bot-upload-instead-of-commit 2025-02-05 16:02:18 -05:00
465e71c12f WIP 2025-02-05 15:54:47 -05:00
df86c93a04 Restrict snapshot bot to non-main branches and to the snapshot dir (#5266)
* Disable snapshot commit bot on main

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Restrict snapshot bot git add dir

* Clean up sanps

* Other git add .

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Clean up after bot (bad bot)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Remove -a from -am

* Clean up

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 15:44:36 -05:00
824669a1c2 WIP 2025-02-05 15:41:08 -05:00
ba8f8a1722 WIP 2025-02-05 15:30:41 -05:00
f4a4e6c5be Upload only the changes 2025-02-05 15:18:51 -05:00
0d148e80aa Clean up 2025-02-05 15:01:03 -05:00
3300993ac8 Separate snapshot from flow tests 2025-02-05 14:53:54 -05:00
033eaed32e Make snapshot bot upload the changes instead of commit 2025-02-05 14:33:05 -05:00
8aabac0be7 Update types.md with keyword args data (#5270)
A few issues:

- There was no description of how `|>` works
- Need to explain our keyword arguments implementation
- It was using old syntax for `angledLine` which now takes an object as its first parameter, not an array
2025-02-05 13:03:28 -06:00
138728a95d Move Helix button to a section with offset plane (3d 'construction' elements) (#5235)
* Move Helix button to a section with offset plane (3d 'construction' elements)
Fixes #5234

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 09:59:58 -05:00
9a92e7d642 Faster local playwright electron test scripts (#5242) 2025-02-05 09:51:23 -05:00
efedc8de58 Point snapshot bot tokens to the right ones (#5265)
Add prefix to secrets for create-github-app-token
2025-02-05 09:46:46 -05:00
f7ee248a26 Fix to use more accurate types with custom isArray() and add lint (#5261)
* Fix to use more accurate types with custom isArray()

* Add lint against Array.isArray()
2025-02-05 09:01:45 -05:00
336f4f27ba Release new kcl-lib and derive-docs (#5259)
* Release new kcl-lib and derive-docs

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 08:53:50 -05:00
e1f128d64a Refactor execution module (#5162)
* cargo update, etc

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor execution/mod.rs (code motion)

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor caching out of ExecutorContext plus some tidying up

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Move caching logic to inside execution

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-05 17:53:49 +13:00
74 changed files with 3892 additions and 3739 deletions

View File

@ -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": [

View File

@ -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
View 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

View File

@ -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])

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -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"
},

View 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)
}

View File

@ -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)

View File

@ -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

View File

@ -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}
/>

View File

@ -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') {

View File

@ -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),

View File

@ -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]'
)

View File

@ -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)
}

View File

@ -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',

View File

@ -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)

View File

@ -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)
)
},

View File

@ -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 {

View File

@ -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',
[
{

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"]

View File

@ -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"

View File

@ -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),

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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(());

View File

@ -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)

View File

@ -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"

View File

@ -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())),

View File

@ -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)
}

View File

@ -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>,

View File

@ -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));
}
}

View File

@ -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(&param.identifier.name, arg.value.clone(), (&param.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(
&param.identifier.name,
default_val.clone().into(),
(&param.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(&param.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(&param.identifier.name, arg_val, (&param.identifier).into())?;
} else {
let Some(unlabeled) = args.unlabeled.take() else {
let param_name = &param.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(
&param.identifier.name,
unlabeled.value.clone(),
(&param.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:?}"
);
}
}
}

View File

@ -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::<()>()
}
}

File diff suppressed because it is too large Load Diff

View 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(),

View 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 };
}
}
}

File diff suppressed because it is too large Load Diff

View 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
}
}
}

View File

@ -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())
}
}

View File

@ -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},

View File

@ -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) {}

View File

@ -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> {

View File

@ -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)

View File

@ -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),
};

View File

@ -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

View File

@ -1,4 +1,4 @@
///! Web assembly utils.
//! Web assembly utils.
use std::{
pin::Pin,
task::{Context, Poll},

View File

@ -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

View File

@ -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.

View File

@ -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()
);
}

View File

@ -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.