Compare commits

...

42 Commits

Author SHA1 Message Date
3c23cada8e Release KCL 80 (#7391) 2025-06-05 20:34:58 +00:00
81782b04ec Rename and reorder contributor guide sections (#7387) 2025-06-05 15:06:15 -04:00
9ade6676b7 Fix docs for loft (#7389) 2025-06-05 19:05:46 +00:00
86e5c37678 Make web end-to-end tests exclusive with desktop (#7388)
* Make web end-to-end tests exclusive with desktop

* Use double quotes to support Windows
2025-06-05 13:42:36 -04:00
8c36d742e5 Add mirror2d operations to Feature Tree (#7308)
* Add mirror2d operations

* Update output
2025-06-05 12:52:53 -04:00
f6a3a3d0cd Change to use nodePath instead of sourceRange for Operations (#7320)
* Add NodePath to operations

* Change to use nodePath to get pathToNode instead of sourceRange

* Add additional node path unit test

* Update output

* Fix import statement NodePaths

* Update output

* Factor into function
2025-06-05 12:24:34 -04:00
b5f81cb84a Run fmt (#7383) 2025-06-05 11:46:48 -04:00
4bb17c192f Stabilize multiprofile sketch test further (#7373)
* increase timeout after rectangle tool is clicked from 200ms -> 400ms

* Delay after each primitive mouse interaction

* Update snapshots

* Update snapshots

---------

Co-authored-by: jacebrowning <jacebrowning@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-05 11:28:40 -04:00
51ce55e782 Remove nested <button> elements (#7291)
* remove nested <button> elements to avoid dom warning about nested buttons

* keep using Popover.Button to allow popover functionality

* fmt

* match ShareButton margin to main branch to make snapshot tests happy
2025-06-05 17:24:04 +02:00
427d55d13e KCL: Optimization, ty Jon (#7379)
This avoids a clone
2025-06-05 08:34:16 -05:00
4575b32dbc KCL parser: Allow .prop or [index] to follow any expression (#7371)
Previously in a member expression like `foo.x` or `foo[3]`, `foo` had to be an identifier. You could not do something like `f().x` (and if you tried, you got a cryptic error). Rather than make the error better, we should just accept any expression to be the LHS of a member expression (aka its 'object').

This does knock our "parse lots of function calls" from 58 to 55 calls before it stack overflows. But I think it's fine, we'll address this in https://github.com/KittyCAD/modeling-app/pull/6226 when I get back to it.

Closes https://github.com/KittyCAD/modeling-app/issues/7273
2025-06-05 09:23:48 -04:00
9136fb0d1b KCL: Improve error messages for var referenced in own definition (#7374)
Jon pointed out that my new error message wasn't showing up in some
cases, and it should store/restore the previous var being defined.
2025-06-04 23:48:15 -05:00
33d5a9cdc1 Execution refactoring (#7376)
* Move import graph to execution

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

* Refactor artifact handling

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

* Refactor caching to separate global state from per-module state

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-06-05 15:56:43 +12:00
c25dfabc94 Stronger types and better error handling in executeMock (#7370)
This brings the `execute_mock` function into line with the `execute` function, which I tweaked in https://github.com/KittyCAD/modeling-app/pull/7351. Now mock execution, like real execution, will always return a properly-formatted KCL error, instead of any possible JS value.

Also, incidentally, I noticed that send_response always succeeds, so I changed it from Result<()> to void.
2025-06-04 18:24:24 -04:00
4502ad62b2 Fix vite warning (#7360)
Specifically this warning:
```
[vite] warning: This case clause will never be evaluated because it duplicates an earlier case clause
|      case 'angledLine':
|      case 'startProfile':
|      case 'arcTo':
|           ^
|        return fnName
|      default:
```
2025-06-04 17:08:06 -04:00
5235a731ba Move sketch functions to KCL; remove Rust decl dead code (#7335)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-06-04 15:41:01 -04:00
5ceb92d117 Try to avoid the black screen again & improve error messages (#7327)
* Fix the black screen of death

* fmt

* make check

* Clean up

* Fix up zoom to fit

* Change how emulateNetworkConditions work

* Do NOT use browser's offline/online mechanisms

* Fix test

* Improve network error messages

* Signal offline when failed event comes in

* Don't use logic on components that only want a loader

* Remove unnecessary pause state transition

---------

Co-authored-by: jacebrowning <jacebrowning@gmail.com>
2025-06-04 13:59:22 -04:00
ff92c73ac4 Update dependabot config (#7362) 2025-06-04 10:08:54 -04:00
bbb6fffbcc Allow edit for Translate and Rotate in Feature Tree (#7345)
* WIP: Wire up edit on transforms to the new feature tree standalone ops
Fixes #7338

* Clean up diff

* Oopsie
2025-06-04 09:35:01 -04:00
37fca0d1df #7326 Fix "Zoom to fit to shared model on web" test (#7328)
* fix test

* fix e2e test in another way so it doesnt break unit tests

* Cleanup

* Update src/hooks/useQueryParamEffects.ts

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* hasAskToOpen should only be used if not in desktop

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-06-04 09:37:25 +02:00
e3694e4781 Fix crash while editing code while in sketch mode (#7284)
* Fix crash by checking bounds

* Add unit test
2025-06-04 12:10:09 +10:00
f97bdaf8b7 Release KCL 79 (#7347) 2025-06-03 22:04:56 -04:00
3f3693e12d Type ascription produces two incompatible fields (#7355)
# Symptoms

This code produces a big ugly confusing error in the frontend, see #7340.

# Root cause

I added a new test case, with an unknown type. In `ast.snap` under `body[0].declaration.init.ty` there two different `type` fields in the AST node for the type's name, and they have conflicting values Primitive and Identifier.

<img width="602" alt="Screenshot 2025-06-03 at 4 04 55 PM" src="https://github.com/user-attachments/assets/913a0fa0-3e8d-473f-bb64-003d44915be0" />

# Solution

Change the `enum PrimitiveType` variant from `Named(Node<Identifier>)` to `Named { name: Node<Identifier> }` so that the fields nest differently.

Now the error correctly points out to the user that the type `NotARealType` can't be found. Much better error message that shows the user the problem.

# Alternative solutions

Stop the duplicated JSON fields altogether. I tried this previously in https://github.com/KittyCAD/modeling-app/pull/4369 but it was very involved, and I didn't think it was worth it. Maybe I should reopen that PR and solve this properly.

Closes #7340
2025-06-03 20:05:40 -04:00
73660d1db8 Bump criterion from 0.5.1 to 0.6.0 (#7357) 2025-06-04 09:34:34 +10:00
c373f33507 KCL: Fix format 2025-06-04 09:04:35 +10:00
2dc76a71cc Stabilize test: "Can edit a sketch with multiple profiles, dragging segments to edit them, and adding one new profile" (#7356) 2025-06-03 18:20:06 -04:00
ce42966f2b Upgrade to Rust 1.87 (#7346)
I ignored some new clippy lints about large differences between enum variants.
We can always revisit these later (the compiler suggests boxing them so
that the enum variants are similar size)
2025-06-03 17:32:24 -04:00
b47b9c9613 KCL: Emit proper errors in unexpected edge cases (#7351)
There's some bug in the frontend or KCL somewhere, which results in the TypeScript frontend sending an AST (serialized to JSON) to the KCL executor, but the JSON cannot be deserialized into an AST. If this happens, it's a bug in ZDS, not a user error. 

The problem is that this sort of error will cause the frontend to silently stop rendering KCL, and it won't show the user any errors. They need to open up the console and look at the error there, and even if they do, it's hard to understand.

This PR changes how we report these unexpected errors due to bugs in ZDS. ZDS should not silently stop working, it should at least print a half-decent error like this:

<img width="527" alt="nicer error" src="https://github.com/user-attachments/assets/1bb37a64-0915-4472-849c-d146f397356b" />

## Fix

Right now, the wasm library exports a function `execute`. It previous returned an error as a String if one occurred. The frontend assumed this error string would be JSON that matched the schema `KclErrorWithOutputs`. This was not always true! For example, if something couldn't be serialized to JSON, we'd take the raw Serde error and stringify that. It wouldn't match `KclErrorWithOutputs`.

Now I've changed `execute` so that if it errors, it'll returns a JsValue not a string. So that's one check (can this string be deserialized into a JSON object) that can be removed -- it'll return a JSON object directly now. The next check is "does this JSON object conform to the KclErrorWithOutputs schema". To prove that's correct, I changed `execute` to be a thin wrapper around `fn execute_typed` which returns `Result<ExecOutcome, KclErrorWithOutputs>`. Now we know the error will be the right type.
2025-06-03 15:37:17 -05:00
2af2144f89 Add query parameter to skip the embedded "sign in" view (#7352)
Add query parameter to skip sign-in view if not necessary
2025-06-03 16:23:23 -04:00
bd37c488ee Fix trackball, finally add an E2E test for it (#7341)
* Fix orbit style setting not updating in camControls

* Break apart camera movement tests, add trackball to orbit one

* I don't think zoom was actually testing changes, this fixes that

* test refactor: pass in expected cam pos, not its inverse

* Lints

* Lint fix broke the test, fix fix

* Gah biome whyyy did you format other test names like that?
2025-06-03 14:56:19 -04:00
a3551e4b2f Bump csscolorparser from 0.7.0 to 0.7.2 (#7344) 2025-06-03 11:50:09 -05:00
d3979edb41 KCL: Better error message when using var in its own definition (#7339)
I thought I did this in https://github.com/KittyCAD/modeling-app/pull/7325, but I forgot to actually set the better message.

Actually fixes, for real this time, https://github.com/KittyCAD/modeling-app/issues/6072 this time.
2025-06-03 16:46:28 +00:00
095a7a575b Bump tokio-tungstenite from 0.24.0 to 0.26.2 (#7329) 2025-06-03 10:16:22 -05:00
b5c8ca05a5 Fix whitespace in updater toast (#7331)
pierremtb/adhoc/whitespace-fix-updater-toast
2025-06-03 10:15:42 -04:00
33f7badf41 point and click-ify mounting plate (#7287)
* point and click-ify mounting plate

* Update kcl-samples simulation test output

* Update public/kcl-samples/mounting-plate/main.kcl

* Update public/kcl-samples/mounting-plate/main.kcl

* fix

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-03 08:10:37 -04:00
569935c21f Move segment functions to KCL (#7333)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-06-03 15:15:51 +12:00
7680605085 KCL: Fix 'cryptic' error when referencing a variable in its own declaration (#7325)
Previously, `x = cos(x)` would just say "`x` is undefined". Now it says that `x` cannot be referenced in its own definition, try using a different variable instead.

To do this, I've added a new `Option<String>` field to the mod-local executor context, tracking the current variable declaration. This means cloning some strings, implying a small performance hit. I think it's fine, for the better diagnostics.

In the future we could refactor this to use a &str or store variable labels in stack-allocated strings like docs.rs/compact_str or something.

Closes https://github.com/KittyCAD/modeling-app/issues/6072
2025-06-02 18:25:55 -04:00
2bb6c74f42 Update error message after engine change (#7330) 2025-06-02 18:05:46 -04:00
faf4d42b6a Update getSketchExprsFromSelection to use codeRef.pathToNode (#6737) 2025-06-02 16:49:41 -04:00
8dd2a86191 Make sweep tests more point-and-click like and clean up pathToNode retrieval (#6963)
WIP: udpate sweep point-and-click tests and mess with pathToNode
Fixes #6952
2025-06-02 16:49:20 -04:00
13c4de77c3 Fix broken WASM test (#7324)
Previous PR (#7321) was set to automerge, but apparently the npm-unit-test
CI target doesn't block merges if it fails. Fixed now.
2025-06-02 15:56:49 -04:00
e29ee9d1ca KCL: Use named fields for KclError (#7321)
We've changed the unnamed field of `KclError` variants to a named called `details`.

To clarify: previously KCL errors looked like this:

```rust
pub enum KclError {
    Lexical(KclErrorDetails),
    Syntax(KclErrorDetails),
```

Now they look like this:

```rust
pub enum KclError {
    Lexical { details: KclErrorDetails },
    Syntax { details: KclErrorDetails },
}
```

This lets us more easily add fields to the errors. For example, in the UndefinedValue case, adding a field for what the undefined name was. This PR refactors the code to make my PR in https://github.com/KittyCAD/modeling-app/pull/7309 much easier.

Pure refactor, should not change any behaviour.
2025-06-02 14:30:57 -04:00
511 changed files with 79153 additions and 159288 deletions

853
.github/dependabot.yml vendored

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
# Contributor Guide # Contributor Guide
## Running a development build ## Installing dependencies
Install a node version manager such as [fnm](https://github.com/Schniz/fnm?tab=readme-ov-#installation). Install a node version manager such as [fnm](https://github.com/Schniz/fnm?tab=readme-ov-#installation).
@ -31,7 +31,9 @@ npm run install:rust:windows
npm run install:wasm-pack:cargo npm run install:wasm-pack:cargo
``` ```
Then to build the WASM layer, run: ## Building the app
To build the WASM layer, run:
``` ```
# macOS/Linux # macOS/Linux
@ -74,7 +76,7 @@ enable third-party cookies. You can enable third-party cookies by clicking on
the eye with a slash through it in the URL bar, and clicking on "Enable the eye with a slash through it in the URL bar, and clicking on "Enable
Third-Party Cookies". Third-Party Cookies".
## Desktop ### Developing with Electron
To spin up the desktop app, `npm install` and `npm run build:wasm` need to have been done before hand then: To spin up the desktop app, `npm install` and `npm run build:wasm` need to have been done before hand then:
@ -88,114 +90,7 @@ Devtools can be opened with the usual Command-Option-I (macOS) or Ctrl-Shift-I (
To package the app for your platform with electron-builder, run `npm run tronb:package:dev` (or `npm run tronb:package:prod` to point to the .env.production variables). To package the app for your platform with electron-builder, run `npm run tronb:package:dev` (or `npm run tronb:package:prod` to point to the .env.production variables).
## Checking out commits / Bisecting ## Running tests
Which commands from setup are one off vs. need to be run every time?
The following will need to be run when checking out a new commit and guarantees the build is not stale:
```bash
npm install
npm run build:wasm
npm start
```
## Before submitting a PR
Before you submit a contribution PR to this repo, please ensure that:
- There is a corresponding issue for the changes you want to make, so that discussion of approach can be had before work begins.
- You have separated out refactoring commits from feature commits as much as possible
- You have run all of the following commands locally:
- `npm run fmt`
- `npm run tsc`
- `npm run test`
- Here they are all together: `npm run fmt && npm run tsc && npm run test`
## Release a new version
#### 1. Create a 'Cut release $VERSION' issue
It will be used to document changelog discussions and release testing.
https://github.com/KittyCAD/modeling-app/issues/new
#### 2. Push a new tag
Decide on a `v`-prefixed semver `VERSION` (e.g. `v1.2.3`) with the team and tag the repo on the latest main:
```
git tag $VERSION --message=""
git push origin $VERSION
```
This will trigger the `build-apps` workflow to set the version, build & sign the apps, and generate release files.
The workflow should be listed right away [in this list](https://github.com/KittyCAD/modeling-app/actions/workflows/build-apps.yml?query=event%3Apush).
#### 3. Manually test artifacts
##### Release builds
The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in step 2).
Manually test against [this list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue.
A prompt should show up asking for a downgrade to the last release version. Running through that at the end of testing
and making sure the current release candidate has the ability to be updated to what electron-updater points to is critical,
but what is actually being downloaded and installed isn't.
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing.
```
# Windows (PowerShell)
& 'C:\Program Files\Zoo Design Studio\Zoo Design Studio.exe'
# macOS
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
# Linux
./Zoo Design Studio-{version}-{arch}-linux.AppImage
```
#### 4. Bump the KCL version
Follow the instructions [here](./rust/README.md) to publish new crates.
This ensures that the KCL accepted by the app is also accepted by the CLI.
#### 5. Publish the release
Head over to https://github.com/KittyCAD/modeling-app/releases/new, pick the newly created tag and type it in the **Release title** field as well.
Click **Generate release notes** as a starting point to discuss the changelog in the issue. Once done, make sure **Set as the latest release** is checked, and click **Publish release**.
A new `publish-apps-release` workflow will start and you should be able to find it [here](https://github.com/KittyCAD/modeling-app/actions?query=event%3Arelease). On success, the files will be uploaded to the public bucket as well as to the GitHub release, and the announcement on Discord will be sent.
#### 6. Close the issue
If everything is well and the release is out to the public, the issue tracking the release shall be closed.
## Fuzzing the parser
Make sure you install cargo fuzz:
```bash
$ cargo install cargo-fuzz
```
```bash
$ cd rust/kcl-lib
# list the fuzz targets
$ cargo fuzz list
# run the parser fuzzer
$ cargo +nightly fuzz run parser
```
For more information on fuzzing you can check out
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
## Tests
### Playwright tests ### Playwright tests
@ -313,8 +208,103 @@ then run tests that target the KCL language:
npm run test:rust npm run test:rust
``` ```
### Fuzzing the parser
Make sure you install cargo fuzz:
```bash
$ cargo install cargo-fuzz
```
```bash
$ cd rust/kcl-lib
# list the fuzz targets
$ cargo fuzz list
# run the parser fuzzer
$ cargo +nightly fuzz run parser
```
For more information on fuzzing you can check out
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
### Logging ### Logging
To display logging (to the terminal or console) set `ZOO_LOG=1`. This will log some warnings and simple performance metrics. To view these in test runs, use `-- --nocapture`. To display logging (to the terminal or console) set `ZOO_LOG=1`. This will log some warnings and simple performance metrics. To view these in test runs, use `-- --nocapture`.
To enable memory metrics, build with `--features dhat-heap`. To enable memory metrics, build with `--features dhat-heap`.
## Proposing changes
Before you submit a contribution PR to this repo, please ensure that:
- There is a corresponding issue for the changes you want to make, so that discussion of approach can be had before work begins.
- You have separated out refactoring commits from feature commits as much as possible
- You have run all of the following commands locally:
- `npm run fmt`
- `npm run tsc`
- `npm run test`
- Here they are all together: `npm run fmt && npm run tsc && npm run test`
## Shipping releases
#### 1. Create a 'Cut release $VERSION' issue
It will be used to document changelog discussions and release testing.
https://github.com/KittyCAD/modeling-app/issues/new
#### 2. Push a new tag
Decide on a `v`-prefixed semver `VERSION` (e.g. `v1.2.3`) with the team and tag the repo on the latest main:
```
git tag $VERSION --message=""
git push origin $VERSION
```
This will trigger the `build-apps` workflow to set the version, build & sign the apps, and generate release files.
The workflow should be listed right away [in this list](https://github.com/KittyCAD/modeling-app/actions/workflows/build-apps.yml?query=event%3Apush).
#### 3. Manually test artifacts
##### Release builds
The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in step 2).
Manually test against [this list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue.
A prompt should show up asking for a downgrade to the last release version. Running through that at the end of testing
and making sure the current release candidate has the ability to be updated to what electron-updater points to is critical,
but what is actually being downloaded and installed isn't.
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing.
```
# Windows (PowerShell)
& 'C:\Program Files\Zoo Design Studio\Zoo Design Studio.exe'
# macOS
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
# Linux
./Zoo Design Studio-{version}-{arch}-linux.AppImage
```
#### 4. Bump the KCL version
Follow the instructions [here](./rust/README.md) to publish new crates.
This ensures that the KCL accepted by the app is also accepted by the CLI.
#### 5. Publish the release
Head over to https://github.com/KittyCAD/modeling-app/releases/new, pick the newly created tag and type it in the **Release title** field as well.
Click **Generate release notes** as a starting point to discuss the changelog in the issue. Once done, make sure **Set as the latest release** is checked, and click **Publish release**.
A new `publish-apps-release` workflow will start and you should be able to find it [here](https://github.com/KittyCAD/modeling-app/actions?query=event%3Arelease). On success, the files will be uploaded to the public bucket as well as to the GitHub release, and the announcement on Discord will be sent.
#### 6. Close the issue
If everything is well and the release is out to the public, the issue tracking the release shall be closed.

View File

@ -1,24 +1,23 @@
--- ---
title: "appearance::hexString" title: "appearance::hexString"
subtitle: "Function in std::appearance" subtitle: "Function in std::appearance"
excerpt: "" excerpt: "Build a color from its red, green and blue components. These must be between 0 and 255."
layout: manual layout: manual
--- ---
Build a color from its red, green and blue components. These must be between 0 and 255.
```kcl ```kcl
appearance::hexString(@rgb: [number(_); 3]): string appearance::hexString(@rgb: [number(_); 3]): string
``` ```
Build a color from its red, green and blue components.
These must be between 0 and 255.
### Arguments ### Arguments
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `rgb` | [`[number(_); 3]`](/docs/kcl-std/types/std-types-number) | | Yes | | `rgb` | [`[number(_); 3]`](/docs/kcl-std/types/std-types-number) | The red, blue and green components of the color. Must be between 0 and 255. | Yes |
### Returns ### Returns

View File

@ -1,11 +1,11 @@
--- ---
title: "reduce" title: "reduce"
subtitle: "Function in std::array" subtitle: "Function in std::array"
excerpt: "" excerpt: "Take a starting value. Then, for each element of an array, calculate the next value, using the previous value and the element."
layout: manual layout: manual
--- ---
Take a starting value. Then, for each element of an array, calculate the next value, using the previous value and the element.
```kcl ```kcl
reduce( reduce(
@ -15,8 +15,7 @@ reduce(
): any ): any
``` ```
Take a starting value. Then, for each element of an array, calculate the next value,
using the previous value and the element.
### Arguments ### Arguments
@ -28,7 +27,7 @@ using the previous value and the element.
### Returns ### Returns
[`any`](/docs/kcl-std/types/std-types-any) [`any`](/docs/kcl-std/types/std-types-any) - The [`any`](/docs/kcl-std/types/std-types-any) type is the type of all possible values in KCL. I.e., if a function accepts an argument with type [`any`](/docs/kcl-std/types/std-types-any), then it can accept any value.
### Examples ### Examples

View File

@ -1,11 +1,11 @@
--- ---
title: "assert" title: "assert"
subtitle: "Function in std" subtitle: "Function in std"
excerpt: "" excerpt: "Check a value meets some expected conditions at runtime. Program terminates with an error if conditions aren't met. If you provide multiple conditions, they will all be checked and all must be met."
layout: manual layout: manual
--- ---
Check a value meets some expected conditions at runtime. Program terminates with an error if conditions aren't met. If you provide multiple conditions, they will all be checked and all must be met.
```kcl ```kcl
assert( assert(
@ -20,8 +20,7 @@ assert(
) )
``` ```
Check a value meets some expected conditions at runtime. Program terminates with an error if conditions aren't met.
If you provide multiple conditions, they will all be checked and all must be met.
### Arguments ### Arguments

View File

@ -1,11 +1,11 @@
--- ---
title: "polar" title: "polar"
subtitle: "Function in std::math" subtitle: "Function in std::math"
excerpt: "" excerpt: "Convert polar/sphere (azimuth, elevation, distance) coordinates to cartesian (x/y/z grid) coordinates."
layout: manual layout: manual
--- ---
Convert polar/sphere (azimuth, elevation, distance) coordinates to cartesian (x/y/z grid) coordinates.
```kcl ```kcl
polar( polar(
@ -14,8 +14,7 @@ polar(
): Point2d ): Point2d
``` ```
Convert polar/sphere (azimuth, elevation, distance) coordinates to
cartesian (x/y/z grid) coordinates.
### Arguments ### Arguments

View File

@ -1,11 +1,11 @@
--- ---
title: "rem" title: "rem"
subtitle: "Function in std::math" subtitle: "Function in std::math"
excerpt: "" excerpt: "Compute the remainder after dividing `num` by `div`. If `num` is negative, the result will be too."
layout: manual layout: manual
--- ---
Compute the remainder after dividing `num` by `div`. If `num` is negative, the result will be too.
```kcl ```kcl
rem( rem(
@ -14,8 +14,7 @@ rem(
): number ): number
``` ```
Compute the remainder after dividing `num` by `div`.
If `num` is negative, the result will be too.
### Arguments ### Arguments

View File

@ -10,13 +10,13 @@ Draw a line segment relative to the current origin using the polar measure of so
```kcl ```kcl
angledLine( angledLine(
@sketch: Sketch, @sketch: Sketch,
angle: number, angle: number(Angle),
length?: number, length?: number(Length),
lengthX?: number, lengthX?: number(Length),
lengthY?: number, lengthY?: number(Length),
endAbsoluteX?: number, endAbsoluteX?: number(Length),
endAbsoluteY?: number, endAbsoluteY?: number(Length),
tag?: TagDeclarator, tag?: tag,
): Sketch ): Sketch
``` ```
@ -27,13 +27,13 @@ angledLine(
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes | | `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes |
| `angle` | [`number`](/docs/kcl-std/types/std-types-number) | Which angle should the line be drawn at? | Yes | | `angle` | [`number(Angle)`](/docs/kcl-std/types/std-types-number) | Which angle should the line be drawn at? | Yes |
| `length` | [`number`](/docs/kcl-std/types/std-types-number) | Draw the line this distance along the given angle. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given. | No | | `length` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Draw the line this distance along the given angle. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given. | No |
| `lengthX` | [`number`](/docs/kcl-std/types/std-types-number) | Draw the line this distance along the X axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given. | No | | `lengthX` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Draw the line this distance along the X axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given. | No |
| `lengthY` | [`number`](/docs/kcl-std/types/std-types-number) | Draw the line this distance along the Y axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given. | No | | `lengthY` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Draw the line this distance along the Y axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given. | No |
| `endAbsoluteX` | [`number`](/docs/kcl-std/types/std-types-number) | Draw the line along the given angle until it reaches this point along the X axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given. | No | | `endAbsoluteX` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Draw the line along the given angle until it reaches this point along the X axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given. | No |
| `endAbsoluteY` | [`number`](/docs/kcl-std/types/std-types-number) | Draw the line along the given angle until it reaches this point along the Y axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given. | No | | `endAbsoluteY` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Draw the line along the given angle until it reaches this point along the Y axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given. | No |
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator) | Create a new tag which refers to this line | No | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this line. | No |
### Returns ### Returns
@ -46,7 +46,10 @@ angledLine(
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> yLine(endAbsolute = 15) |> yLine(endAbsolute = 15)
|> angledLine(angle = 30, length = 15) |> angledLine(
angle = 30,
length = 15,
)
|> line(end = [8, -10]) |> line(end = [8, -10])
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -10,10 +10,10 @@ Draw an angled line from the current origin, constructing a line segment such th
```kcl ```kcl
angledLineThatIntersects( angledLineThatIntersects(
@sketch: Sketch, @sketch: Sketch,
angle: number, angle: number(Angle),
intersectTag: TagIdentifier, intersectTag: tag,
offset?: number, offset?: number(Length),
tag?: TagDeclarator, tag?: tag,
): Sketch ): Sketch
``` ```
@ -24,10 +24,10 @@ angledLineThatIntersects(
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes | | `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes |
| `angle` | [`number`](/docs/kcl-std/types/std-types-number) | Which angle should the line be drawn at? | Yes | | `angle` | [`number(Angle)`](/docs/kcl-std/types/std-types-number) | Which angle should the line be drawn at? | Yes |
| `intersectTag` | [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier) | The tag of the line to intersect with | Yes | | `intersectTag` | [`tag`](/docs/kcl-std/types/std-types-tag) | The tag of the line to intersect with. | Yes |
| `offset` | [`number`](/docs/kcl-std/types/std-types-number) | The offset from the intersecting line. Defaults to 0. | No | | `offset` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The offset from the intersecting line. | No |
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator) | Create a new tag which refers to this line | No | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this line. | No |
### Returns ### Returns
@ -42,7 +42,11 @@ exampleSketch = startSketchOn(XZ)
|> line(endAbsolute = [5, 10]) |> line(endAbsolute = [5, 10])
|> line(endAbsolute = [-10, 10], tag = $lineToIntersect) |> line(endAbsolute = [-10, 10], tag = $lineToIntersect)
|> line(endAbsolute = [0, 20]) |> line(endAbsolute = [0, 20])
|> angledLineThatIntersects(angle = 80, intersectTag = lineToIntersect, offset = 10) |> angledLineThatIntersects(
angle = 80,
intersectTag = lineToIntersect,
offset = 10,
)
|> close() |> close()
example = extrude(exampleSketch, length = 10) example = extrude(exampleSketch, length = 10)

View File

@ -10,32 +10,37 @@ Draw a curved line segment along an imaginary circle.
```kcl ```kcl
arc( arc(
@sketch: Sketch, @sketch: Sketch,
angleStart?: number, angleStart?: number(Angle),
angleEnd?: number, angleEnd?: number(Angle),
radius?: number, radius?: number(Length),
diameter?: number, diameter?: number(Length),
interiorAbsolute?: Point2d, interiorAbsolute?: Point2d,
endAbsolute?: Point2d, endAbsolute?: Point2d,
tag?: TagDeclarator, tag?: tag,
): Sketch ): Sketch
``` ```
The arc is constructed such that the current position of the sketch is placed along an imaginary circle of the specified radius, at angleStart degrees. The resulting arc is the segment of the imaginary circle from that origin point to angleEnd, radius away from the center of the imaginary circle. The arc is constructed such that the current position of the sketch is
placed along an imaginary circle of the specified radius, at angleStart
degrees. The resulting arc is the segment of the imaginary circle from
that origin point to angleEnd, radius away from the center of the imaginary
circle.
Unless this makes a lot of sense and feels like what you're looking for to construct your shape, you're likely looking for tangentialArc. Unless this makes a lot of sense and feels like what you're looking
for to construct your shape, you're likely looking for tangentialArc.
### Arguments ### Arguments
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes | | `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes |
| `angleStart` | [`number`](/docs/kcl-std/types/std-types-number) | Where along the circle should this arc start? | No | | `angleStart` | [`number(Angle)`](/docs/kcl-std/types/std-types-number) | Where along the circle should this arc start? | No |
| `angleEnd` | [`number`](/docs/kcl-std/types/std-types-number) | Where along the circle should this arc end? | No | | `angleEnd` | [`number(Angle)`](/docs/kcl-std/types/std-types-number) | Where along the circle should this arc end? | No |
| `radius` | [`number`](/docs/kcl-std/types/std-types-number) | How large should the circle be? Incompatible with `diameter`. | No | | `radius` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | How large should the circle be? Incompatible with `diameter`. | No |
| `diameter` | [`number`](/docs/kcl-std/types/std-types-number) | How large should the circle be? Incompatible with `radius`. | No | | `diameter` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | How large should the circle be? Incompatible with `radius`. | No |
| `interiorAbsolute` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | Any point between the arc's start and end? Requires `endAbsolute`. Incompatible with `angleStart` or `angleEnd` | No | | `interiorAbsolute` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | Any point between the arc's start and end? Requires `endAbsolute`. Incompatible with `angleStart` or `angleEnd`. | No |
| `endAbsolute` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | Where should this arc end? Requires `interiorAbsolute`. Incompatible with `angleStart` or `angleEnd` | No | | `endAbsolute` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | Where should this arc end? Requires `interiorAbsolute`. Incompatible with `angleStart` or `angleEnd`. | No |
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator) | Create a new tag which refers to this line | No | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this arc. | No |
### Returns ### Returns
@ -48,7 +53,11 @@ Unless this makes a lot of sense and feels like what you're looking for to const
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [10, 0]) |> line(end = [10, 0])
|> arc(angleStart = 0, angleEnd = 280, radius = 16) |> arc(
angleStart = 0,
angleEnd = 280,
radius = 16
)
|> close() |> close()
example = extrude(exampleSketch, length = 10) example = extrude(exampleSketch, length = 10)
``` ```
@ -58,7 +67,10 @@ example = extrude(exampleSketch, length = 10)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> arc(endAbsolute = [10, 0], interiorAbsolute = [5, 5]) |> arc(
endAbsolute = [10,0],
interiorAbsolute = [5,5]
)
|> close() |> close()
example = extrude(exampleSketch, length = 10) example = extrude(exampleSketch, length = 10)
``` ```

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,11 @@
--- ---
title: "circle" title: "circle"
subtitle: "Function in std::sketch" subtitle: "Function in std::sketch"
excerpt: "" excerpt: "Construct a 2-dimensional circle, of the specified radius, centered at the provided (x, y) origin point."
layout: manual layout: manual
--- ---
Construct a 2-dimensional circle, of the specified radius, centered at the provided (x, y) origin point.
```kcl ```kcl
circle( circle(
@ -17,8 +17,7 @@ circle(
): Sketch ): Sketch
``` ```
Construct a 2-dimensional circle, of the specified radius, centered at
the provided (x, y) origin point.
### Arguments ### Arguments

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,11 @@
--- ---
title: "extrude" title: "extrude"
subtitle: "Function in std::sketch" subtitle: "Function in std::sketch"
excerpt: "" excerpt: "Extend a 2-dimensional sketch through a third dimension in order to create new 3-dimensional volume, or if extruded into an existing volume, cut into an existing solid."
layout: manual layout: manual
--- ---
Extend a 2-dimensional sketch through a third dimension in order to create new 3-dimensional volume, or if extruded into an existing volume, cut into an existing solid.
```kcl ```kcl
extrude( extrude(
@ -18,9 +18,6 @@ extrude(
): [Solid; 1+] ): [Solid; 1+]
``` ```
Extend a 2-dimensional sketch through a third dimension in order to
create new 3-dimensional volume, or if extruded into an existing volume,cut into an existing solid.
You can provide more than one sketch to extrude, and they will all be You can provide more than one sketch to extrude, and they will all be
extruded in the same direction. extruded in the same direction.

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@ layout: manual
Extract the 'x' axis value of the last line segment in the provided 2-d sketch. Extract the 'x' axis value of the last line segment in the provided 2-d sketch.
```kcl ```kcl
lastSegX(@sketch: Sketch): number lastSegX(@sketch: Sketch): number(Length)
``` ```
@ -17,11 +17,11 @@ lastSegX(@sketch: Sketch): number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | The sketch whose line segment is being queried | Yes | | `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | The sketch whose line segment is being queried. | Yes |
### Returns ### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number. [`number(Length)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples

View File

@ -8,7 +8,7 @@ layout: manual
Extract the 'y' axis value of the last line segment in the provided 2-d sketch. Extract the 'y' axis value of the last line segment in the provided 2-d sketch.
```kcl ```kcl
lastSegY(@sketch: Sketch): number lastSegY(@sketch: Sketch): number(Length)
``` ```
@ -17,11 +17,11 @@ lastSegY(@sketch: Sketch): number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | The sketch whose line segment is being queried | Yes | | `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | The sketch whose line segment is being queried. | Yes |
### Returns ### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number. [`number(Length)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples

View File

@ -12,7 +12,7 @@ line(
@sketch: Sketch, @sketch: Sketch,
endAbsolute?: Point2d, endAbsolute?: Point2d,
end?: Point2d, end?: Point2d,
tag?: TagDeclarator, tag?: tag,
): Sketch ): Sketch
``` ```
@ -25,7 +25,7 @@ line(
| `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes | | `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes |
| `endAbsolute` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | Which absolute point should this line go to? Incompatible with `end`. | No | | `endAbsolute` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | Which absolute point should this line go to? Incompatible with `end`. | No |
| `end` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | How far away (along the X and Y axes) should this line go? Incompatible with `endAbsolute`. | No | | `end` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | How far away (along the X and Y axes) should this line go? Incompatible with `endAbsolute`. | No |
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator) | Create a new tag which refers to this line | No | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this line. | No |
### Returns ### Returns

View File

@ -19,7 +19,7 @@ loft(
): Solid ): Solid
``` ```
The sketches need to closed and on the same plane. The sketches need to be closed and on different planes that are parallel.
### Arguments ### Arguments

View File

@ -1,11 +1,11 @@
--- ---
title: "patternCircular2d" title: "patternCircular2d"
subtitle: "Function in std::sketch" subtitle: "Function in std::sketch"
excerpt: "" excerpt: "Repeat a 2-dimensional sketch some number of times along a partial or complete circle some specified number of times. Each object may additionally be rotated along the circle, ensuring orientation of the solid with respect to the center of the circle is maintained."
layout: manual layout: manual
--- ---
Repeat a 2-dimensional sketch some number of times along a partial or complete circle some specified number of times. Each object may additionally be rotated along the circle, ensuring orientation of the solid with respect to the center of the circle is maintained.
```kcl ```kcl
patternCircular2d( patternCircular2d(
@ -18,9 +18,7 @@ patternCircular2d(
): [Sketch; 1+] ): [Sketch; 1+]
``` ```
Repeat a 2-dimensional sketch some number of times along a partial or
complete circle some specified number of times. Each object mayadditionally be rotated along the circle, ensuring orientation of the
solid with respect to the center of the circle is maintained.
### Arguments ### Arguments

View File

@ -1,11 +1,11 @@
--- ---
title: "patternLinear2d" title: "patternLinear2d"
subtitle: "Function in std::sketch" subtitle: "Function in std::sketch"
excerpt: "" excerpt: "Repeat a 2-dimensional sketch along some dimension, with a dynamic amount of distance between each repetition, some specified number of times."
layout: manual layout: manual
--- ---
Repeat a 2-dimensional sketch along some dimension, with a dynamic amount of distance between each repetition, some specified number of times.
```kcl ```kcl
patternLinear2d( patternLinear2d(
@ -17,8 +17,7 @@ patternLinear2d(
): [Sketch; 1+] ): [Sketch; 1+]
``` ```
Repeat a 2-dimensional sketch along some dimension, with a dynamic amount
of distance between each repetition, some specified number of times.
### Arguments ### Arguments

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@ layout: manual
Compute the angle (in degrees) of the provided line segment. Compute the angle (in degrees) of the provided line segment.
```kcl ```kcl
segAng(@tag: TagIdentifier): number segAng(@tag: tag): number(Angle)
``` ```
@ -17,11 +17,11 @@ segAng(@tag: TagIdentifier): number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier) | The line segment being queried by its tag | Yes | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | The line segment being queried by its tag. | Yes |
### Returns ### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number. [`number(Angle)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples

View File

@ -8,7 +8,7 @@ layout: manual
Compute the ending point of the provided line segment. Compute the ending point of the provided line segment.
```kcl ```kcl
segEnd(@tag: TagIdentifier): Point2d segEnd(@tag: tag): Point2d
``` ```
@ -17,7 +17,7 @@ segEnd(@tag: TagIdentifier): Point2d
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier) | The line segment being queried by its tag | Yes | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | The line segment being queried by its tag. | Yes |
### Returns ### Returns
@ -39,9 +39,9 @@ cube = startSketchOn(XY)
fn cylinder(radius, tag) { fn cylinder(radius, tag) {
return startSketchOn(XY) return startSketchOn(XY)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> circle(radius = radius, center = segEnd(tag)) |> circle(radius = radius, center = segEnd(tag) )
|> extrude(length = radius) |> extrude(length = radius)
} }
cylinder(radius = 1, tag = line1) cylinder(radius = 1, tag = line1)

View File

@ -8,7 +8,7 @@ layout: manual
Compute the ending point of the provided line segment along the 'x' axis. Compute the ending point of the provided line segment along the 'x' axis.
```kcl ```kcl
segEndX(@tag: TagIdentifier): number segEndX(@tag: tag): number(Length)
``` ```
@ -17,11 +17,11 @@ segEndX(@tag: TagIdentifier): number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier) | The line segment being queried by its tag | Yes | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | The line segment being queried by its tag. | Yes |
### Returns ### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number. [`number(Length)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples

View File

@ -8,7 +8,7 @@ layout: manual
Compute the ending point of the provided line segment along the 'y' axis. Compute the ending point of the provided line segment along the 'y' axis.
```kcl ```kcl
segEndY(@tag: TagIdentifier): number segEndY(@tag: tag): number(Length)
``` ```
@ -17,11 +17,11 @@ segEndY(@tag: TagIdentifier): number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier) | The line segment being queried by its tag | Yes | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | The line segment being queried by its tag. | Yes |
### Returns ### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number. [`number(Length)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples

View File

@ -8,7 +8,7 @@ layout: manual
Compute the length of the provided line segment. Compute the length of the provided line segment.
```kcl ```kcl
segLen(@tag: TagIdentifier): number segLen(@tag: tag): number(Length)
``` ```
@ -17,11 +17,11 @@ segLen(@tag: TagIdentifier): number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier) | The line segment being queried by its tag | Yes | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | The line segment being queried by its tag. | Yes |
### Returns ### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number. [`number(Length)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples
@ -29,9 +29,16 @@ segLen(@tag: TagIdentifier): number
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 60, length = 10, tag = $thing) |> angledLine(
angle = 60,
length = 10,
tag = $thing,
)
|> tangentialArc(angle = -120, radius = 5) |> tangentialArc(angle = -120, radius = 5)
|> angledLine(angle = -60, length = segLen(thing)) |> angledLine(
angle = -60,
length = segLen(thing),
)
|> close() |> close()
example = extrude(exampleSketch, length = 5) example = extrude(exampleSketch, length = 5)

View File

@ -8,7 +8,7 @@ layout: manual
Compute the starting point of the provided line segment. Compute the starting point of the provided line segment.
```kcl ```kcl
segStart(@tag: TagIdentifier): Point2d segStart(@tag: tag): Point2d
``` ```
@ -17,7 +17,7 @@ segStart(@tag: TagIdentifier): Point2d
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier) | The line segment being queried by its tag | Yes | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | The line segment being queried by its tag. | Yes |
### Returns ### Returns
@ -39,9 +39,9 @@ cube = startSketchOn(XY)
fn cylinder(radius, tag) { fn cylinder(radius, tag) {
return startSketchOn(XY) return startSketchOn(XY)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> circle(radius = radius, center = segStart(tag)) |> circle( radius = radius, center = segStart(tag) )
|> extrude(length = radius) |> extrude(length = radius)
} }
cylinder(radius = 1, tag = line1) cylinder(radius = 1, tag = line1)

View File

@ -8,7 +8,7 @@ layout: manual
Compute the starting point of the provided line segment along the 'x' axis. Compute the starting point of the provided line segment along the 'x' axis.
```kcl ```kcl
segStartX(@tag: TagIdentifier): number segStartX(@tag: tag): number(Length)
``` ```
@ -17,11 +17,11 @@ segStartX(@tag: TagIdentifier): number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier) | The line segment being queried by its tag | Yes | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | The line segment being queried by its tag. | Yes |
### Returns ### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number. [`number(Length)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples

View File

@ -8,7 +8,7 @@ layout: manual
Compute the starting point of the provided line segment along the 'y' axis. Compute the starting point of the provided line segment along the 'y' axis.
```kcl ```kcl
segStartY(@tag: TagIdentifier): number segStartY(@tag: tag): number(Length)
``` ```
@ -17,11 +17,11 @@ segStartY(@tag: TagIdentifier): number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier) | The line segment being queried by its tag | Yes | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | The line segment being queried by its tag. | Yes |
### Returns ### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number. [`number(Length)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples
@ -32,7 +32,7 @@ exampleSketch = startSketchOn(XZ)
|> line(end = [20, 0]) |> line(end = [20, 0])
|> line(end = [0, 3], tag = $thing) |> line(end = [0, 3], tag = $thing)
|> line(end = [-10, 0]) |> line(end = [-10, 0])
|> line(end = [0, 20 - segStartY(thing)]) |> line(end = [0, 20-segStartY(thing)])
|> line(end = [-10, 0]) |> line(end = [-10, 0])
|> close() |> close()

View File

@ -9,9 +9,9 @@ Start a new profile at a given point.
```kcl ```kcl
startProfile( startProfile(
@sketchSurface: Plane | Face, @startProfileOn: Plane | Face,
at: Point2d, at: Point2d,
tag?: TagDeclarator, tag?: tag,
): Sketch ): Sketch
``` ```
@ -21,9 +21,9 @@ startProfile(
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `sketchSurface` | [`Plane`](/docs/kcl-std/types/std-types-Plane) or [`Face`](/docs/kcl-std/types/std-types-Face) | What to start the profile on | Yes | | `startProfileOn` | [`Plane`](/docs/kcl-std/types/std-types-Plane) or [`Face`](/docs/kcl-std/types/std-types-Face) | What to start the profile on. | Yes |
| `at` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | Where to start the profile. An absolute point. | Yes | | `at` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | Where to start the profile. An absolute point. | Yes |
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator) | Tag this first starting point | No | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Tag this first starting point. | No |
### Returns ### Returns

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@ layout: manual
Returns the angle coming out of the end of the segment in degrees. Returns the angle coming out of the end of the segment in degrees.
```kcl ```kcl
tangentToEnd(@tag: TagIdentifier): number tangentToEnd(@tag: tag): number(Angle)
``` ```
@ -17,11 +17,11 @@ tangentToEnd(@tag: TagIdentifier): number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier) | The line segment being queried by its tag | Yes | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | The line segment being queried by its tag. | Yes |
### Returns ### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number. [`number(Angle)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples
@ -32,7 +32,10 @@ pillSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [20, 0]) |> line(end = [20, 0])
|> tangentialArc(end = [0, 10], tag = $arc1) |> tangentialArc(end = [0, 10], tag = $arc1)
|> angledLine(angle = tangentToEnd(arc1), length = 20) |> angledLine(
angle = tangentToEnd(arc1),
length = 20,
)
|> tangentialArc(end = [0, -10]) |> tangentialArc(end = [0, -10])
|> close() |> close()
@ -47,7 +50,10 @@ pillSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [0, 20]) |> line(end = [0, 20])
|> tangentialArc(endAbsolute = [10, 20], tag = $arc1) |> tangentialArc(endAbsolute = [10, 20], tag = $arc1)
|> angledLine(angle = tangentToEnd(arc1), length = 20) |> angledLine(
angle = tangentToEnd(arc1),
length = 20,
)
|> tangentialArc(end = [-10, 0]) |> tangentialArc(end = [-10, 0])
|> close() |> close()
@ -60,7 +66,10 @@ pillExtrude = extrude(pillSketch, length = 10)
rectangleSketch = startSketchOn(XZ) rectangleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [10, 0], tag = $seg1) |> line(end = [10, 0], tag = $seg1)
|> angledLine(angle = tangentToEnd(seg1), length = 10) |> angledLine(
angle = tangentToEnd(seg1),
length = 10,
)
|> line(end = [0, 10]) |> line(end = [0, 10])
|> line(end = [-20, 0]) |> line(end = [-20, 0])
|> close() |> close()
@ -73,7 +82,11 @@ rectangleExtrude = extrude(rectangleSketch, length = 10)
```kcl ```kcl
bottom = startSketchOn(XY) bottom = startSketchOn(XY)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> arc(endAbsolute = [10, 10], interiorAbsolute = [5, 1], tag = $arc1) |> arc(
endAbsolute = [10, 10],
interiorAbsolute = [5, 1],
tag = $arc1,
)
|> angledLine(angle = tangentToEnd(arc1), length = 20) |> angledLine(angle = tangentToEnd(arc1), length = 20)
|> close() |> close()
``` ```
@ -82,7 +95,7 @@ bottom = startSketchOn(XY)
```kcl ```kcl
circSketch = startSketchOn(XY) circSketch = startSketchOn(XY)
|> circle(center = [0, 0], radius = 3, tag = $circ) |> circle(center = [0, 0], radius= 3, tag = $circ)
triangleSketch = startSketchOn(XY) triangleSketch = startSketchOn(XY)
|> startProfile(at = [-5, 0]) |> startProfile(at = [-5, 0])

View File

@ -12,14 +12,18 @@ tangentialArc(
@sketch: Sketch, @sketch: Sketch,
endAbsolute?: Point2d, endAbsolute?: Point2d,
end?: Point2d, end?: Point2d,
radius?: number, radius?: number(Length),
diameter?: number, diameter?: number(Length),
angle?: number, angle?: number(Angle),
tag?: TagDeclarator, tag?: tag,
): Sketch ): Sketch
``` ```
When using radius and angle, draw a curved line segment along part of an imaginary circle. The arc is constructed such that the last line segment is placed tangent to the imaginary circle of the specified radius. The resulting arc is the segment of the imaginary circle from that tangent point for 'angle' degrees along the imaginary circle. When using radius and angle, draw a curved line segment along part of an
imaginary circle. The arc is constructed such that the last line segment is
placed tangent to the imaginary circle of the specified radius. The
resulting arc is the segment of the imaginary circle from that tangent point
for 'angle' degrees along the imaginary circle.
### Arguments ### Arguments
@ -28,10 +32,10 @@ When using radius and angle, draw a curved line segment along part of an imagina
| `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes | | `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes |
| `endAbsolute` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | Which absolute point should this arc go to? Incompatible with `end`, `radius`, and `offset`. | No | | `endAbsolute` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | Which absolute point should this arc go to? Incompatible with `end`, `radius`, and `offset`. | No |
| `end` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | How far away (along the X and Y axes) should this arc go? Incompatible with `endAbsolute`, `radius`, and `offset`. | No | | `end` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | How far away (along the X and Y axes) should this arc go? Incompatible with `endAbsolute`, `radius`, and `offset`. | No |
| `radius` | [`number`](/docs/kcl-std/types/std-types-number) | Radius of the imaginary circle. `angle` must be given. Incompatible with `end` and `endAbsolute` and `diameter`. | No | | `radius` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Radius of the imaginary circle. `angle` must be given. Incompatible with `end` and `endAbsolute` and `diameter`. | No |
| `diameter` | [`number`](/docs/kcl-std/types/std-types-number) | Diameter of the imaginary circle. `angle` must be given. Incompatible with `end` and `endAbsolute` and `radius`. | No | | `diameter` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Diameter of the imaginary circle. `angle` must be given. Incompatible with `end` and `endAbsolute` and `radius`. | No |
| `angle` | [`number`](/docs/kcl-std/types/std-types-number) | Offset of the arc in degrees. `radius` must be given. Incompatible with `end` and `endAbsolute`. | No | | `angle` | [`number(Angle)`](/docs/kcl-std/types/std-types-number) | Offset of the arc. `radius` must be given. Incompatible with `end` and `endAbsolute`. | No |
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator) | Create a new tag which refers to this arc | No | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this arc. | No |
### Returns ### Returns
@ -43,7 +47,10 @@ When using radius and angle, draw a curved line segment along part of an imagina
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 45, length = 10) |> angledLine(
angle = 45,
length = 10,
)
|> tangentialArc(end = [0, -10]) |> tangentialArc(end = [0, -10])
|> line(end = [-10, 0]) |> line(end = [-10, 0])
|> close() |> close()
@ -56,7 +63,10 @@ example = extrude(exampleSketch, length = 10)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 60, length = 10) |> angledLine(
angle = 60,
length = 10,
)
|> tangentialArc(endAbsolute = [15, 15]) |> tangentialArc(endAbsolute = [15, 15])
|> line(end = [10, -15]) |> line(end = [10, -15])
|> close() |> close()
@ -69,9 +79,15 @@ example = extrude(exampleSketch, length = 10)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 60, length = 10) |> angledLine(
angle = 60,
length = 10,
)
|> tangentialArc(radius = 10, angle = -120) |> tangentialArc(radius = 10, angle = -120)
|> angledLine(angle = -60, length = 10) |> angledLine(
angle = -60,
length = 10,
)
|> close() |> close()
example = extrude(exampleSketch, length = 10) example = extrude(exampleSketch, length = 10)

View File

@ -10,9 +10,9 @@ Draw a line relative to the current origin to a specified distance away from the
```kcl ```kcl
xLine( xLine(
@sketch: Sketch, @sketch: Sketch,
length?: number, length?: number(Length),
endAbsolute?: number, endAbsolute?: number(Length),
tag?: TagDeclarator, tag?: tag,
): Sketch ): Sketch
``` ```
@ -23,9 +23,9 @@ xLine(
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes | | `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes |
| `length` | [`number`](/docs/kcl-std/types/std-types-number) | How far away along the X axis should this line go? Incompatible with `endAbsolute`. | No | | `length` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | How far away along the X axis should this line go? Incompatible with `endAbsolute`. | No |
| `endAbsolute` | [`number`](/docs/kcl-std/types/std-types-number) | Which absolute X value should this line go to? Incompatible with `length`. | No | | `endAbsolute` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Which absolute X value should this line go to? Incompatible with `length`. | No |
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator) | Create a new tag which refers to this line | No | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this line. | No |
### Returns ### Returns
@ -38,10 +38,16 @@ xLine(
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> xLine(length = 15) |> xLine(length = 15)
|> angledLine(angle = 80, length = 15) |> angledLine(
angle = 80,
length = 15,
)
|> line(end = [8, -10]) |> line(end = [8, -10])
|> xLine(length = 10) |> xLine(length = 10)
|> angledLine(angle = 120, length = 30) |> angledLine(
angle = 120,
length = 30,
)
|> xLine(length = -15) |> xLine(length = -15)
|> close() |> close()

View File

@ -10,9 +10,9 @@ Draw a line relative to the current origin to a specified distance away from the
```kcl ```kcl
yLine( yLine(
@sketch: Sketch, @sketch: Sketch,
length?: number, length?: number(Length),
endAbsolute?: number, endAbsolute?: number(Length),
tag?: TagDeclarator, tag?: tag,
): Sketch ): Sketch
``` ```
@ -23,9 +23,9 @@ yLine(
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes | | `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes |
| `length` | [`number`](/docs/kcl-std/types/std-types-number) | How far away along the Y axis should this line go? Incompatible with `endAbsolute`. | No | | `length` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | How far away along the Y axis should this line go? Incompatible with `endAbsolute`. | No |
| `endAbsolute` | [`number`](/docs/kcl-std/types/std-types-number) | Which absolute Y value should this line go to? Incompatible with `length`. | No | | `endAbsolute` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Which absolute Y value should this line go to? Incompatible with `length`. | No |
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator) | Create a new tag which refers to this line | No | | [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this line. | No |
### Returns ### Returns
@ -38,7 +38,10 @@ yLine(
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> yLine(length = 15) |> yLine(length = 15)
|> angledLine(angle = 30, length = 15) |> angledLine(
angle = 30,
length = 15,
)
|> line(end = [8, -10]) |> line(end = [8, -10])
|> yLine(length = -5) |> yLine(length = -5)
|> close() |> close()

View File

@ -1,11 +1,11 @@
--- ---
title: "intersect" title: "intersect"
subtitle: "Function in std::solid" subtitle: "Function in std::solid"
excerpt: "" excerpt: "Intersect returns the shared volume between multiple solids, preserving only overlapping regions."
layout: manual layout: manual
--- ---
Intersect returns the shared volume between multiple solids, preserving only overlapping regions.
```kcl ```kcl
intersect( intersect(
@ -14,8 +14,6 @@ intersect(
): [Solid; 1+] ): [Solid; 1+]
``` ```
Intersect returns the shared volume between multiple solids, preserving only
overlapping regions.
Intersect computes the geometric intersection of multiple solid bodies, Intersect computes the geometric intersection of multiple solid bodies,
returning a new solid representing the volume that is common to all input returning a new solid representing the volume that is common to all input
solids. This operation is useful for determining shared material regions, solids. This operation is useful for determining shared material regions,

View File

@ -1,11 +1,11 @@
--- ---
title: "patternCircular3d" title: "patternCircular3d"
subtitle: "Function in std::solid" subtitle: "Function in std::solid"
excerpt: "" excerpt: "Repeat a 3-dimensional solid some number of times along a partial or complete circle some specified number of times. Each object may additionally be rotated along the circle, ensuring orientation of the solid with respect to the center of the circle is maintained."
layout: manual layout: manual
--- ---
Repeat a 3-dimensional solid some number of times along a partial or complete circle some specified number of times. Each object may additionally be rotated along the circle, ensuring orientation of the solid with respect to the center of the circle is maintained.
```kcl ```kcl
patternCircular3d( patternCircular3d(
@ -19,9 +19,7 @@ patternCircular3d(
): [Solid; 1+] ): [Solid; 1+]
``` ```
Repeat a 3-dimensional solid some number of times along a partial or
complete circle some specified number of times. Each object mayadditionally be rotated along the circle, ensuring orientation of the
solid with respect to the center of the circle is maintained.
### Arguments ### Arguments

View File

@ -1,11 +1,11 @@
--- ---
title: "patternLinear3d" title: "patternLinear3d"
subtitle: "Function in std::solid" subtitle: "Function in std::solid"
excerpt: "" excerpt: "Repeat a 3-dimensional solid along a linear path, with a dynamic amount of distance between each repetition, some specified number of times."
layout: manual layout: manual
--- ---
Repeat a 3-dimensional solid along a linear path, with a dynamic amount of distance between each repetition, some specified number of times.
```kcl ```kcl
patternLinear3d( patternLinear3d(
@ -17,8 +17,7 @@ patternLinear3d(
): [Solid; 1+] ): [Solid; 1+]
``` ```
Repeat a 3-dimensional solid along a linear path, with a dynamic amount
of distance between each repetition, some specified number of times.
### Arguments ### Arguments

View File

@ -1,11 +1,11 @@
--- ---
title: "shell" title: "shell"
subtitle: "Function in std::solid" subtitle: "Function in std::solid"
excerpt: "" excerpt: "Remove volume from a 3-dimensional shape such that a wall of the provided thickness remains, taking volume starting at the provided face, leaving it open in that direction."
layout: manual layout: manual
--- ---
Remove volume from a 3-dimensional shape such that a wall of the provided thickness remains, taking volume starting at the provided face, leaving it open in that direction.
```kcl ```kcl
shell( shell(
@ -15,8 +15,7 @@ shell(
): [Solid] ): [Solid]
``` ```
Remove volume from a 3-dimensional shape such that a wall of the
provided thickness remains, taking volume starting at the providedface, leaving it open in that direction.
### Arguments ### Arguments

View File

@ -47,47 +47,47 @@ layout: manual
* [`sqrt`](/docs/kcl-std/functions/std-math-sqrt) * [`sqrt`](/docs/kcl-std/functions/std-math-sqrt)
* [`tan`](/docs/kcl-std/functions/std-math-tan) * [`tan`](/docs/kcl-std/functions/std-math-tan)
* [**std::sketch**](/docs/kcl-std/modules/std-sketch) * [**std::sketch**](/docs/kcl-std/modules/std-sketch)
* [`angledLine`](/docs/kcl-std/angledLine) * [`angledLine`](/docs/kcl-std/functions/std-sketch-angledLine)
* [`angledLineThatIntersects`](/docs/kcl-std/angledLineThatIntersects) * [`angledLineThatIntersects`](/docs/kcl-std/functions/std-sketch-angledLineThatIntersects)
* [`arc`](/docs/kcl-std/arc) * [`arc`](/docs/kcl-std/functions/std-sketch-arc)
* [`bezierCurve`](/docs/kcl-std/bezierCurve) * [`bezierCurve`](/docs/kcl-std/functions/std-sketch-bezierCurve)
* [`circle`](/docs/kcl-std/functions/std-sketch-circle) * [`circle`](/docs/kcl-std/functions/std-sketch-circle)
* [`circleThreePoint`](/docs/kcl-std/functions/std-sketch-circleThreePoint) * [`circleThreePoint`](/docs/kcl-std/functions/std-sketch-circleThreePoint)
* [`close`](/docs/kcl-std/close) * [`close`](/docs/kcl-std/functions/std-sketch-close)
* [`extrude`](/docs/kcl-std/functions/std-sketch-extrude) * [`extrude`](/docs/kcl-std/functions/std-sketch-extrude)
* [`getCommonEdge`](/docs/kcl-std/functions/std-sketch-getCommonEdge) * [`getCommonEdge`](/docs/kcl-std/functions/std-sketch-getCommonEdge)
* [`getNextAdjacentEdge`](/docs/kcl-std/functions/std-sketch-getNextAdjacentEdge) * [`getNextAdjacentEdge`](/docs/kcl-std/functions/std-sketch-getNextAdjacentEdge)
* [`getOppositeEdge`](/docs/kcl-std/functions/std-sketch-getOppositeEdge) * [`getOppositeEdge`](/docs/kcl-std/functions/std-sketch-getOppositeEdge)
* [`getPreviousAdjacentEdge`](/docs/kcl-std/functions/std-sketch-getPreviousAdjacentEdge) * [`getPreviousAdjacentEdge`](/docs/kcl-std/functions/std-sketch-getPreviousAdjacentEdge)
* [`involuteCircular`](/docs/kcl-std/involuteCircular) * [`involuteCircular`](/docs/kcl-std/functions/std-sketch-involuteCircular)
* [`lastSegX`](/docs/kcl-std/lastSegX) * [`lastSegX`](/docs/kcl-std/functions/std-sketch-lastSegX)
* [`lastSegY`](/docs/kcl-std/lastSegY) * [`lastSegY`](/docs/kcl-std/functions/std-sketch-lastSegY)
* [`line`](/docs/kcl-std/line) * [`line`](/docs/kcl-std/functions/std-sketch-line)
* [`loft`](/docs/kcl-std/functions/std-sketch-loft) * [`loft`](/docs/kcl-std/functions/std-sketch-loft)
* [`patternCircular2d`](/docs/kcl-std/functions/std-sketch-patternCircular2d) * [`patternCircular2d`](/docs/kcl-std/functions/std-sketch-patternCircular2d)
* [`patternLinear2d`](/docs/kcl-std/functions/std-sketch-patternLinear2d) * [`patternLinear2d`](/docs/kcl-std/functions/std-sketch-patternLinear2d)
* [`patternTransform2d`](/docs/kcl-std/functions/std-sketch-patternTransform2d) * [`patternTransform2d`](/docs/kcl-std/functions/std-sketch-patternTransform2d)
* [`polygon`](/docs/kcl-std/functions/std-sketch-polygon) * [`polygon`](/docs/kcl-std/functions/std-sketch-polygon)
* [`profileStart`](/docs/kcl-std/profileStart) * [`profileStart`](/docs/kcl-std/functions/std-sketch-profileStart)
* [`profileStartX`](/docs/kcl-std/profileStartX) * [`profileStartX`](/docs/kcl-std/functions/std-sketch-profileStartX)
* [`profileStartY`](/docs/kcl-std/profileStartY) * [`profileStartY`](/docs/kcl-std/functions/std-sketch-profileStartY)
* [`revolve`](/docs/kcl-std/functions/std-sketch-revolve) * [`revolve`](/docs/kcl-std/functions/std-sketch-revolve)
* [`segAng`](/docs/kcl-std/segAng) * [`segAng`](/docs/kcl-std/functions/std-sketch-segAng)
* [`segEnd`](/docs/kcl-std/segEnd) * [`segEnd`](/docs/kcl-std/functions/std-sketch-segEnd)
* [`segEndX`](/docs/kcl-std/segEndX) * [`segEndX`](/docs/kcl-std/functions/std-sketch-segEndX)
* [`segEndY`](/docs/kcl-std/segEndY) * [`segEndY`](/docs/kcl-std/functions/std-sketch-segEndY)
* [`segLen`](/docs/kcl-std/segLen) * [`segLen`](/docs/kcl-std/functions/std-sketch-segLen)
* [`segStart`](/docs/kcl-std/segStart) * [`segStart`](/docs/kcl-std/functions/std-sketch-segStart)
* [`segStartX`](/docs/kcl-std/segStartX) * [`segStartX`](/docs/kcl-std/functions/std-sketch-segStartX)
* [`segStartY`](/docs/kcl-std/segStartY) * [`segStartY`](/docs/kcl-std/functions/std-sketch-segStartY)
* [`startProfile`](/docs/kcl-std/startProfile) * [`startProfile`](/docs/kcl-std/functions/std-sketch-startProfile)
* [`startSketchOn`](/docs/kcl-std/startSketchOn) * [`startSketchOn`](/docs/kcl-std/functions/std-sketch-startSketchOn)
* [`subtract2d`](/docs/kcl-std/subtract2d) * [`subtract2d`](/docs/kcl-std/functions/std-sketch-subtract2d)
* [`sweep`](/docs/kcl-std/functions/std-sketch-sweep) * [`sweep`](/docs/kcl-std/functions/std-sketch-sweep)
* [`tangentToEnd`](/docs/kcl-std/tangentToEnd) * [`tangentToEnd`](/docs/kcl-std/functions/std-sketch-tangentToEnd)
* [`tangentialArc`](/docs/kcl-std/tangentialArc) * [`tangentialArc`](/docs/kcl-std/functions/std-sketch-tangentialArc)
* [`xLine`](/docs/kcl-std/xLine) * [`xLine`](/docs/kcl-std/functions/std-sketch-xLine)
* [`yLine`](/docs/kcl-std/yLine) * [`yLine`](/docs/kcl-std/functions/std-sketch-yLine)
* [**std::solid**](/docs/kcl-std/modules/std-solid) * [**std::solid**](/docs/kcl-std/modules/std-solid)
* [`appearance`](/docs/kcl-std/functions/std-solid-appearance) * [`appearance`](/docs/kcl-std/functions/std-solid-appearance)
* [`chamfer`](/docs/kcl-std/functions/std-solid-chamfer) * [`chamfer`](/docs/kcl-std/functions/std-solid-chamfer)
@ -144,11 +144,7 @@ layout: manual
See also the [types overview](/docs/kcl-lang/types) See also the [types overview](/docs/kcl-lang/types)
* [**Primitive types**](/docs/kcl-lang/types) * [**Primitive types**](/docs/kcl-lang/types)
* [`End`](/docs/kcl-lang/types#End)
* [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry) * [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry)
* [`Start`](/docs/kcl-lang/types#Start)
* [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator)
* [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier)
* [`any`](/docs/kcl-std/types/std-types-any) * [`any`](/docs/kcl-std/types/std-types-any)
* [`bool`](/docs/kcl-std/types/std-types-bool) * [`bool`](/docs/kcl-std/types/std-types-bool)
* [`fn`](/docs/kcl-std/types/std-types-fn) * [`fn`](/docs/kcl-std/types/std-types-fn)

View File

@ -12,45 +12,45 @@ This module contains functions for creating and manipulating sketches, and makin
## Functions and constants ## Functions and constants
* [`angledLine`](/docs/kcl-std/angledLine) * [`angledLine`](/docs/kcl-std/functions/std-sketch-angledLine)
* [`angledLineThatIntersects`](/docs/kcl-std/angledLineThatIntersects) * [`angledLineThatIntersects`](/docs/kcl-std/functions/std-sketch-angledLineThatIntersects)
* [`arc`](/docs/kcl-std/arc) * [`arc`](/docs/kcl-std/functions/std-sketch-arc)
* [`bezierCurve`](/docs/kcl-std/bezierCurve) * [`bezierCurve`](/docs/kcl-std/functions/std-sketch-bezierCurve)
* [`circle`](/docs/kcl-std/functions/std-sketch-circle) * [`circle`](/docs/kcl-std/functions/std-sketch-circle)
* [`circleThreePoint`](/docs/kcl-std/functions/std-sketch-circleThreePoint) * [`circleThreePoint`](/docs/kcl-std/functions/std-sketch-circleThreePoint)
* [`close`](/docs/kcl-std/close) * [`close`](/docs/kcl-std/functions/std-sketch-close)
* [`extrude`](/docs/kcl-std/functions/std-sketch-extrude) * [`extrude`](/docs/kcl-std/functions/std-sketch-extrude)
* [`getCommonEdge`](/docs/kcl-std/functions/std-sketch-getCommonEdge) * [`getCommonEdge`](/docs/kcl-std/functions/std-sketch-getCommonEdge)
* [`getNextAdjacentEdge`](/docs/kcl-std/functions/std-sketch-getNextAdjacentEdge) * [`getNextAdjacentEdge`](/docs/kcl-std/functions/std-sketch-getNextAdjacentEdge)
* [`getOppositeEdge`](/docs/kcl-std/functions/std-sketch-getOppositeEdge) * [`getOppositeEdge`](/docs/kcl-std/functions/std-sketch-getOppositeEdge)
* [`getPreviousAdjacentEdge`](/docs/kcl-std/functions/std-sketch-getPreviousAdjacentEdge) * [`getPreviousAdjacentEdge`](/docs/kcl-std/functions/std-sketch-getPreviousAdjacentEdge)
* [`involuteCircular`](/docs/kcl-std/involuteCircular) * [`involuteCircular`](/docs/kcl-std/functions/std-sketch-involuteCircular)
* [`lastSegX`](/docs/kcl-std/lastSegX) * [`lastSegX`](/docs/kcl-std/functions/std-sketch-lastSegX)
* [`lastSegY`](/docs/kcl-std/lastSegY) * [`lastSegY`](/docs/kcl-std/functions/std-sketch-lastSegY)
* [`line`](/docs/kcl-std/line) * [`line`](/docs/kcl-std/functions/std-sketch-line)
* [`loft`](/docs/kcl-std/functions/std-sketch-loft) * [`loft`](/docs/kcl-std/functions/std-sketch-loft)
* [`patternCircular2d`](/docs/kcl-std/functions/std-sketch-patternCircular2d) * [`patternCircular2d`](/docs/kcl-std/functions/std-sketch-patternCircular2d)
* [`patternLinear2d`](/docs/kcl-std/functions/std-sketch-patternLinear2d) * [`patternLinear2d`](/docs/kcl-std/functions/std-sketch-patternLinear2d)
* [`patternTransform2d`](/docs/kcl-std/functions/std-sketch-patternTransform2d) * [`patternTransform2d`](/docs/kcl-std/functions/std-sketch-patternTransform2d)
* [`polygon`](/docs/kcl-std/functions/std-sketch-polygon) * [`polygon`](/docs/kcl-std/functions/std-sketch-polygon)
* [`profileStart`](/docs/kcl-std/profileStart) * [`profileStart`](/docs/kcl-std/functions/std-sketch-profileStart)
* [`profileStartX`](/docs/kcl-std/profileStartX) * [`profileStartX`](/docs/kcl-std/functions/std-sketch-profileStartX)
* [`profileStartY`](/docs/kcl-std/profileStartY) * [`profileStartY`](/docs/kcl-std/functions/std-sketch-profileStartY)
* [`revolve`](/docs/kcl-std/functions/std-sketch-revolve) * [`revolve`](/docs/kcl-std/functions/std-sketch-revolve)
* [`segAng`](/docs/kcl-std/segAng) * [`segAng`](/docs/kcl-std/functions/std-sketch-segAng)
* [`segEnd`](/docs/kcl-std/segEnd) * [`segEnd`](/docs/kcl-std/functions/std-sketch-segEnd)
* [`segEndX`](/docs/kcl-std/segEndX) * [`segEndX`](/docs/kcl-std/functions/std-sketch-segEndX)
* [`segEndY`](/docs/kcl-std/segEndY) * [`segEndY`](/docs/kcl-std/functions/std-sketch-segEndY)
* [`segLen`](/docs/kcl-std/segLen) * [`segLen`](/docs/kcl-std/functions/std-sketch-segLen)
* [`segStart`](/docs/kcl-std/segStart) * [`segStart`](/docs/kcl-std/functions/std-sketch-segStart)
* [`segStartX`](/docs/kcl-std/segStartX) * [`segStartX`](/docs/kcl-std/functions/std-sketch-segStartX)
* [`segStartY`](/docs/kcl-std/segStartY) * [`segStartY`](/docs/kcl-std/functions/std-sketch-segStartY)
* [`startProfile`](/docs/kcl-std/startProfile) * [`startProfile`](/docs/kcl-std/functions/std-sketch-startProfile)
* [`startSketchOn`](/docs/kcl-std/startSketchOn) * [`startSketchOn`](/docs/kcl-std/functions/std-sketch-startSketchOn)
* [`subtract2d`](/docs/kcl-std/subtract2d) * [`subtract2d`](/docs/kcl-std/functions/std-sketch-subtract2d)
* [`sweep`](/docs/kcl-std/functions/std-sketch-sweep) * [`sweep`](/docs/kcl-std/functions/std-sketch-sweep)
* [`tangentToEnd`](/docs/kcl-std/tangentToEnd) * [`tangentToEnd`](/docs/kcl-std/functions/std-sketch-tangentToEnd)
* [`tangentialArc`](/docs/kcl-std/tangentialArc) * [`tangentialArc`](/docs/kcl-std/functions/std-sketch-tangentialArc)
* [`xLine`](/docs/kcl-std/xLine) * [`xLine`](/docs/kcl-std/functions/std-sketch-xLine)
* [`yLine`](/docs/kcl-std/yLine) * [`yLine`](/docs/kcl-std/functions/std-sketch-yLine)

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,13 @@
--- ---
title: "any" title: "any"
subtitle: "Type in std::types" subtitle: "Type in std::types"
excerpt: "" excerpt: "The [`any`](/docs/kcl-std/types/std-types-any) type is the type of all possible values in KCL. I.e., if a function accepts an argument with type [`any`](/docs/kcl-std/types/std-types-any), then it can accept any value."
layout: manual layout: manual
--- ---
The [`any`](/docs/kcl-std/types/std-types-any) type is the type of all possible values in KCL. I.e., if a function accepts an argument with type [`any`](/docs/kcl-std/types/std-types-any), then it can accept any value.
The [`any`](/docs/kcl-std/types/std-types-any) type is the type of all possible values in KCL. I.e., if a function accepts an argument
with type [`any`](/docs/kcl-std/types/std-types-any), then it can accept any value.
### Examples ### Examples

View File

@ -534,7 +534,7 @@ profile001 = startProfile(sketch001, at = [-484.34, 484.95])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
` `
const targetURL = `?create-file&name=test&units=mm&code=${encodeURIComponent(btoa(code))}&ask-open-desktop` const targetURL = `?create-file=true&name=test&units=mm&code=${encodeURIComponent(btoa(code))}&ask-open-desktop=true`
await page.goto(page.url() + targetURL) await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL) expect(page.url()).toContain(targetURL)
}) })

View File

@ -964,8 +964,6 @@ a1 = startSketchOn(offsetPlane(XY, offset = 10))
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.keyboard.press('Enter') // accepting the auto complete, not a new line await page.keyboard.press('Enter') // accepting the auto complete, not a new line
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.keyboard.type('12') await page.keyboard.type('12')
@ -984,8 +982,6 @@ a1 = startSketchOn(offsetPlane(XY, offset = 10))
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// finish line with comment // finish line with comment
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.type('5') await page.keyboard.type('5')
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
@ -1001,8 +997,8 @@ a1 = startSketchOn(offsetPlane(XY, offset = 10))
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`@settings(defaultLengthUnit = in) `@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ) sketch001 = startSketchOn(XZ)
|> startProfile(%, at = [0, 12]) |> startProfile(at = [0, 12])
|> xLine(%, length = 5) // lin`.replaceAll('\n', '') |> xLine(length = 5) // lin`.replaceAll('\n', '')
) )
// expect there to be no KCL errors // expect there to be no KCL errors
@ -1040,8 +1036,6 @@ sketch001 = startSketchOn(XZ)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.keyboard.press('Tab') // accepting the auto complete, not a new line await page.keyboard.press('Tab') // accepting the auto complete, not a new line
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.keyboard.type('12') await page.keyboard.type('12')
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -1057,7 +1051,6 @@ sketch001 = startSketchOn(XZ)
await page.waitForTimeout(100) await page.waitForTimeout(100)
// press arrow down then tab to accept xLine // press arrow down then tab to accept xLine
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('Tab')
// finish line with comment // finish line with comment
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -1076,8 +1069,8 @@ sketch001 = startSketchOn(XZ)
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`@settings(defaultLengthUnit = in) `@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ) sketch001 = startSketchOn(XZ)
|> startProfile(%, at = [0, 12]) |> startProfile(at = [0, 12])
|> xLine(%, length = 5) // lin`.replaceAll('\n', '') |> xLine(length = 5) // lin`.replaceAll('\n', '')
) )
}) })
}) })

View File

@ -1766,7 +1766,7 @@ loft001 = loft([sketch001, sketch002])
sketch001 = startSketchOn(YZ) sketch001 = startSketchOn(YZ)
profile001 = circle(sketch001, center = [0, 0], radius = 500) profile001 = circle(sketch001, center = [0, 0], radius = 500)
sketch002 = startSketchOn(XZ) sketch002 = startSketchOn(XZ)
|> startProfile(at = [0, 0]) profile002 = startProfile(sketch002, at = [0, 0])
|> xLine(length = -500) |> xLine(length = -500)
|> tangentialArc(endAbsolute = [-2000, 500])`, |> tangentialArc(endAbsolute = [-2000, 500])`,
}, },
@ -1782,7 +1782,7 @@ profile001 = startProfile(sketch001, at = [-400, -400])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
sketch002 = startSketchOn(XZ) sketch002 = startSketchOn(XZ)
|> startProfile(at = [0, 0]) profile002 = startProfile(sketch002, at = [0, 0])
|> xLine(length = -500) |> xLine(length = -500)
|> tangentialArc(endAbsolute = [-2000, 500])`, |> tangentialArc(endAbsolute = [-2000, 500])`,
}, },
@ -1810,9 +1810,9 @@ sketch002 = startSketchOn(XZ)
testPoint.x - 50, testPoint.x - 50,
testPoint.y testPoint.y
) )
const sweepDeclaration = 'sweep001 = sweep(profile001, path = sketch002)' const sweepDeclaration = 'sweep001 = sweep(profile001, path = profile002)'
const editedSweepDeclaration = const editedSweepDeclaration =
'sweep001 = sweep(profile001, path = sketch002, sectional = true)' 'sweep001 = sweep(profile001, path = profile002, sectional = true)'
await test.step(`Look for sketch001`, async () => { await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code') await toolbar.closePane('code')

View File

@ -169,7 +169,8 @@ test(
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
const crypticErrorText = `The arg tag was given, but it was the wrong type` const crypticErrorText =
'tag requires a value with type `tag`, but found string'
await expect(page.getByText(crypticErrorText).first()).toBeVisible() await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared. // black pixel means the scene has been cleared.
@ -367,7 +368,8 @@ test(
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
const crypticErrorText = `The arg tag was given, but it was the wrong type` const crypticErrorText =
'tag requires a value with type `tag`, but found string'
await expect(page.getByText(crypticErrorText).first()).toBeVisible() await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared. // black pixel means the scene has been cleared.
@ -405,7 +407,8 @@ test(
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
const crypticErrorText = `The arg tag was given, but it was the wrong type` const crypticErrorText =
'tag requires a value with type `tag`, but found string'
await expect(page.getByText(crypticErrorText).first()).toBeVisible() await expect(page.getByText(crypticErrorText).first()).toBeVisible()
} }
) )

View File

@ -749,7 +749,7 @@ sketch001 = startSketchOn(XZ)
// expect the code to have changed // expect the code to have changed
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ) `sketch001 = startSketchOn(XZ)
|> startProfile(at = [8.41, -9.97]) |> startProfile(at = [8.41, -9.97])
|> line(end = [12.73, -0.09]) |> line(end = [12.73, -0.09])
|> line(end = [1.99, 2.06]) |> line(end = [1.99, 2.06])
|> tangentialArc(endAbsolute = [24.95, -5.38]) |> tangentialArc(endAbsolute = [24.95, -5.38])
@ -2329,16 +2329,18 @@ profile004 = circleThreePoint(sketch001, p1 = [13.44, -6.8], p2 = [13.39, -2.07]
await page.mouse.down() await page.mouse.down()
await rectDragTo() await rectDragTo()
await page.mouse.up() await page.mouse.up()
await page.waitForTimeout(200)
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`angledLine(angle = -7, length = 10.27, tag = $rectangleSegmentA001)` `angledLine(angle = -7, length = 10.27, tag = $rectangleSegmentA001)`
) )
}) })
await test.step('edit existing circl', async () => { await test.step('edit existing circle', async () => {
await circleEdge() await circleEdge()
await page.mouse.down() await page.mouse.down()
await dragCircleTo() await dragCircleTo()
await page.mouse.up() await page.mouse.up()
await page.waitForTimeout(200)
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`profile003 = circle(sketch001, center = [6.92, -4.2], radius = 4.81)` `profile003 = circle(sketch001, center = [6.92, -4.2], radius = 4.81)`
) )
@ -2349,6 +2351,7 @@ profile004 = circleThreePoint(sketch001, p1 = [13.44, -6.8], p2 = [13.39, -2.07]
await page.mouse.down() await page.mouse.down()
await circ3PEnd() await circ3PEnd()
await page.mouse.up() await page.mouse.up()
await page.waitForTimeout(200)
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`profile004 = circleThreePoint( `profile004 = circleThreePoint(
sketch001, sketch001,
@ -2362,7 +2365,7 @@ profile004 = circleThreePoint(sketch001, p1 = [13.44, -6.8], p2 = [13.39, -2.07]
await test.step('add new profile', async () => { await test.step('add new profile', async () => {
await toolbar.rectangleBtn.click() await toolbar.rectangleBtn.click()
await page.waitForTimeout(100) await page.waitForTimeout(200)
await rectStart() await rectStart()
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`profile005 = startProfile(sketch001, at = [15.68, -3.84])` `profile005 = startProfile(sketch001, at = [15.68, -3.84])`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -63,7 +63,7 @@ test.describe('Test network related behaviors', () => {
}) })
// Expect the network to be down // Expect the network to be down
await expect(networkToggle).toContainText('Problem') await expect(networkToggle).toContainText('Network health (Offline)')
// Click the network widget // Click the network widget
await networkWidget.click() await networkWidget.click()
@ -160,7 +160,8 @@ test.describe('Test network related behaviors', () => {
// Expect the network to be down // Expect the network to be down
await networkToggle.hover() await networkToggle.hover()
await expect(networkToggle).toContainText('Problem')
await expect(networkToggle).toContainText('Network health (Offline)')
// Ensure we are not in sketch mode // Ensure we are not in sketch mode
await expect( await expect(

View File

@ -364,11 +364,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
) )
} }
// Chrome devtools protocol session only works in Chromium
const browserType = page.context().browser()?.browserType().name()
const cdpSession =
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
const util = { const util = {
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page), waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
waitForPageLoad: () => waitForPageLoad(page), waitForPageLoad: () => waitForPageLoad(page),
@ -489,15 +484,9 @@ export async function getUtils(page: Page, test_?: typeof test) {
emulateNetworkConditions: async ( emulateNetworkConditions: async (
networkOptions: Protocol.Network.emulateNetworkConditionsParameters networkOptions: Protocol.Network.emulateNetworkConditionsParameters
) => { ) => {
if (cdpSession === null) { return networkOptions.offline
// Use a fail safe if we can't simulate disconnect (on Safari) ? page.evaluate('window.engineCommandManager.offline()')
return page.evaluate('window.engineCommandManager.tearDown()') : page.evaluate('window.engineCommandManager.online()')
}
return cdpSession?.send(
'Network.emulateNetworkConditions',
networkOptions
)
}, },
toNormalizedCode(text: string) { toNormalizedCode(text: string) {

View File

@ -3,187 +3,250 @@ import { uuidv4 } from '@src/lib/utils'
import { getUtils } from '@e2e/playwright/test-utils' import { getUtils } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test' import { expect, test } from '@e2e/playwright/zoo-test'
import type { Page } from '@playwright/test'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
test.describe('Testing Camera Movement', () => { test.describe('Testing Camera Movement', () => {
test('Can move camera reliably', async ({ /**
* hack that we're implemented our own retry instead of using retries built into playwright.
* however each of these camera drags can be flaky, because of udp
* and so putting them together means only one needs to fail to make this test extra flaky.
* this way we can retry within the test
* We could break them out into separate tests, but the longest past of the test is waiting
* for the stream to start, so it can be good to bundle related things together.
*/
const bakeInRetries = async ({
mouseActions,
afterPosition,
beforePosition,
retryCount = 0,
page, page,
context,
homePage,
scene, scene,
}: {
mouseActions: () => Promise<void>
beforePosition: [number, number, number]
afterPosition: [number, number, number]
retryCount?: number
page: Page
scene: SceneFixture
}) => { }) => {
const acceptableCamError = 5
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await test.step('Set up initial camera position', async () =>
await scene.connectionEstablished() await scene.moveCameraTo({
x: beforePosition[0],
y: beforePosition[1],
z: beforePosition[2],
}))
await u.openAndClearDebugPanel() await test.step('Do actions and watch for changes', async () =>
await u.closeKclCodePanel() u.doAndWaitForImageDiff(async () => {
const camPos: [number, number, number] = [0, 85, 85]
const bakeInRetries = async (
mouseActions: any,
xyz: [number, number, number],
cnt = 0
) => {
// hack that we're implemented our own retry instead of using retries built into playwright.
// however each of these camera drags can be flaky, because of udp
// and so putting them together means only one needs to fail to make this test extra flaky.
// this way we can retry within the test
// We could break them out into separate tests, but the longest past of the test is waiting
// for the stream to start, so it can be good to bundle related things together.
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: camPos[0], y: camPos[1], z: camPos[2] },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
// rotate
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// const yo = page.getByTestId('cam-x-position').inputValue()
await u.doAndWaitForImageDiff(async () => {
await mouseActions() await mouseActions()
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.closeDebugPanel() await u.closeDebugPanel()
await page.waitForTimeout(100) await page.waitForTimeout(100)
}, 300) }, 300))
await u.openAndClearDebugPanel()
await expect(page.getByTestId('cam-x-position')).toBeAttached()
const vals = await Promise.all([
page.getByTestId('cam-x-position').inputValue(),
page.getByTestId('cam-y-position').inputValue(),
page.getByTestId('cam-z-position').inputValue(),
])
const errors = vals.map((v, i) => Math.abs(Number(v) - afterPosition[i]))
let shouldRetry = false
if (errors.some((e) => e > acceptableCamError)) {
if (retryCount > 2) {
console.log('xVal', vals[0], 'xError', errors[0])
console.log('yVal', vals[1], 'yError', errors[1])
console.log('zVal', vals[2], 'zError', errors[2])
throw new Error('Camera position not as expected', {
cause: {
vals,
errors,
},
})
}
shouldRetry = true
}
if (shouldRetry) {
await bakeInRetries({
mouseActions,
afterPosition: afterPosition,
beforePosition: beforePosition,
retryCount: retryCount + 1,
page,
scene,
})
}
}
test(
'Can pan and zoom camera reliably',
{
tag: '@web',
},
async ({ page, homePage, scene, cmdBar }) => {
const u = await getUtils(page)
const camInitialPosition: [number, number, number] = [0, 85, 85]
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await page.getByTestId('cam-x-position').isVisible() await u.closeKclCodePanel()
const vals = await Promise.all([ await test.step('Pan', async () => {
page.getByTestId('cam-x-position').inputValue(), await bakeInRetries({
page.getByTestId('cam-y-position').inputValue(), mouseActions: async () => {
page.getByTestId('cam-z-position').inputValue(), await page.keyboard.down('Shift')
]) await page.mouse.move(600, 200)
const xError = Math.abs(Number(vals[0]) + xyz[0]) await page.mouse.down({ button: 'right' })
const yError = Math.abs(Number(vals[1]) + xyz[1]) // Gotcha: remove steps:2 from this 700,200 mouse move. This bricked the test on local host engine.
const zError = Math.abs(Number(vals[2]) + xyz[2]) await page.mouse.move(700, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
await page.waitForTimeout(200)
},
afterPosition: [19, 85, 85],
beforePosition: camInitialPosition,
page,
scene,
})
})
let shouldRetry = false await test.step('Zoom with click and drag', async () => {
await bakeInRetries({
mouseActions: async () => {
await page.keyboard.down('Control')
await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 300)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
},
afterPosition: [0, 118, 118],
beforePosition: camInitialPosition,
page,
scene,
})
})
if (xError > 5 || yError > 5 || zError > 5) { await test.step('Zoom with scrollwheel', async () => {
if (cnt > 2) { const refreshCamValuesCmd: EngineCommand = {
console.log('xVal', vals[0], 'xError', xError) type: 'modeling_cmd_req',
console.log('yVal', vals[1], 'yError', yError) cmd_id: uuidv4(),
console.log('zVal', vals[2], 'zError', zError) cmd: {
type: 'default_camera_get_settings',
throw new Error('Camera position not as expected') },
} }
shouldRetry = true await bakeInRetries({
} mouseActions: async () => {
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.mouse.move(700, 400)
await page.waitForTimeout(100) await page.mouse.wheel(0, -150)
if (shouldRetry) await bakeInRetries(mouseActions, xyz, cnt + 1)
// Scroll zooming doesn't update the debug pane's cam position values,
// so we have to force a refresh.
await u.openAndClearDebugPanel()
await u.sendCustomCmd(refreshCamValuesCmd)
await u.waitForCmdReceive('default_camera_get_settings')
await u.closeDebugPanel()
},
afterPosition: [0, 42.5, 42.5],
beforePosition: camInitialPosition,
page,
scene,
})
})
} }
await bakeInRetries(async () => { )
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.waitForTimeout(100)
const appLogoBBox = await page.getByTestId('app-logo').boundingBox() test(
expect(appLogoBBox).not.toBeNull() 'Can orbit camera reliably',
if (!appLogoBBox) throw new Error('app logo not found') {
await page.mouse.move( tag: '@web',
appLogoBBox.x + appLogoBBox.width / 2, },
appLogoBBox.y + appLogoBBox.height / 2 async ({ page, homePage, scene, cmdBar }) => {
) const u = await getUtils(page)
await page.waitForTimeout(100) const initialCamPosition: [number, number, number] = [0, 85, 85]
await page.mouse.move(600, 303)
await page.waitForTimeout(100)
await page.mouse.up({ button: 'right' })
}, [4, -10.5, -120])
await bakeInRetries(async () => { await homePage.goToModelingScene()
await page.keyboard.down('Shift') // this turns on the debug pane setting as well
await page.mouse.move(600, 200) await scene.settled(cmdBar)
await page.mouse.down({ button: 'right' })
// Gotcha: remove steps:2 from this 700,200 mouse move. This bricked the test on local host engine.
await page.mouse.move(700, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
}, [-19, -85, -85])
const camCommand: EngineCommand = { await u.openAndClearDebugPanel()
type: 'modeling_cmd_req', await u.closeKclCodePanel()
cmd_id: uuidv4(),
cmd: { await test.step('Test orbit with spherical mode', async () => {
type: 'default_camera_look_at', await bakeInRetries({
center: { x: 0, y: 0, z: 0 }, mouseActions: async () => {
vantage: { x: camPos[0], y: camPos[1], z: camPos[2] }, await page.mouse.move(700, 200)
up: { x: 0, y: 0, z: 1 }, await page.mouse.down({ button: 'right' })
}, await page.waitForTimeout(100)
const appLogoBBox = await page.getByTestId('app-logo').boundingBox()
expect(appLogoBBox).not.toBeNull()
if (!appLogoBBox) throw new Error('app logo not found')
await page.mouse.move(
appLogoBBox.x + appLogoBBox.width / 2,
appLogoBBox.y + appLogoBBox.height / 2
)
await page.waitForTimeout(100)
await page.mouse.move(600, 303)
await page.waitForTimeout(100)
await page.mouse.up({ button: 'right' })
},
afterPosition: [-4, 10.5, 120],
beforePosition: initialCamPosition,
page,
scene,
})
})
await test.step('Test orbit with trackball mode', async () => {
await test.step('Set orbitMode to trackball', async () => {
await cmdBar.openCmdBar()
await cmdBar.selectOption({ name: 'camera orbit' }).click()
await cmdBar.selectOption({ name: 'trackball' }).click()
await expect(
page.getByText(`camera orbit to "trackball"`)
).toBeVisible()
})
await bakeInRetries({
mouseActions: async () => {
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.waitForTimeout(100)
const appLogoBBox = await page.getByTestId('app-logo').boundingBox()
expect(appLogoBBox).not.toBeNull()
if (!appLogoBBox) {
throw new Error('app logo not found')
}
await page.mouse.move(
appLogoBBox.x + appLogoBBox.width / 2,
appLogoBBox.y + appLogoBBox.height / 2
)
await page.waitForTimeout(100)
await page.mouse.move(600, 303)
await page.waitForTimeout(100)
await page.mouse.up({ button: 'right' })
},
afterPosition: [18.06, -42.79, 110.87],
beforePosition: initialCamPosition,
page,
scene,
})
})
} }
const updateCamCommand: EngineCommand = { )
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
await u.clearCommandLogs()
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(200)
// zoom
await u.doAndWaitForImageDiff(async () => {
await page.keyboard.down('Control')
await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 300)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
await u.openDebugPanel()
await page.waitForTimeout(300)
await u.clearCommandLogs()
await u.closeDebugPanel()
}, 300)
// zoom with scroll
await u.openAndClearDebugPanel()
// TODO, it appears we don't get the cam setting back from the engine when the interaction is zoom into `backInRetries` once the information is sent back on zoom
// await expect(Math.abs(Number(await page.getByTestId('cam-x-position').inputValue()) + 12)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-y-position').inputValue()) - 85)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-z-position').inputValue()) - 85)).toBeLessThan(1.5)
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await bakeInRetries(async () => {
await page.mouse.move(700, 400)
await page.mouse.wheel(0, -100)
}, [0, -85, -85])
})
// TODO: fix after electron migration is merged // TODO: fix after electron migration is merged
test('Zoom should be consistent when exiting or entering sketches', async ({ test('Zoom should be consistent when exiting or entering sketches', async ({

View File

@ -133,9 +133,9 @@
"test:unit": "vitest run --mode development --exclude **/jest-component-unit-tests/*", "test:unit": "vitest run --mode development --exclude **/jest-component-unit-tests/*",
"test:unit:components": "jest -c jest-component-unit-tests/jest.config.ts --rootDir jest-component-unit-tests/", "test:unit:components": "jest -c jest-component-unit-tests/jest.config.ts --rootDir jest-component-unit-tests/",
"test:e2e:web": "cross-env TARGET=web NODE_ENV=development playwright test --config=playwright.config.ts --project=\"Google Chrome\" --grep=@web", "test:e2e:web": "cross-env TARGET=web NODE_ENV=development playwright test --config=playwright.config.ts --project=\"Google Chrome\" --grep=@web",
"test:e2e:desktop": "cross-env TARGET=desktop playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot", "test:e2e:desktop": "cross-env TARGET=desktop playwright test --config=playwright.electron.config.ts --grep-invert=\"@snapshot|@web\"",
"test:e2e:desktop:local": "cross-env TARGET=desktop npm run tronb:vite:dev && playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot --grep-invert=\"$(curl --silent https://test-analysis-bot.hawk-dinosaur.ts.net/projects/KittyCAD/modeling-app/tests/disabled/regex)\"", "test:e2e:desktop:local": "cross-env TARGET=desktop npm run tronb:vite:dev && playwright test --config=playwright.electron.config.ts --grep-invert=\"@snapshot|@web\" --grep-invert=\"$(curl --silent https://test-analysis-bot.hawk-dinosaur.ts.net/projects/KittyCAD/modeling-app/tests/disabled/regex)\"",
"test:e2e:desktop:local-engine": "cross-env TARGET=desktop npm run tronb:vite:dev && playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot|@skipLocalEngine' --grep-invert=\"$(curl --silent https://test-analysis-bot.hawk-dinosaur.ts.net/projects/KittyCAD/modeling-app/tests/disabled/regex)\"", "test:e2e:desktop:local-engine": "cross-env TARGET=desktop npm run tronb:vite:dev && playwright test --config=playwright.electron.config.ts --grep-invert=\"@snapshot|@web|@skipLocalEngine\" --grep-invert=\"$(curl --silent https://test-analysis-bot.hawk-dinosaur.ts.net/projects/KittyCAD/modeling-app/tests/disabled/regex)\"",
"test:unit:local": "npm run simpleserver:bg && npm run test:unit; kill-port 3000" "test:unit:local": "npm run simpleserver:bg && npm run test:unit; kill-port 3000"
}, },
"browserslist": { "browserslist": {

View File

@ -11,24 +11,20 @@ filletRadius = 0.5
plateThickness = .5 plateThickness = .5
centerHoleDiameter = 2 centerHoleDiameter = 2
// Create a function that defines the body width and length of the mounting plate. Tag the corners so they can be passed through the fillet function.
fn rectShape(pos, w, l) {
rr = startSketchOn(XY)
|> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
|> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
|> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
|> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
|> close(tag = $edge4)
return rr
}
// Define the hole radius and x, y location constants // Define the hole radius and x, y location constants
holeRadius = .25 holeRadius = .25
holeIndex = .75 holeIndex = .75
sketch001 = startSketchOn(XY)
rectShape = startProfile(sketch001, at = [-plateWidth / 2, plateLength / 2])
|> angledLine(angle = 0, length = plateWidth, tag = $basePlateEdge1)
|> angledLine(angle = segAng(basePlateEdge1) - 90, length = plateLength, tag = $basePlateEdge2)
|> angledLine(angle = segAng(basePlateEdge1), length = -segLen(basePlateEdge1), tag = $basePlateEdge3)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $basePlateEdge4)
|> close()
// Create the mounting plate extrusion, holes, and fillets // Create the mounting plate extrusion, holes, and fillets
rs = rectShape(pos = [0, 0], w = plateWidth, l = plateLength) part = rectShape
part = rs
|> subtract2d(tool = circle( |> subtract2d(tool = circle(
center = [ center = [
-plateWidth / 2 + holeIndex, -plateWidth / 2 + holeIndex,
@ -62,9 +58,9 @@ part = rs
|> fillet( |> fillet(
radius = filletRadius, radius = filletRadius,
tags = [ tags = [
getPreviousAdjacentEdge(rs.tags.edge1), getCommonEdge(faces = [basePlateEdge3, basePlateEdge2]),
getPreviousAdjacentEdge(rs.tags.edge2), getCommonEdge(faces = [basePlateEdge4, basePlateEdge3]),
getPreviousAdjacentEdge(rs.tags.edge3), getCommonEdge(faces = [basePlateEdge4, basePlateEdge1]),
getPreviousAdjacentEdge(rs.tags.edge4) getCommonEdge(faces = [basePlateEdge2, basePlateEdge1])
], ],
) )

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

114
rust/Cargo.lock generated
View File

@ -2,16 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
dependencies = [
"lazy_static",
"regex",
]
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.24.2" version = "0.24.2"
@ -627,26 +617,22 @@ dependencies = [
[[package]] [[package]]
name = "criterion" name = "criterion"
version = "0.5.1" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
dependencies = [ dependencies = [
"anes", "anes",
"cast", "cast",
"ciborium", "ciborium",
"clap", "clap",
"criterion-plot", "criterion-plot",
"futures", "itertools 0.13.0",
"is-terminal",
"itertools 0.10.5",
"num-traits 0.2.19", "num-traits 0.2.19",
"once_cell",
"oorandom", "oorandom",
"plotters", "plotters",
"rayon", "rayon",
"regex", "regex",
"serde", "serde",
"serde_derive",
"serde_json", "serde_json",
"tinytemplate", "tinytemplate",
"tokio", "tokio",
@ -715,9 +701,9 @@ dependencies = [
[[package]] [[package]]
name = "csscolorparser" name = "csscolorparser"
version = "0.7.0" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f9a16a848a7fb95dd47ce387ac1ee9a6df879ba784b815537fcd388a1a8288" checksum = "5fda6aace1fbef3aa217b27f4c8d7d071ef2a70a5ca51050b1f17d40299d3f16"
dependencies = [ dependencies = [
"phf", "phf",
] ]
@ -1311,15 +1297,6 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "home"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.12" version = "0.2.12"
@ -1815,7 +1792,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-bumper" name = "kcl-bumper"
version = "0.1.78" version = "0.1.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1826,26 +1803,16 @@ dependencies = [
[[package]] [[package]]
name = "kcl-derive-docs" name = "kcl-derive-docs"
version = "0.1.78" version = "0.1.80"
dependencies = [ dependencies = [
"Inflector",
"anyhow",
"convert_case",
"expectorate",
"once_cell",
"pretty_assertions",
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex",
"rustfmt-wrapper",
"serde",
"serde_tokenstream",
"syn 2.0.100", "syn 2.0.100",
] ]
[[package]] [[package]]
name = "kcl-directory-test-macro" name = "kcl-directory-test-macro"
version = "0.1.78" version = "0.1.80"
dependencies = [ dependencies = [
"convert_case", "convert_case",
"proc-macro2", "proc-macro2",
@ -1855,7 +1822,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-language-server" name = "kcl-language-server"
version = "0.2.78" version = "0.2.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1876,7 +1843,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-language-server-release" name = "kcl-language-server-release"
version = "0.1.78" version = "0.1.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1896,7 +1863,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.78" version = "0.2.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",
@ -1973,7 +1940,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-python-bindings" name = "kcl-python-bindings"
version = "0.3.78" version = "0.3.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"kcl-lib", "kcl-lib",
@ -1988,7 +1955,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.78" version = "0.1.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hyper 0.14.32", "hyper 0.14.32",
@ -2001,7 +1968,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-to-core" name = "kcl-to-core"
version = "0.1.78" version = "0.1.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -2015,7 +1982,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-wasm-lib" name = "kcl-wasm-lib"
version = "0.1.78" version = "0.1.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bson", "bson",
@ -3354,19 +3321,6 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustfmt-wrapper"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1adc9dfed5cc999077978cc7163b9282c5751c8d39827c4ea8c8c220ca5a440"
dependencies = [
"serde",
"tempfile",
"thiserror 1.0.69",
"toml",
"toolchain_find",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.0.2" version = "1.0.2"
@ -3613,18 +3567,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_tokenstream"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64060d864397305347a78851c51588fd283767e7e7589829e8121d65512340f1"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.100",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -4200,9 +4142,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.24.0" version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
@ -4261,19 +4203,6 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "toolchain_find"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc8c9a7f0a2966e1acdaf0461023d0b01471eeead645370cf4c3f5cff153f2a"
dependencies = [
"home",
"once_cell",
"regex",
"semver",
"walkdir",
]
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.4.13" version = "0.4.13"
@ -4456,21 +4385,20 @@ dependencies = [
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.24.0" version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [ dependencies = [
"byteorder",
"bytes", "bytes",
"data-encoding", "data-encoding",
"http 1.3.1", "http 1.3.1",
"httparse", "httparse",
"log", "log",
"rand 0.8.5", "rand 0.9.0",
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"sha1", "sha1",
"thiserror 1.0.69", "thiserror 2.0.12",
"utf-8", "utf-8",
] ]

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-bumper" name = "kcl-bumper"
version = "0.1.78" version = "0.1.80"
edition = "2021" edition = "2021"
repository = "https://github.com/KittyCAD/modeling-api" repository = "https://github.com/KittyCAD/modeling-api"
rust-version = "1.76" rust-version = "1.76"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-derive-docs" name = "kcl-derive-docs"
description = "A tool for generating documentation from Rust derive macros" description = "A tool for generating documentation from Rust derive macros"
version = "0.1.78" version = "0.1.80"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -12,21 +12,9 @@ proc-macro = true
bench = false bench = false
[dependencies] [dependencies]
Inflector = "0.11.4"
convert_case = "0.8.0"
once_cell = "1.21.3"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
regex = "1.11"
serde = { workspace = true }
serde_tokenstream = "0.2"
syn = { version = "2.0.96", features = ["full"] } syn = { version = "2.0.96", features = ["full"] }
[dev-dependencies]
anyhow = { workspace = true }
expectorate = "1.1.0"
pretty_assertions = "1.4.1"
rustfmt-wrapper = "0.2.1"
[lints] [lints]
workspace = true workspace = true

View File

@ -134,6 +134,50 @@ pub const TEST_NAMES: &[&str] = &[
"std-sketch-patternLinear2d-1", "std-sketch-patternLinear2d-1",
"std-sketch-patternCircular2d-0", "std-sketch-patternCircular2d-0",
"std-sketch-circleThreePoint-0", "std-sketch-circleThreePoint-0",
"std-sketch-segStart-0",
"std-sketch-segStartX-0",
"std-sketch-segStartY-0",
"std-sketch-segEnd-0",
"std-sketch-segEndX-0",
"std-sketch-segEndY-0",
"std-sketch-lastSegX-0",
"std-sketch-lastSegY-0",
"std-sketch-segAng-0",
"std-sketch-segLen-0",
"std-sketch-tangentToEnd-0",
"std-sketch-tangentToEnd-1",
"std-sketch-tangentToEnd-2",
"std-sketch-tangentToEnd-3",
"std-sketch-tangentToEnd-4",
"std-sketch-profileStart-0",
"std-sketch-profileStartX-0",
"std-sketch-profileStartY-0",
"std-sketch-startProfile-0",
"std-sketch-startProfile-1",
"std-sketch-startProfile-2",
"std-sketch-startSketchOn-0",
"std-sketch-startSketchOn-1",
"std-sketch-startSketchOn-2",
"std-sketch-startSketchOn-3",
"std-sketch-startSketchOn-4",
"std-sketch-startSketchOn-5",
"std-sketch-angledLineThatIntersects-0",
"std-sketch-angledLine-0",
"std-sketch-arc-0",
"std-sketch-arc-1",
"std-sketch-bezierCurve-0",
"std-sketch-bezierCurve-1",
"std-sketch-close-0",
"std-sketch-close-1",
"std-sketch-involuteCircular-0",
"std-sketch-line-0",
"std-sketch-subtract2d-0",
"std-sketch-subtract2d-1",
"std-sketch-tangentialArc-0",
"std-sketch-tangentialArc-1",
"std-sketch-tangentialArc-2",
"std-sketch-xLine-0",
"std-sketch-yLine-0",
"std-solid-appearance-0", "std-solid-appearance-0",
"std-solid-appearance-1", "std-solid-appearance-1",
"std-solid-appearance-2", "std-solid-appearance-2",

View File

@ -3,29 +3,6 @@
#![allow(clippy::style)] #![allow(clippy::style)]
mod example_tests; mod example_tests;
#[cfg(test)]
mod tests;
mod unbox;
use std::collections::HashMap;
use convert_case::Casing;
use inflector::{cases::camelcase::to_camel_case, Inflector};
use once_cell::sync::Lazy;
use quote::{format_ident, quote, quote_spanned, ToTokens};
use regex::Regex;
use serde::Deserialize;
use serde_tokenstream::{from_tokenstream, Error};
use syn::{
parse::{Parse, ParseStream},
Attribute, Signature, Visibility,
};
use unbox::unbox;
#[proc_macro_attribute]
pub fn stdlib(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
do_output(do_stdlib(attr.into(), item.into()))
}
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn for_each_example_test(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn for_each_example_test(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
@ -36,845 +13,3 @@ pub fn for_each_example_test(_attr: proc_macro::TokenStream, item: proc_macro::T
pub fn for_all_example_test(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn for_all_example_test(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
example_tests::do_for_all_example_test(item.into()).into() example_tests::do_for_all_example_test(item.into()).into()
} }
/// Describes an argument of a stdlib function.
#[derive(Deserialize, Debug)]
struct ArgMetadata {
/// Docs for the argument.
docs: String,
/// If this argument is optional, it should still be included in completion snippets.
/// Does not do anything if the argument is already required.
#[serde(default)]
include_in_snippet: bool,
/// The snippet should suggest this value for the arg.
#[serde(default)]
snippet_value: Option<String>,
/// The snippet should suggest this value for the arg.
#[serde(default)]
snippet_value_array: Option<Vec<String>>,
}
#[derive(Deserialize, Debug)]
struct StdlibMetadata {
/// The name of the function in the API.
name: String,
/// Tags for the function.
#[serde(default)]
tags: Vec<String>,
/// Whether the function is unpublished.
/// Then docs will not be generated.
#[serde(default)]
unpublished: bool,
/// Whether the function is deprecated.
/// Then specific docs detailing that this is deprecated will be generated.
#[serde(default)]
deprecated: bool,
/// Whether the function is displayed in the feature tree.
/// If true, calls to the function will be available for display.
/// If false, calls to the function will never be displayed.
#[serde(default)]
feature_tree_operation: bool,
/// If true, the first argument is unlabeled.
/// If false, all arguments require labels.
#[serde(default)]
unlabeled_first: bool,
/// Key = argument name, value = argument doc.
#[serde(default)]
args: HashMap<String, ArgMetadata>,
}
fn do_stdlib(
attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
let metadata = from_tokenstream(&attr)?;
do_stdlib_inner(metadata, attr, item)
}
fn do_output(res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>) -> proc_macro::TokenStream {
match res {
Err(err) => err.to_compile_error().into(),
Ok((stdlib_docs, errors)) => {
let compiler_errors = errors.iter().map(|err| err.to_compile_error());
let output = quote! {
#stdlib_docs
#( #compiler_errors )*
};
output.into()
}
}
}
fn do_stdlib_inner(
metadata: StdlibMetadata,
_attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
let ast: ItemFnForSignature = syn::parse2(item.clone())?;
let mut errors = Vec::new();
if ast.sig.constness.is_some() {
errors.push(Error::new_spanned(
&ast.sig.constness,
"stdlib functions may not be const functions",
));
}
if ast.sig.unsafety.is_some() {
errors.push(Error::new_spanned(
&ast.sig.unsafety,
"stdlib functions may not be unsafe",
));
}
if ast.sig.abi.is_some() {
errors.push(Error::new_spanned(
&ast.sig.abi,
"stdlib functions may not use an alternate ABI",
));
}
if !ast.sig.generics.params.is_empty() {
if ast.sig.generics.params.iter().any(|generic_type| match generic_type {
syn::GenericParam::Lifetime(_) => false,
syn::GenericParam::Type(_) => true,
syn::GenericParam::Const(_) => true,
}) {
errors.push(Error::new_spanned(
&ast.sig.generics,
"Stdlib functions may not be generic over types or constants, only lifetimes.",
));
}
}
if ast.sig.variadic.is_some() {
errors.push(Error::new_spanned(&ast.sig.variadic, "no language C here"));
}
let name = metadata.name;
// Fail if the name is not camel case.
// Remove some known suffix exceptions first.
let name_cleaned = name.strip_suffix("2d").unwrap_or(name.as_str());
let name_cleaned = name.strip_suffix("3d").unwrap_or(name_cleaned);
if !name_cleaned.is_camel_case() {
errors.push(Error::new_spanned(
&ast.sig.ident,
format!("stdlib function names must be in camel case: `{}`", name),
));
}
let name_ident = format_ident!("{}", name.to_case(convert_case::Case::UpperCamel));
let name_str = name.to_string();
let fn_name = &ast.sig.ident;
let fn_name_str = fn_name.to_string().replace("inner_", "");
let fn_name_ident = format_ident!("{}", fn_name_str);
let boxed_fn_name_ident = format_ident!("boxed_{}", fn_name_str);
let _visibility = &ast.vis;
let doc_info = extract_doc_from_attrs(&ast.attrs);
let comment_text = {
let mut buf = String::new();
buf.push_str("Std lib function: ");
buf.push_str(&name_str);
if let Some(s) = &doc_info.summary {
buf.push_str("\n");
buf.push_str(&s);
}
if let Some(s) = &doc_info.description {
buf.push_str("\n");
buf.push_str(&s);
}
buf
};
let description_doc_comment = quote! {
#[doc = #comment_text]
};
let summary = if let Some(summary) = doc_info.summary {
quote! { #summary }
} else {
quote! { "" }
};
let description = if let Some(description) = doc_info.description {
quote! { #description }
} else {
quote! { "" }
};
if doc_info.code_blocks.is_empty() {
errors.push(Error::new_spanned(
&ast.sig,
"stdlib functions must have at least one code block",
));
}
// Make sure the function name is in all the code blocks.
for code_block in doc_info.code_blocks.iter() {
if !code_block.0.contains(&name) {
errors.push(Error::new_spanned(
&ast.sig,
format!(
"stdlib functions must have the function name `{}` in the code block",
name
),
));
}
}
let test_code_blocks = doc_info
.code_blocks
.iter()
.enumerate()
.map(|(index, (code_block, norun))| {
if !norun {
generate_code_block_test(&fn_name_str, code_block, index)
} else {
quote! {}
}
})
.collect::<Vec<_>>();
let (cb, norun): (Vec<_>, Vec<_>) = doc_info.code_blocks.into_iter().unzip();
let code_blocks = quote! {
let code_blocks = vec![#(#cb),*];
let norun = vec![#(#norun),*];
code_blocks.iter().zip(norun).map(|(cb, norun)| {
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
(program.ast.recast(&options, 0), norun)
}).collect::<Vec<(String, bool)>>()
};
let tags = metadata
.tags
.iter()
.map(|tag| {
quote! { #tag.to_string() }
})
.collect::<Vec<_>>();
let deprecated = if metadata.deprecated {
quote! { true }
} else {
quote! { false }
};
let unpublished = if metadata.unpublished {
quote! { true }
} else {
quote! { false }
};
let feature_tree_operation = if metadata.feature_tree_operation {
quote! { true }
} else {
quote! { false }
};
let docs_crate = get_crate(None);
// When the user attaches this proc macro to a function with the wrong type
// signature, the resulting errors can be deeply inscrutable. To attempt to
// make failures easier to understand, we inject code that asserts the types
// of the various parameters. We do this by calling dummy functions that
// require a type that satisfies SharedExtractor or ExclusiveExtractor.
let mut arg_types = Vec::new();
for (i, arg) in ast.sig.inputs.iter().enumerate() {
// Get the name of the argument.
let arg_name = match arg {
syn::FnArg::Receiver(pat) => {
let span = pat.self_token.span.unwrap();
span.source_text().unwrap().to_string()
}
syn::FnArg::Typed(pat) => match &*pat.pat {
syn::Pat::Ident(ident) => ident.ident.to_string(),
_ => {
errors.push(Error::new_spanned(
&pat.pat,
"stdlib functions may not use destructuring patterns",
));
continue;
}
},
}
.trim_start_matches('_')
.to_string();
// These aren't really KCL args, they're just state that each stdlib function's impl needs.
if arg_name == "exec_state" || arg_name == "args" {
continue;
}
let ty = match arg {
syn::FnArg::Receiver(pat) => pat.ty.as_ref().into_token_stream(),
syn::FnArg::Typed(pat) => pat.ty.as_ref().into_token_stream(),
};
let (ty_string, ty_ident) = clean_ty_string(ty.to_string().as_str());
let ty_string = rust_type_to_openapi_type(&ty_string);
let required = !ty_ident.to_string().starts_with("Option <");
let Some(arg_meta) = metadata.args.get(&arg_name) else {
errors.push(Error::new_spanned(arg, format!("arg {arg_name} not found")));
continue;
};
let description = arg_meta.docs.clone();
let include_in_snippet = required || arg_meta.include_in_snippet;
let snippet_value = arg_meta.snippet_value.clone();
let snippet_value_array = arg_meta.snippet_value_array.clone();
if snippet_value.is_some() && snippet_value_array.is_some() {
errors.push(Error::new_spanned(arg, format!("arg {arg_name} has set both snippet_value and snippet_value array, but at most one of these may be set. Please delete one of them.")));
}
let label_required = !(i == 0 && metadata.unlabeled_first);
let camel_case_arg_name = to_camel_case(&arg_name);
if ty_string != "ExecState" && ty_string != "Args" {
let schema = quote! {
generator.root_schema_for::<#ty_ident>()
};
let q0 = quote! {
name: #camel_case_arg_name.to_string(),
type_: #ty_string.to_string(),
schema: #schema,
required: #required,
label_required: #label_required,
description: #description.to_string(),
include_in_snippet: #include_in_snippet,
};
let q1 = if let Some(snippet_value) = snippet_value {
quote! {
snippet_value: Some(#snippet_value.to_owned()),
}
} else {
quote! {
snippet_value: None,
}
};
let q2 = if let Some(snippet_value_array) = snippet_value_array {
quote! {
snippet_value_array: Some(vec![
#(#snippet_value_array.to_owned()),*
]),
}
} else {
quote! {
snippet_value_array: None,
}
};
arg_types.push(quote! {
#docs_crate::StdLibFnArg {
#q0
#q1
#q2
}
});
}
}
let return_type_inner = match &ast.sig.output {
syn::ReturnType::Default => quote! { () },
syn::ReturnType::Type(_, ty) => {
// Get the inside of the result.
match &**ty {
syn::Type::Path(syn::TypePath { path, .. }) => {
let path = &path.segments;
if path.len() == 1 {
let seg = &path[0];
if seg.ident == "Result" {
if let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
args,
..
}) = &seg.arguments
{
if args.len() == 2 || args.len() == 1 {
let mut args = args.iter();
let ok = args.next().unwrap();
if let syn::GenericArgument::Type(ty) = ok {
let ty = unbox(ty.clone());
quote! { #ty }
} else {
quote! { () }
}
} else {
quote! { () }
}
} else {
quote! { () }
}
} else {
let ty = unbox(*ty.clone());
quote! { #ty }
}
} else {
quote! { () }
}
}
_ => {
quote! { () }
}
}
}
};
let ret_ty_string = return_type_inner.to_string().replace(' ', "");
let return_type = if !ret_ty_string.is_empty() || ret_ty_string != "()" {
let ret_ty_string = rust_type_to_openapi_type(&ret_ty_string);
quote! {
let schema = generator.root_schema_for::<#return_type_inner>();
Some(#docs_crate::StdLibFnArg {
name: "".to_string(),
type_: #ret_ty_string.to_string(),
schema,
required: true,
label_required: true,
description: String::new(),
include_in_snippet: true,
snippet_value: None,
snippet_value_array: None,
})
}
} else {
quote! {
None
}
};
// For reasons that are not well understood unused constants that use the
// (default) call_site() Span do not trigger the dead_code lint. Because
// defining but not using an endpoint is likely a programming error, we
// want to be sure to have the compiler flag this. We force this by using
// the span from the name of the function to which this macro was applied.
let span = ast.sig.ident.span();
let const_struct = quote_spanned! {span=>
pub(crate) const #name_ident: #name_ident = #name_ident {};
};
let test_mod_name = format_ident!("test_examples_{}", fn_name_str);
// The final TokenStream returned will have a few components that reference
// `#name_ident`, the name of the function to which this macro was applied...
let stream = quote! {
#[cfg(test)]
mod #test_mod_name {
#(#test_code_blocks)*
}
// ... a struct type called `#name_ident` that has no members
#[allow(non_camel_case_types, missing_docs)]
#description_doc_comment
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars::JsonSchema, ts_rs::TS)]
#[ts(export)]
pub(crate) struct #name_ident {}
// ... a constant of type `#name` whose identifier is also #name_ident
#[allow(non_upper_case_globals, missing_docs)]
#description_doc_comment
#const_struct
fn #boxed_fn_name_ident(
exec_state: &mut crate::execution::ExecState,
args: crate::std::Args,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>> + Send + '_>,
> {
Box::pin(#fn_name_ident(exec_state, args))
}
impl #docs_crate::StdLibFn for #name_ident
{
fn name(&self) -> String {
#name_str.to_string()
}
fn summary(&self) -> String {
#summary.to_string()
}
fn description(&self) -> String {
#description.to_string()
}
fn tags(&self) -> Vec<String> {
vec![#(#tags),*]
}
fn args(&self, inline_subschemas: bool) -> Vec<#docs_crate::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
// We set this to false so we can recurse them later.
settings.inline_subschemas = inline_subschemas;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
vec![#(#arg_types),*]
}
fn return_value(&self, inline_subschemas: bool) -> Option<#docs_crate::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
// We set this to false so we can recurse them later.
settings.inline_subschemas = inline_subschemas;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
#return_type
}
fn unpublished(&self) -> bool {
#unpublished
}
fn deprecated(&self) -> bool {
#deprecated
}
fn feature_tree_operation(&self) -> bool {
#feature_tree_operation
}
fn examples(&self) -> Vec<(String, bool)> {
#code_blocks
}
fn std_lib_fn(&self) -> crate::std::StdFn {
#boxed_fn_name_ident
}
fn clone_box(&self) -> Box<dyn #docs_crate::StdLibFn> {
Box::new(self.clone())
}
}
#item
};
// Prepend the usage message if any errors were detected.
if !errors.is_empty() {
errors.insert(0, Error::new_spanned(&ast.sig, ""));
}
Ok((stream, errors))
}
#[allow(dead_code)]
fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
let compile_errors = errors.iter().map(syn::Error::to_compile_error);
quote!(#(#compile_errors)*)
}
fn get_crate(var: Option<String>) -> proc_macro2::TokenStream {
if let Some(s) = var {
if let Ok(ts) = syn::parse_str(s.as_str()) {
return ts;
}
}
quote!(crate::docs)
}
#[derive(Debug)]
struct DocInfo {
pub summary: Option<String>,
pub description: Option<String>,
pub code_blocks: Vec<(String, bool)>,
}
fn extract_doc_from_attrs(attrs: &[syn::Attribute]) -> DocInfo {
let doc = syn::Ident::new("doc", proc_macro2::Span::call_site());
let raw_lines = attrs.iter().flat_map(|attr| {
if let syn::Meta::NameValue(nv) = &attr.meta {
if nv.path.is_ident(&doc) {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s), ..
}) = &nv.value
{
return normalize_comment_string(s.value());
}
}
}
Vec::new()
});
// Parse any code blocks from the doc string.
let mut code_blocks: Vec<(String, bool)> = Vec::new();
let mut code_block: Option<(String, bool)> = None;
let mut parsed_lines = Vec::new();
for line in raw_lines {
if line.starts_with("```") {
if let Some((inner_code_block, norun)) = code_block {
code_blocks.push((inner_code_block.trim().to_owned(), norun));
code_block = None;
} else {
let norun = line.contains("kcl,norun") || line.contains("kcl,no_run");
code_block = Some((String::new(), norun));
}
continue;
}
if let Some((code_block, _)) = &mut code_block {
code_block.push_str(&line);
code_block.push('\n');
} else {
parsed_lines.push(line);
}
}
if let Some((code_block, norun)) = code_block {
code_blocks.push((code_block.trim().to_string(), norun));
}
let mut summary = None;
let mut description: Option<String> = None;
for line in parsed_lines {
if line.is_empty() {
if let Some(desc) = &mut description {
// Handle fully blank comments as newlines we keep.
if !desc.is_empty() && !desc.ends_with('\n') {
if desc.ends_with(' ') {
desc.pop().unwrap();
}
desc.push_str("\n\n");
}
} else if summary.is_some() {
description = Some(String::new());
}
continue;
}
if let Some(desc) = &mut description {
desc.push_str(&line);
// Default to space-separating comment fragments.
desc.push(' ');
continue;
}
if summary.is_none() {
summary = Some(String::new());
}
match &mut summary {
Some(summary) => {
summary.push_str(&line);
// Default to space-separating comment fragments.
summary.push(' ');
}
None => unreachable!(),
}
}
// Trim the summary and description.
if let Some(s) = &mut summary {
while s.ends_with(' ') || s.ends_with('\n') {
s.pop().unwrap();
}
if s.is_empty() {
summary = None;
}
}
if let Some(d) = &mut description {
while d.ends_with(' ') || d.ends_with('\n') {
d.pop().unwrap();
}
if d.is_empty() {
description = None;
}
}
DocInfo {
summary,
description,
code_blocks,
}
}
fn normalize_comment_string(s: String) -> Vec<String> {
s.split('\n')
.map(|s| {
// Rust-style comments are intrinsically single-line.
// We only want to trim a single space character from the start of
// a line, and only if it's the first character.
s.strip_prefix(' ').unwrap_or(s).trim_end().to_owned()
})
.collect()
}
/// Represent an item without concern for its body which may (or may not)
/// contain syntax errors.
#[derive(Clone)]
struct ItemFnForSignature {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub sig: Signature,
pub _block: proc_macro2::TokenStream,
}
impl Parse for ItemFnForSignature {
fn parse(input: ParseStream) -> syn::parse::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let vis: Visibility = input.parse()?;
let sig: Signature = input.parse()?;
let block = input.parse()?;
Ok(ItemFnForSignature {
attrs,
vis,
sig,
_block: block,
})
}
}
fn clean_ty_string(t: &str) -> (String, proc_macro2::TokenStream) {
let mut ty_string = t
.replace("& 'a", "")
.replace('&', "")
.replace("mut", "")
.replace("< 'a >", "")
.replace(' ', "");
if ty_string.starts_with("ExecState") {
ty_string = "ExecState".to_string();
}
if ty_string.starts_with("Args") {
ty_string = "Args".to_string();
}
let ty_string = ty_string.trim().to_string();
let ty_ident = if ty_string.starts_with("Vec<") {
let ty_string = ty_string.trim_start_matches("Vec<").trim_end_matches('>');
let (_, ty_ident) = clean_ty_string(&ty_string);
quote! {
Vec<#ty_ident>
}
} else if ty_string.starts_with("kittycad::types::") {
let ty_string = ty_string.trim_start_matches("kittycad::types::").trim_end_matches('>');
let ty_ident = format_ident!("{}", ty_string);
quote! {
kittycad::types::#ty_ident
}
} else if ty_string.starts_with("Option<") {
let ty_string = ty_string.trim_start_matches("Option<").trim_end_matches('>');
let (_, ty_ident) = clean_ty_string(&ty_string);
quote! {
Option<#ty_ident>
}
} else if let Some((inner_array_type, num)) = parse_array_type(&ty_string) {
let ty_string = inner_array_type.to_owned();
let (_, ty_ident) = clean_ty_string(&ty_string);
quote! {
[#ty_ident; #num]
}
} else if ty_string.starts_with("Box<") {
let ty_string = ty_string.trim_start_matches("Box<").trim_end_matches('>');
let (_, ty_ident) = clean_ty_string(&ty_string);
quote! {
#ty_ident
}
} else {
let ty_ident = format_ident!("{}", ty_string);
quote! {
#ty_ident
}
};
(ty_string, ty_ident)
}
fn rust_type_to_openapi_type(t: &str) -> String {
let mut t = t.to_string();
// Turn vecs into arrays.
// TODO: handle nested types
if t.starts_with("Vec<") {
t = t.replace("Vec<", "[").replace('>', "]");
}
if t.starts_with("Box<") {
t = t.replace("Box<", "").replace('>', "");
}
if t.starts_with("Option<") {
t = t.replace("Option<", "").replace('>', "");
}
if t == "[TyF64;2]" {
return "Point2d".to_owned();
}
if t == "[TyF64;3]" {
return "Point3d".to_owned();
}
if let Some((inner_type, _length)) = parse_array_type(&t) {
t = format!("[{inner_type}]")
}
if t == "f64" || t == "TyF64" || t == "u32" || t == "NonZeroU32" {
return "number".to_string();
} else if t == "str" || t == "String" {
return "string".to_string();
} else {
return t.replace("f64", "number").replace("TyF64", "number").to_string();
}
}
fn parse_array_type(type_name: &str) -> Option<(&str, usize)> {
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[([a-zA-Z0-9<>]+); ?(\d+)\]").unwrap());
let cap = RE.captures(type_name)?;
let inner_type = cap.get(1)?;
let length = cap.get(2)?.as_str().parse().ok()?;
Some((inner_type.as_str(), length))
}
// For each kcl code block, we want to generate a test that checks that the
// code block is valid kcl code and compiles and executes.
fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> proc_macro2::TokenStream {
let test_name = format_ident!("kcl_test_example_{}{}", fn_name, index);
let test_name_mock = format_ident!("test_mock_example_{}{}", fn_name, index);
let output_test_name_str = format!("serial_test_example_{}{}", fn_name, index);
quote! {
#[tokio::test(flavor = "multi_thread")]
async fn #test_name_mock() -> miette::Result<()> {
let program = crate::Program::parse_no_errs(#code_block).unwrap();
let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())),
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
if let Err(e) = ctx.run(&program, &mut crate::execution::ExecState::new(&ctx)).await {
return Err(miette::Report::new(crate::errors::Report {
error: e.error,
filename: format!("{}{}", #fn_name, #index),
kcl_source: #code_block.to_string(),
}));
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn #test_name() -> miette::Result<()> {
let code = #code_block;
// Note, `crate` must be kcl_lib
let result = match crate::test_server::execute_and_snapshot(code, None).await {
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e.error,
filename: format!("{}{}", #fn_name, #index),
kcl_source: #code_block.to_string(),
}));
}
Err(other_err)=> panic!("{}", other_err),
Ok(img) => img,
};
twenty_twenty::assert_image(&format!("tests/outputs/{}.png", #output_test_name_str), &result, 0.99);
Ok(())
}
}
}

View File

@ -1,508 +0,0 @@
use anyhow::Result;
use quote::quote;
use crate::{do_stdlib, parse_array_type};
fn clean_text(s: &str) -> String {
// Add newlines after end-braces at <= two levels of indentation.
if cfg!(not(windows)) {
let regex = regex::Regex::new(r"(})(\n\s{0,8}[^} ])").unwrap();
regex.replace_all(s, "$1\n$2").to_string()
} else {
let regex = regex::Regex::new(r"(})(\r\n\s{0,8}[^} ])").unwrap();
regex.replace_all(s, "$1\r\n$2").to_string()
}
}
/// Format a TokenStream as a string and run `rustfmt` on the result.
fn get_text_fmt(output: &proc_macro2::TokenStream) -> Result<String> {
// Format the file with rustfmt.
let content = rustfmt_wrapper::rustfmt(output).unwrap();
Ok(clean_text(&content))
}
#[test]
fn test_get_inner_array_type() {
for (expected, input) in [
(Some(("f64", 2)), "[f64;2]"),
(Some(("String", 2)), "[String; 2]"),
(Some(("Option<String>", 12)), "[Option<String>;12]"),
(Some(("Option<String>", 12)), "[Option<String>; 12]"),
] {
let actual = parse_array_type(input);
assert_eq!(actual, expected);
}
}
#[test]
fn test_args_with_refs() {
let (item, mut errors) = do_stdlib(
quote! {
name = "someFn",
args = {
data = { docs = "The data for this function"},
},
},
quote! {
/// Docs
/// ```
/// someFn()
/// ```
fn someFn(
data: &'a str,
) -> i32 {
3
}
},
)
.unwrap();
if let Some(e) = errors.pop() {
panic!("{e}");
}
expectorate::assert_contents("tests/args_with_refs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_args_with_lifetime() {
let (item, mut errors) = do_stdlib(
quote! {
name = "someFn",
args = {
data = { docs = "Arg for the function" },
}
},
quote! {
/// Docs
/// ```
/// someFn()
/// ```
fn someFn<'a>(
data: Foo<'a>,
) -> i32 {
3
}
},
)
.unwrap();
if let Some(e) = errors.pop() {
panic!("{e}");
}
expectorate::assert_contents("tests/args_with_lifetime.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_args_with_exec_state() {
let (item, mut errors) = do_stdlib(
quote! {
name = "someFunction",
},
quote! {
/// Docs
/// ```
/// someFunction()
/// ```
fn inner_some_function<'a>(
exec_state: &mut ExecState,
args: &Args,
) -> i32 {
3
}
},
)
.unwrap();
if let Some(e) = errors.pop() {
panic!("{e}");
}
expectorate::assert_contents("tests/test_args_with_exec_state.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_line_to() {
let (item, errors) = do_stdlib(
quote! {
name = "lineTo",
args = {
data = { docs = "the sketch you're adding the line to" },
sketch = { docs = "the sketch you're adding the line to" },
}
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is another code block.
/// yes sirrr.
/// lineTo
/// ```
fn inner_line_to(
data: LineToData,
sketch: Sketch,
args: &Args,
) -> Result<Sketch, KclError> {
Ok(())
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/lineTo.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_min() {
let (item, errors) = do_stdlib(
quote! {
name = "min",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is another code block.
/// yes sirrr.
/// min
/// ```
fn inner_min(
/// The args to do shit to.
args: Vec<f64>
) -> f64 {
let mut min = std::f64::MAX;
for arg in args.iter() {
if *arg < min {
min = *arg;
}
}
min
}
},
)
.unwrap();
let _expected = quote! {};
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/min.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_show() {
let (item, errors) = do_stdlib(
quote! {
name = "show",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is code.
/// It does other shit.
/// show
/// ```
fn inner_show(
/// The args to do shit to.
_args: Vec<f64>
) {
}
},
)
.unwrap();
let _expected = quote! {};
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/show.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_box() {
let (item, errors) = do_stdlib(
quote! {
name = "show",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is code.
/// It does other shit.
/// show
/// ```
fn inner_show(
/// The args to do shit to.
args: Box<f64>
) -> Box<f64> {
args
}
},
)
.unwrap();
let _expected = quote! {};
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/box.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_option() {
let (item, errors) = do_stdlib(
quote! {
name = "show",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is code.
/// It does other shit.
/// show
/// ```
fn inner_show(
/// The args to do shit to.
args: Option<f64>
) -> Result<Box<f64>> {
args
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/option.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_array() {
let (item, errors) = do_stdlib(
quote! {
name = "show",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is another code block.
/// yes sirrr.
/// show
/// ```
fn inner_show(
/// The args to do shit to.
args: [f64; 2]
) -> Result<Box<f64>> {
args
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/array.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_option_input_format() {
let (item, errors) = do_stdlib(
quote! {
name = "import",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is code.
/// It does other shit.
/// import
/// ```
fn inner_import(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Box<f64>> {
args
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/option_input_format.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_return_vec_sketch() {
let (item, errors) = do_stdlib(
quote! {
name = "import",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is code.
/// It does other shit.
/// import
/// ```
fn inner_import(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Vec<Sketch>> {
args
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/return_vec_sketch.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_return_vec_box_sketch() {
let (item, errors) = do_stdlib(
quote! {
name = "import",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is code.
/// It does other shit.
/// import
/// ```
fn inner_import(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Vec<Box<Sketch>>> {
args
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/return_vec_box_sketch.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_doc_comment_with_code() {
let (item, errors) = do_stdlib(
quote! {
name = "myFunc",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is another code block.
/// yes sirrr.
/// myFunc
/// ```
fn inner_my_func(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Vec<Box<Sketch>>> {
args
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/doc_comment_with_code.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_fail_non_camel_case() {
let (_, errors) = do_stdlib(
quote! {
name = "import_thing",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is another code block.
/// yes sirrr.
/// ```
fn inner_import_thing(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Vec<Box<Sketch>>> {
args
}
},
)
.unwrap();
assert!(!errors.is_empty());
assert_eq!(
errors[1].to_string(),
"stdlib function names must be in camel case: `import_thing`"
);
}
#[test]
fn test_stdlib_fail_no_code_block() {
let (_, errors) = do_stdlib(
quote! {
name = "import",
},
quote! {
fn inner_import(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Vec<Box<Sketch>>> {
args
}
},
)
.unwrap();
assert!(!errors.is_empty());
assert_eq!(
errors[1].to_string(),
"stdlib functions must have at least one code block"
);
}
#[test]
fn test_stdlib_fail_name_not_in_code_block() {
let (_, errors) = do_stdlib(
quote! {
name = "import",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is another code block.
/// yes sirrr.
/// ```
fn inner_import(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Vec<Box<Sketch>>> {
args
}
},
)
.unwrap();
assert!(!errors.is_empty());
assert_eq!(
errors[1].to_string(),
"stdlib functions must have the function name `import` in the code block"
);
}

View File

@ -1,91 +0,0 @@
// Unbox a Vec<Box<T>> to Vec<T>.
// Unbox a Box<T> to T.
pub(crate) fn unbox(t: syn::Type) -> syn::Type {
unbox_inner(unbox_vec(t))
}
// Unbox a syn::Type that is boxed to the inner object.
fn unbox_inner(t: syn::Type) -> syn::Type {
match t {
syn::Type::Path(syn::TypePath { ref path, .. }) => {
let path = &path.segments;
if path.len() == 1 {
let seg = &path[0];
if seg.ident == "Box" {
if let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { args, .. }) =
&seg.arguments
{
if args.len() == 1 {
let mut args = args.iter();
let ok = args.next().unwrap();
if let syn::GenericArgument::Type(ty) = ok {
return ty.clone();
}
}
}
}
}
}
_ => {
return t;
}
}
t
}
// For a Vec<Box<T>> return Vec<T>.
// For a Vec<T> return Vec<T>.
fn unbox_vec(t: syn::Type) -> syn::Type {
match t {
syn::Type::Path(syn::TypePath { ref path, .. }) => {
let path = &path.segments;
if path.len() == 1 {
let seg = &path[0];
if seg.ident == "Vec" {
if let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { args, .. }) =
&seg.arguments
{
if args.len() == 1 {
let mut args = args.iter();
let ok = args.next().unwrap();
if let syn::GenericArgument::Type(ty) = ok {
let unboxed = unbox(ty.clone());
// Wrap it back in a vec.
let wrapped = syn::Type::Path(syn::TypePath {
qself: None,
path: syn::Path {
leading_colon: None,
segments: {
let mut segments = syn::punctuated::Punctuated::new();
segments.push_value(syn::PathSegment {
ident: syn::Ident::new("Vec", proc_macro2::Span::call_site()),
arguments: syn::PathArguments::AngleBracketed(
syn::AngleBracketedGenericArguments {
colon2_token: None,
lt_token: syn::token::Lt::default(),
args: {
let mut args = syn::punctuated::Punctuated::new();
args.push_value(syn::GenericArgument::Type(unboxed));
args
},
gt_token: syn::token::Gt::default(),
},
),
});
segments
},
},
});
return wrapped;
}
}
}
}
}
}
_ => {
return t;
}
}
t
}

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-directory-test-macro" name = "kcl-directory-test-macro"
description = "A tool for generating tests from a directory of kcl files" description = "A tool for generating tests from a directory of kcl files"
version = "0.1.78" version = "0.1.80"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "kcl-language-server-release" name = "kcl-language-server-release"
version = "0.1.78" version = "0.1.80"
edition = "2021" edition = "2021"
authors = ["KittyCAD Inc <kcl@kittycad.io>"] authors = ["KittyCAD Inc <kcl@kittycad.io>"]
publish = false publish = false

View File

@ -2,7 +2,7 @@
name = "kcl-language-server" name = "kcl-language-server"
description = "A language server for KCL." description = "A language server for KCL."
authors = ["KittyCAD Inc <kcl@kittycad.io>"] authors = ["KittyCAD Inc <kcl@kittycad.io>"]
version = "0.2.78" version = "0.2.80"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.2.78" version = "0.2.80"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -31,7 +31,7 @@ clap = { version = "4.5.36", default-features = false, optional = true, features
"derive", "derive",
] } ] }
convert_case = "0.8.0" convert_case = "0.8.0"
csscolorparser = "0.7.0" csscolorparser = "0.7.2"
dashmap = { workspace = true } dashmap = { workspace = true }
dhat = { version = "0.3", optional = true } dhat = { version = "0.3", optional = true }
fnv = "1.0.7" fnv = "1.0.7"
@ -107,7 +107,7 @@ web-sys = { version = "0.3.76", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
instant = "0.1.13" instant = "0.1.13"
tokio = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] }
tokio-tungstenite = { version = "0.24.0", features = [ tokio-tungstenite = { version = "0.26.2", features = [
"rustls-tls-native-roots", "rustls-tls-native-roots",
] } ] }
tower-lsp = { workspace = true, features = ["proposed", "default"] } tower-lsp = { workspace = true, features = ["proposed", "default"] }
@ -130,7 +130,7 @@ tabled = ["dep:tabled"]
[dev-dependencies] [dev-dependencies]
approx = "0.5" approx = "0.5"
base64 = "0.22.1" base64 = "0.22.1"
criterion = { version = "0.5.1", features = ["async_tokio"] } criterion = { version = "0.6.0", features = ["async_tokio"] }
expectorate = "1.1.0" expectorate = "1.1.0"
handlebars = "6.3.2" handlebars = "6.3.2"
image = { version = "0.25.6", default-features = false, features = ["png"] } image = { version = "0.25.6", default-features = false, features = ["png"] }

View File

@ -1,9 +1,10 @@
use std::{ use std::{
fs, fs,
hint::black_box,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
const IGNORE_DIRS: [&str; 2] = ["step", "screenshots"]; const IGNORE_DIRS: [&str; 2] = ["step", "screenshots"];

View File

@ -1,4 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use std::hint::black_box;
use criterion::{criterion_group, criterion_main, Criterion};
pub fn bench_parse(c: &mut Criterion) { pub fn bench_parse(c: &mut Criterion) {
for (name, file) in [ for (name, file) in [

View File

@ -1,4 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; use std::hint::black_box;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use kcl_lib::kcl_lsp_server; use kcl_lib::kcl_lsp_server;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use tower_lsp::LanguageServer; use tower_lsp::LanguageServer;

View File

@ -762,7 +762,7 @@ async fn kcl_test_stdlib_kcl_error_right_code_path() {
}; };
assert_eq!( assert_eq!(
err.error.message(), err.error.message(),
"This function requires a keyword argument 'center'" "This function requires a keyword argument `center`"
); );
} }
@ -1230,10 +1230,7 @@ secondSketch = startSketchOn(part001, face = '')
let result = execute_and_snapshot(code, None).await; let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err(); let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap(); let err = err.as_kcl_error().unwrap();
assert_eq!( assert_eq!(err.message(), "face requires a value with type `tag`, but found string");
err.message(),
"This function expected the input argument to be tag but it's actually of type string"
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1684,10 +1681,16 @@ example = extrude(exampleSketch, length = 10)
assert_eq!(err.message(), "Cannot have an x constrained angle of 90 degrees"); assert_eq!(err.message(), "Cannot have an x constrained angle of 90 degrees");
assert_eq!( assert_eq!(
err.backtrace(), err.backtrace(),
vec![BacktraceItem { vec![
source_range: SourceRange::new(70, 111, ModuleId::default()), BacktraceItem {
fn_name: Some("angledLine".to_owned()) source_range: SourceRange::new(70, 111, ModuleId::default()),
}] fn_name: Some("angledLine".to_owned())
},
BacktraceItem {
source_range: SourceRange::new(70, 111, ModuleId::default()),
fn_name: None
}
]
); );
} }
@ -1709,10 +1712,16 @@ example = extrude(exampleSketch, length = 10)
assert_eq!(err.message(), "Cannot have an x constrained angle of 270 degrees"); assert_eq!(err.message(), "Cannot have an x constrained angle of 270 degrees");
assert_eq!( assert_eq!(
err.backtrace(), err.backtrace(),
vec![BacktraceItem { vec![
source_range: SourceRange::new(70, 112, ModuleId::default()), BacktraceItem {
fn_name: Some("angledLine".to_owned()) source_range: SourceRange::new(70, 112, ModuleId::default()),
}] fn_name: Some("angledLine".to_owned())
},
BacktraceItem {
source_range: SourceRange::new(70, 112, ModuleId::default()),
fn_name: None
}
]
); );
} }
@ -1734,10 +1743,16 @@ example = extrude(exampleSketch, length = 10)
assert_eq!(err.message(), "Cannot have a y constrained angle of 0 degrees"); assert_eq!(err.message(), "Cannot have a y constrained angle of 0 degrees");
assert_eq!( assert_eq!(
err.backtrace(), err.backtrace(),
vec![BacktraceItem { vec![
source_range: SourceRange::new(70, 110, ModuleId::default()), BacktraceItem {
fn_name: Some("angledLine".to_owned()) source_range: SourceRange::new(70, 110, ModuleId::default()),
}] fn_name: Some("angledLine".to_owned())
},
BacktraceItem {
source_range: SourceRange::new(70, 110, ModuleId::default()),
fn_name: None
}
]
); );
} }
@ -1759,10 +1774,16 @@ example = extrude(exampleSketch, length = 10)
assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees"); assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees");
assert_eq!( assert_eq!(
err.backtrace(), err.backtrace(),
vec![BacktraceItem { vec![
source_range: SourceRange::new(70, 112, ModuleId::default()), BacktraceItem {
fn_name: Some("angledLine".to_owned()) source_range: SourceRange::new(70, 112, ModuleId::default()),
}] fn_name: Some("angledLine".to_owned())
},
BacktraceItem {
source_range: SourceRange::new(70, 112, ModuleId::default()),
fn_name: None
}
]
); );
} }
@ -1784,10 +1805,16 @@ extrusion = extrude(sketch001, length = 10)
assert_eq!(err.message(), "Cannot have an x constrained angle of 90 degrees"); assert_eq!(err.message(), "Cannot have an x constrained angle of 90 degrees");
assert_eq!( assert_eq!(
err.backtrace(), err.backtrace(),
vec![BacktraceItem { vec![
source_range: SourceRange::new(66, 116, ModuleId::default()), BacktraceItem {
fn_name: Some("angledLine".to_owned()) source_range: SourceRange::new(66, 116, ModuleId::default()),
}] fn_name: Some("angledLine".to_owned())
},
BacktraceItem {
source_range: SourceRange::new(66, 116, ModuleId::default()),
fn_name: None,
}
]
); );
} }
@ -1809,10 +1836,16 @@ extrusion = extrude(sketch001, length = 10)
assert_eq!(err.message(), "Cannot have an x constrained angle of 270 degrees"); assert_eq!(err.message(), "Cannot have an x constrained angle of 270 degrees");
assert_eq!( assert_eq!(
err.backtrace(), err.backtrace(),
vec![BacktraceItem { vec![
source_range: SourceRange::new(66, 117, ModuleId::default()), BacktraceItem {
fn_name: Some("angledLine".to_owned()) source_range: SourceRange::new(66, 117, ModuleId::default()),
}] fn_name: Some("angledLine".to_owned())
},
BacktraceItem {
source_range: SourceRange::new(66, 117, ModuleId::default()),
fn_name: None
}
]
); );
} }
@ -1836,10 +1869,16 @@ example = extrude(exampleSketch, length = 10)
assert_eq!(err.message(), "Cannot have a y constrained angle of 0 degrees"); assert_eq!(err.message(), "Cannot have a y constrained angle of 0 degrees");
assert_eq!( assert_eq!(
err.backtrace(), err.backtrace(),
vec![BacktraceItem { vec![
source_range: SourceRange::new(95, 130, ModuleId::default()), BacktraceItem {
fn_name: Some("angledLine".to_owned()) source_range: SourceRange::new(95, 130, ModuleId::default()),
}] fn_name: Some("angledLine".to_owned())
},
BacktraceItem {
source_range: SourceRange::new(95, 130, ModuleId::default()),
fn_name: None
}
]
); );
} }
@ -1863,10 +1902,16 @@ example = extrude(exampleSketch, length = 10)
assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees"); assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees");
assert_eq!( assert_eq!(
err.backtrace(), err.backtrace(),
vec![BacktraceItem { vec![
source_range: SourceRange::new(95, 132, ModuleId::default()), BacktraceItem {
fn_name: Some("angledLine".to_owned()) source_range: SourceRange::new(95, 132, ModuleId::default()),
}] fn_name: Some("angledLine".to_owned())
},
BacktraceItem {
source_range: SourceRange::new(95, 132, ModuleId::default()),
fn_name: None
}
]
); );
} }
@ -1890,10 +1935,16 @@ example = extrude(exampleSketch, length = 10)
assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees"); assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees");
assert_eq!( assert_eq!(
err.backtrace(), err.backtrace(),
vec![BacktraceItem { vec![
source_range: SourceRange::new(95, 133, ModuleId::default()), BacktraceItem {
fn_name: Some("angledLine".to_owned()) source_range: SourceRange::new(95, 133, ModuleId::default()),
}] fn_name: Some("angledLine".to_owned())
},
BacktraceItem {
source_range: SourceRange::new(95, 133, ModuleId::default()),
fn_name: None
}
]
); );
} }
@ -1911,12 +1962,13 @@ someFunction('INVALID')
let err = err.as_kcl_error().unwrap(); let err = err.as_kcl_error().unwrap();
assert_eq!( assert_eq!(
err.message(), err.message(),
"This function expected the input argument to be Solid or Plane but it's actually of type string" "The input argument of `startSketchOn` requires a value with type `Solid | Plane`, but found string"
); );
assert_eq!( assert_eq!(
err.source_ranges(), err.source_ranges(),
vec![ vec![
SourceRange::new(46, 55, ModuleId::default()), SourceRange::new(46, 55, ModuleId::default()),
SourceRange::new(32, 56, ModuleId::default()),
SourceRange::new(60, 83, ModuleId::default()), SourceRange::new(60, 83, ModuleId::default()),
] ]
); );
@ -1925,6 +1977,10 @@ someFunction('INVALID')
vec![ vec![
BacktraceItem { BacktraceItem {
source_range: SourceRange::new(46, 55, ModuleId::default()), source_range: SourceRange::new(46, 55, ModuleId::default()),
fn_name: Some("startSketchOn".to_owned()),
},
BacktraceItem {
source_range: SourceRange::new(32, 56, ModuleId::default()),
fn_name: Some("someFunction".to_owned()), fn_name: Some("someFunction".to_owned()),
}, },
BacktraceItem { BacktraceItem {

View File

@ -2,48 +2,11 @@ use std::{collections::HashMap, fs, path::Path};
use anyhow::Result; use anyhow::Result;
use base64::Engine; use base64::Engine;
use convert_case::Casing;
use indexmap::IndexMap;
use itertools::Itertools;
use serde_json::json; use serde_json::json;
use tokio::task::JoinSet; use tokio::task::JoinSet;
use super::kcl_doc::{ConstData, DocData, ExampleProperties, FnData, ModData, TyData}; use super::kcl_doc::{ConstData, DocData, ExampleProperties, FnData, ModData, TyData};
use crate::{ use crate::ExecutorContext;
docs::{StdLibFn, DECLARED_TYPES},
std::StdLib,
ExecutorContext,
};
// Types with special handling.
const SPECIAL_TYPES: [&str; 4] = ["TagDeclarator", "TagIdentifier", "Start", "End"];
const TYPE_REWRITES: [(&str, &str); 11] = [
("TagNode", "TagDeclarator"),
("SketchData", "Plane | Solid"),
("SketchOrSurface", "Sketch | Plane | Face"),
("SketchSurface", "Plane | Face"),
("SolidOrImportedGeometry", "[Solid] | ImportedGeometry"),
(
"SolidOrSketchOrImportedGeometry",
"[Solid] | [Sketch] | ImportedGeometry",
),
("KclValue", "any"),
("[KclValue]", "[any]"),
("FaceTag", "TagIdentifier | Start | End"),
("GeometryWithImportedGeometry", "Solid | Sketch | ImportedGeometry"),
("SweepPath", "Sketch | Helix"),
];
fn rename_type(input: &str) -> &str {
for (i, o) in TYPE_REWRITES {
if input == i {
return o;
}
}
input
}
fn init_handlebars() -> Result<handlebars::Handlebars<'static>> { fn init_handlebars() -> Result<handlebars::Handlebars<'static>> {
let mut hbs = handlebars::Handlebars::new(); let mut hbs = handlebars::Handlebars::new();
@ -104,7 +67,7 @@ fn init_handlebars() -> Result<handlebars::Handlebars<'static>> {
Ok(hbs) Ok(hbs)
} }
fn generate_index(combined: &IndexMap<String, Box<dyn StdLibFn>>, kcl_lib: &ModData) -> Result<()> { fn generate_index(kcl_lib: &ModData) -> Result<()> {
let hbs = init_handlebars()?; let hbs = init_handlebars()?;
let mut functions = HashMap::new(); let mut functions = HashMap::new();
@ -115,31 +78,6 @@ fn generate_index(combined: &IndexMap<String, Box<dyn StdLibFn>>, kcl_lib: &ModD
let mut types = HashMap::new(); let mut types = HashMap::new();
types.insert("Primitive types".to_owned(), Vec::new()); types.insert("Primitive types".to_owned(), Vec::new());
for key in combined.keys() {
let internal_fn = combined
.get(key)
.ok_or_else(|| anyhow::anyhow!("Failed to get internal function: {}", key))?;
if internal_fn.unpublished() || internal_fn.deprecated() {
continue;
}
let tags = internal_fn.tags();
let module = tags.first().map(|s| format!("std::{s}")).unwrap_or("std".to_owned());
functions
.entry(module.to_owned())
.or_default()
.push((internal_fn.name(), format!("/docs/kcl-std/{}", internal_fn.name())));
}
for name in SPECIAL_TYPES {
types
.get_mut("Primitive types")
.unwrap()
.push((name.to_owned(), format!("/docs/kcl-lang/types#{name}")));
}
for d in kcl_lib.all_docs() { for d in kcl_lib.all_docs() {
if d.hide() { if d.hide() {
continue; continue;
@ -257,8 +195,8 @@ fn generate_example(index: usize, src: &str, props: &ExampleProperties, file_nam
})) }))
} }
fn generate_type_from_kcl(ty: &TyData, file_name: String, example_name: String) -> Result<()> { fn generate_type_from_kcl(ty: &TyData, file_name: String, example_name: String, kcl_std: &ModData) -> Result<()> {
if ty.properties.doc_hidden || !DECLARED_TYPES.contains(&&*ty.name) { if ty.properties.doc_hidden {
return Ok(()); return Ok(());
} }
@ -282,18 +220,14 @@ fn generate_type_from_kcl(ty: &TyData, file_name: String, example_name: String)
}); });
let output = hbs.render("kclType", &data)?; let output = hbs.render("kclType", &data)?;
let output = cleanup_types(&output); let output = cleanup_types(&output, kcl_std);
expectorate::assert_contents(format!("../../docs/kcl-std/{}.md", file_name), &output); expectorate::assert_contents(format!("../../docs/kcl-std/{}.md", file_name), &output);
Ok(()) Ok(())
} }
fn generate_mod_from_kcl(m: &ModData, file_name: String, combined: &IndexMap<String, Box<dyn StdLibFn>>) -> Result<()> { fn generate_mod_from_kcl(m: &ModData, file_name: String) -> Result<()> {
fn list_items( fn list_items(m: &ModData, namespace: &str) -> Vec<gltf_json::Value> {
m: &ModData,
namespace: &str,
combined: &IndexMap<String, Box<dyn StdLibFn>>,
) -> Vec<gltf_json::Value> {
let mut items: Vec<_> = m let mut items: Vec<_> = m
.children .children
.iter() .iter()
@ -301,25 +235,6 @@ fn generate_mod_from_kcl(m: &ModData, file_name: String, combined: &IndexMap<Str
.map(|(_, v)| (v.preferred_name().to_owned(), v.file_name())) .map(|(_, v)| (v.preferred_name().to_owned(), v.file_name()))
.collect(); .collect();
if namespace == "I:" {
// Add in functions declared in Rust
items.extend(
combined
.values()
.filter(|f| {
if f.unpublished() || f.deprecated() {
return false;
}
let tags = f.tags();
let module = tags.first().map(|s| format!("std::{s}")).unwrap_or("std".to_owned());
module == m.qual_name
})
.map(|f| (f.name(), f.name())),
)
}
items.sort(); items.sort();
items items
.into_iter() .into_iter()
@ -333,9 +248,9 @@ fn generate_mod_from_kcl(m: &ModData, file_name: String, combined: &IndexMap<Str
} }
let hbs = init_handlebars()?; let hbs = init_handlebars()?;
let functions = list_items(m, "I:", combined); let functions = list_items(m, "I:");
let modules = list_items(m, "M:", combined); let modules = list_items(m, "M:");
let types = list_items(m, "T:", combined); let types = list_items(m, "T:");
let data = json!({ let data = json!({
"name": m.name, "name": m.name,
@ -391,7 +306,7 @@ fn generate_function_from_kcl(
json!({ json!({
"name": arg.name, "name": arg.name,
"type_": arg.ty, "type_": arg.ty,
"description": docs.or_else(|| arg.ty.as_ref().and_then(|t| super::docs_for_type(t, kcl_std))).unwrap_or_default(), "description": docs.or_else(|| arg.ty.as_ref().and_then(|t| docs_for_type(t, kcl_std))).unwrap_or_default(),
"required": arg.kind.required(), "required": arg.kind.required(),
}) })
}).collect::<Vec<_>>(); }).collect::<Vec<_>>();
@ -408,18 +323,30 @@ fn generate_function_from_kcl(
"return_value": function.return_type.as_ref().map(|t| { "return_value": function.return_type.as_ref().map(|t| {
json!({ json!({
"type_": t, "type_": t,
"description": super::docs_for_type(t, kcl_std).unwrap_or_default(), "description": docs_for_type(t, kcl_std).unwrap_or_default(),
}) })
}), }),
}); });
let output = hbs.render("function", &data)?; let output = hbs.render("function", &data)?;
let output = &cleanup_types(&output); let output = &cleanup_types(&output, kcl_std);
expectorate::assert_contents(format!("../../docs/kcl-std/{}.md", file_name), output); expectorate::assert_contents(format!("../../docs/kcl-std/{}.md", file_name), output);
Ok(()) Ok(())
} }
fn docs_for_type(ty: &str, kcl_std: &ModData) -> Option<String> {
let key = if ty.starts_with("number") { "number" } else { ty };
if !key.contains('|') && !key.contains('[') {
if let Some(data) = kcl_std.find_by_name(key) {
return data.summary().cloned();
}
}
None
}
fn generate_const_from_kcl(cnst: &ConstData, file_name: String, example_name: String) -> Result<()> { fn generate_const_from_kcl(cnst: &ConstData, file_name: String, example_name: String) -> Result<()> {
if cnst.properties.doc_hidden { if cnst.properties.doc_hidden {
return Ok(()); return Ok(());
@ -450,83 +377,7 @@ fn generate_const_from_kcl(cnst: &ConstData, file_name: String, example_name: St
Ok(()) Ok(())
} }
fn generate_function(internal_fn: Box<dyn StdLibFn>, kcl_std: &ModData) -> Result<()> { fn cleanup_types(input: &str, kcl_std: &ModData) -> String {
let hbs = init_handlebars()?;
if internal_fn.unpublished() {
return Ok(());
}
let fn_name = internal_fn.name();
let snake_case_name = clean_function_name(&fn_name);
let examples: Vec<serde_json::Value> = internal_fn
.examples()
.iter()
.enumerate()
.map(|(index, (example, norun))| {
let image_base64 = if !norun {
let image_path = format!(
"{}/tests/outputs/serial_test_example_{}{}.png",
env!("CARGO_MANIFEST_DIR"),
snake_case_name,
index
);
let image_data =
std::fs::read(&image_path).unwrap_or_else(|_| panic!("Failed to read image file: {}", image_path));
base64::engine::general_purpose::STANDARD.encode(&image_data)
} else {
String::new()
};
json!({
"content": example,
"image_base64": image_base64,
})
})
.collect();
let tags = internal_fn.tags();
let module = tags
.first()
.map(|s| &**s)
.map(|m| format!("std::{m}"))
.unwrap_or("std".to_owned());
let data = json!({
"name": fn_name,
"module": module,
"summary": internal_fn.summary(),
"description": internal_fn.description(),
"deprecated": internal_fn.deprecated(),
"fn_signature": internal_fn.fn_signature(true),
"examples": examples,
"args": internal_fn.args(false).iter().map(|arg| {
json!({
"name": arg.name,
"type_": rename_type(&arg.type_),
"description": arg.description(Some(kcl_std)),
"required": arg.required,
})
}).collect::<Vec<_>>(),
"return_value": internal_fn.return_value(false).map(|ret| {
json!({
"type_": rename_type(&ret.type_),
"description": ret.description(Some(kcl_std)),
})
}),
});
let mut output = hbs.render("function", &data)?;
// Fix the links to the types.
output = cleanup_types(&output);
expectorate::assert_contents(format!("../../docs/kcl-std/{}.md", fn_name), &output);
Ok(())
}
fn cleanup_types(input: &str) -> String {
#[derive(Copy, Clone, Eq, PartialEq, Debug)] #[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum State { enum State {
Text, Text,
@ -550,7 +401,7 @@ fn cleanup_types(input: &str) -> String {
if code_type.starts_with(' ') { if code_type.starts_with(' ') {
code.push(' '); code.push(' ');
} }
code.push_str(&cleanup_type_string(code_type.trim(), false)); code.push_str(&cleanup_type_string(code_type.trim(), false, kcl_std));
if code_type.ends_with(' ') { if code_type.ends_with(' ') {
code.push(' '); code.push(' ');
} }
@ -586,7 +437,7 @@ fn cleanup_types(input: &str) -> String {
} }
ticks = 0; ticks = 0;
} else if state == State::Text && ticks == 2 && !code.is_empty() { } else if state == State::Text && ticks == 2 && !code.is_empty() {
output.push_str(&cleanup_type_string(&code, true)); output.push_str(&cleanup_type_string(&code, true, kcl_std));
code = String::new(); code = String::new();
ticks = 0; ticks = 0;
} else if state == State::CodeBlock { } else if state == State::CodeBlock {
@ -631,14 +482,12 @@ fn cleanup_types(input: &str) -> String {
output output
} }
fn cleanup_type_string(input: &str, fmt_for_text: bool) -> String { fn cleanup_type_string(input: &str, fmt_for_text: bool, kcl_std: &ModData) -> String {
assert!( assert!(
!(input.starts_with('[') && input.ends_with(']') && input.contains('|')), !(input.starts_with('[') && input.ends_with(']') && input.contains('|')),
"Arrays of unions are not supported" "Arrays of unions are not supported"
); );
let input = rename_type(input);
let tys: Vec<_> = input let tys: Vec<_> = input
.split('|') .split('|')
.map(|ty| { .map(|ty| {
@ -676,9 +525,7 @@ fn cleanup_type_string(input: &str, fmt_for_text: bool) -> String {
format!("[{prefix}{ty}{suffix}](/docs/kcl-std/types/std-types-number)") format!("[{prefix}{ty}{suffix}](/docs/kcl-std/types/std-types-number)")
} else if fmt_for_text && ty.starts_with("fn") { } else if fmt_for_text && ty.starts_with("fn") {
format!("[{prefix}{ty}{suffix}](/docs/kcl-std/types/std-types-fn)") format!("[{prefix}{ty}{suffix}](/docs/kcl-std/types/std-types-fn)")
} else if fmt_for_text && SPECIAL_TYPES.contains(&ty) { } else if fmt_for_text && matches!(kcl_std.find_by_name(ty), Some(DocData::Ty(_))) {
format!("[{prefix}{ty}{suffix}](/docs/kcl-lang/types#{ty})")
} else if fmt_for_text && DECLARED_TYPES.contains(&ty) {
format!("[{prefix}{ty}{suffix}](/docs/kcl-std/types/std-types-{ty})") format!("[{prefix}{ty}{suffix}](/docs/kcl-std/types/std-types-{ty})")
} else { } else {
format!("{prefix}{ty}{suffix}") format!("{prefix}{ty}{suffix}")
@ -689,73 +536,22 @@ fn cleanup_type_string(input: &str, fmt_for_text: bool) -> String {
tys.join(if fmt_for_text { " or " } else { " | " }) tys.join(if fmt_for_text { " or " } else { " | " })
} }
fn clean_function_name(name: &str) -> String {
// Convert from camel case to snake case.
let mut fn_name = name.to_case(convert_case::Case::Snake);
// Clean the fn name.
if fn_name.starts_with("last_seg_") {
fn_name = fn_name.replace("last_seg_", "last_segment_");
} else if fn_name.contains("_2_d") {
fn_name = fn_name.replace("_2_d", "_2d");
} else if fn_name.contains("_3_d") {
fn_name = fn_name.replace("_3_d", "_3d");
} else if fn_name == "seg_ang" {
fn_name = "segment_angle".to_string();
} else if fn_name == "seg_len" {
fn_name = "segment_length".to_string();
} else if fn_name.starts_with("seg_") {
fn_name = fn_name.replace("seg_", "segment_");
}
fn_name
}
#[test] #[test]
fn test_generate_stdlib_markdown_docs() { fn test_generate_stdlib_markdown_docs() {
let stdlib = StdLib::new();
let combined = stdlib.combined();
let kcl_std = crate::docs::kcl_doc::walk_prelude(); let kcl_std = crate::docs::kcl_doc::walk_prelude();
// Generate the index which is the table of contents. // Generate the index which is the table of contents.
generate_index(&combined, &kcl_std).unwrap(); generate_index(&kcl_std).unwrap();
for key in combined.keys().sorted() {
let internal_fn = combined.get(key).unwrap();
generate_function(internal_fn.clone(), &kcl_std).unwrap();
}
for d in kcl_std.all_docs() { for d in kcl_std.all_docs() {
match d { match d {
DocData::Fn(f) => generate_function_from_kcl(f, d.file_name(), d.example_name(), &kcl_std).unwrap(), DocData::Fn(f) => generate_function_from_kcl(f, d.file_name(), d.example_name(), &kcl_std).unwrap(),
DocData::Const(c) => generate_const_from_kcl(c, d.file_name(), d.example_name()).unwrap(), DocData::Const(c) => generate_const_from_kcl(c, d.file_name(), d.example_name()).unwrap(),
DocData::Ty(t) => generate_type_from_kcl(t, d.file_name(), d.example_name()).unwrap(), DocData::Ty(t) => generate_type_from_kcl(t, d.file_name(), d.example_name(), &kcl_std).unwrap(),
DocData::Mod(m) => generate_mod_from_kcl(m, d.file_name(), &combined).unwrap(), DocData::Mod(m) => generate_mod_from_kcl(m, d.file_name()).unwrap(),
} }
} }
generate_mod_from_kcl(&kcl_std, "modules/std".to_owned(), &combined).unwrap(); generate_mod_from_kcl(&kcl_std, "modules/std".to_owned()).unwrap();
}
#[test]
fn test_generate_stdlib_json_schema() {
// If this test fails and you've modified the AST or something else which affects the json repr
// of stdlib functions, you should rerun the test with `EXPECTORATE=overwrite` to create new
// test data, then check `/docs/kcl-std/std.json` to ensure the changes are expected.
// Alternatively, run `just redo-kcl-stdlib-docs` (make sure to have just installed).
let stdlib = StdLib::new();
let combined = stdlib.combined();
let json_data: Vec<_> = combined
.keys()
.sorted()
.map(|key| {
let internal_fn = combined.get(key).unwrap();
internal_fn.to_json().unwrap()
})
.collect();
expectorate::assert_contents(
"../../docs/kcl-std/std.json",
&serde_json::to_string_pretty(&json_data).unwrap(),
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]

View File

@ -302,6 +302,7 @@ impl DocData {
} }
} }
#[allow(dead_code)]
pub(super) fn summary(&self) -> Option<&String> { pub(super) fn summary(&self) -> Option<&String> {
match self { match self {
DocData::Fn(f) => f.summary.as_ref(), DocData::Fn(f) => f.summary.as_ref(),
@ -462,6 +463,7 @@ impl ModData {
} }
} }
#[allow(dead_code)]
pub fn find_by_name(&self, name: &str) -> Option<&DocData> { pub fn find_by_name(&self, name: &str) -> Option<&DocData> {
if let Some(result) = self if let Some(result) = self
.children .children
@ -812,6 +814,7 @@ impl ArgData {
return Some((index + n - 1, snippet)); return Some((index + n - 1, snippet));
} }
match self.ty.as_deref() { match self.ty.as_deref() {
Some("Sketch") if self.kind == ArgKind::Special => None,
Some(s) if s.starts_with("number") => Some((index, format!(r#"{label}${{{}:10}}"#, index))), Some(s) if s.starts_with("number") => Some((index, format!(r#"{label}${{{}:10}}"#, index))),
Some("Point2d") => Some((index + 1, format!(r#"{label}[${{{}:0}}, ${{{}:0}}]"#, index, index + 1))), Some("Point2d") => Some((index + 1, format!(r#"{label}[${{{}:0}}, ${{{}:0}}]"#, index, index + 1))),
Some("Point3d") => Some(( Some("Point3d") => Some((
@ -827,7 +830,7 @@ impl ArgData {
Some("Sketch") | Some("Sketch | Helix") => Some((index, format!(r#"{label}${{{index}:sketch000}}"#))), Some("Sketch") | Some("Sketch | Helix") => Some((index, format!(r#"{label}${{{index}:sketch000}}"#))),
Some("Edge") => Some((index, format!(r#"{label}${{{index}:tag_or_edge_fn}}"#))), Some("Edge") => Some((index, format!(r#"{label}${{{index}:tag_or_edge_fn}}"#))),
Some("[Edge; 1+]") => Some((index, format!(r#"{label}[${{{index}:tag_or_edge_fn}}]"#))), Some("[Edge; 1+]") => Some((index, format!(r#"{label}[${{{index}:tag_or_edge_fn}}]"#))),
Some("Plane") => Some((index, format!(r#"{label}${{{}:XY}}"#, index))), Some("Plane") | Some("Solid | Plane") => Some((index, format!(r#"{label}${{{}:XY}}"#, index))),
Some("[tag; 2]") => Some(( Some("[tag; 2]") => Some((
index + 1, index + 1,
format!(r#"{label}[${{{}:tag}}, ${{{}:tag}}]"#, index, index + 1), format!(r#"{label}[${{{}:tag}}, ${{{}:tag}}]"#, index, index + 1),
@ -989,7 +992,7 @@ trait ApplyMeta {
} }
let mut summary = None; let mut summary = None;
let mut description = None; let mut description: Option<String> = None;
let mut example: Option<(String, ExampleProperties)> = None; let mut example: Option<(String, ExampleProperties)> = None;
let mut examples = Vec::new(); let mut examples = Vec::new();
for l in comments.iter().filter(|l| l.starts_with("///")).map(|l| { for l in comments.iter().filter(|l| l.starts_with("///")).map(|l| {
@ -999,22 +1002,6 @@ trait ApplyMeta {
&l[3..] &l[3..]
} }
}) { }) {
if description.is_none() && summary.is_none() {
summary = Some(l.to_owned());
continue;
}
if description.is_none() {
if l.is_empty() {
description = Some(String::new());
} else {
description = summary;
summary = None;
let d = description.as_mut().unwrap();
d.push('\n');
d.push_str(l);
}
continue;
}
#[allow(clippy::manual_strip)] #[allow(clippy::manual_strip)]
if l.starts_with("```") { if l.starts_with("```") {
if let Some((e, p)) = example { if let Some((e, p)) = example {
@ -1050,12 +1037,36 @@ trait ApplyMeta {
continue; continue;
} }
} }
// An empty line outside of an example. This either starts the description (with or
// without a summary) or adds a blank line to the description.
if l.is_empty() {
match &mut description {
Some(d) => {
d.push('\n');
}
None => description = Some(String::new()),
}
continue;
}
// Our first line, start the summary.
if description.is_none() && summary.is_none() {
summary = Some(l.to_owned());
continue;
}
// Append the line to either the description or summary.
match &mut description { match &mut description {
Some(d) => { Some(d) => {
d.push_str(l); d.push_str(l);
d.push('\n'); d.push('\n');
} }
None => unreachable!(), None => {
let s = summary.as_mut().unwrap();
s.push(' ');
s.push_str(l);
}
} }
} }
assert!(example.is_none()); assert!(example.is_none());

File diff suppressed because it is too large Load Diff

View File

@ -67,6 +67,7 @@ pub struct TcpRead {
/// Occurs when client couldn't read from the WebSocket to the engine. /// Occurs when client couldn't read from the WebSocket to the engine.
// #[derive(Debug)] // #[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum WebSocketReadError { pub enum WebSocketReadError {
/// Could not read a message due to WebSocket errors. /// Could not read a message due to WebSocket errors.
Read(tokio_tungstenite::tungstenite::Error), Read(tokio_tungstenite::tungstenite::Error),
@ -206,7 +207,7 @@ impl EngineConnection {
async fn inner_send_to_engine(request: WebSocketRequest, tcp_write: &mut WebSocketTcpWrite) -> Result<()> { async fn inner_send_to_engine(request: WebSocketRequest, tcp_write: &mut WebSocketTcpWrite) -> Result<()> {
let msg = serde_json::to_string(&request).map_err(|e| anyhow!("could not serialize json: {e}"))?; let msg = serde_json::to_string(&request).map_err(|e| anyhow!("could not serialize json: {e}"))?;
tcp_write tcp_write
.send(WsMsg::Text(msg)) .send(WsMsg::Text(msg.into()))
.await .await
.map_err(|e| anyhow!("could not send json over websocket: {e}"))?; .map_err(|e| anyhow!("could not send json over websocket: {e}"))?;
Ok(()) Ok(())
@ -216,19 +217,17 @@ impl EngineConnection {
async fn inner_send_to_engine_binary(request: WebSocketRequest, tcp_write: &mut WebSocketTcpWrite) -> Result<()> { async fn inner_send_to_engine_binary(request: WebSocketRequest, tcp_write: &mut WebSocketTcpWrite) -> Result<()> {
let msg = bson::to_vec(&request).map_err(|e| anyhow!("could not serialize bson: {e}"))?; let msg = bson::to_vec(&request).map_err(|e| anyhow!("could not serialize bson: {e}"))?;
tcp_write tcp_write
.send(WsMsg::Binary(msg)) .send(WsMsg::Binary(msg.into()))
.await .await
.map_err(|e| anyhow!("could not send json over websocket: {e}"))?; .map_err(|e| anyhow!("could not send json over websocket: {e}"))?;
Ok(()) Ok(())
} }
pub async fn new(ws: reqwest::Upgraded) -> Result<EngineConnection> { pub async fn new(ws: reqwest::Upgraded) -> Result<EngineConnection> {
let wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig { let wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
// 4294967296 bytes, which is around 4.2 GB. // 4294967296 bytes, which is around 4.2 GB.
max_message_size: Some(usize::MAX), .max_message_size(Some(usize::MAX))
max_frame_size: Some(usize::MAX), .max_frame_size(Some(usize::MAX));
..Default::default()
};
let ws_stream = tokio_tungstenite::WebSocketStream::from_raw_socket( let ws_stream = tokio_tungstenite::WebSocketStream::from_raw_socket(
ws, ws,
@ -439,7 +438,7 @@ impl EngineManager for EngineConnection {
request_sent: tx, request_sent: tx,
}) })
.await .await
.map_err(|e| KclError::Engine(KclErrorDetails::new(format!("Failed to send debug: {}", e), vec![])))?; .map_err(|e| KclError::new_engine(KclErrorDetails::new(format!("Failed to send debug: {}", e), vec![])))?;
let _ = rx.await; let _ = rx.await;
Ok(()) Ok(())
@ -474,7 +473,7 @@ impl EngineManager for EngineConnection {
}) })
.await .await
.map_err(|e| { .map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to send modeling command: {}", e), format!("Failed to send modeling command: {}", e),
vec![source_range], vec![source_range],
)) ))
@ -483,13 +482,13 @@ impl EngineManager for EngineConnection {
// Wait for the request to be sent. // Wait for the request to be sent.
rx.await rx.await
.map_err(|e| { .map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("could not send request to the engine actor: {e}"), format!("could not send request to the engine actor: {e}"),
vec![source_range], vec![source_range],
)) ))
})? })?
.map_err(|e| { .map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("could not send request to the engine: {e}"), format!("could not send request to the engine: {e}"),
vec![source_range], vec![source_range],
)) ))
@ -516,12 +515,12 @@ impl EngineManager for EngineConnection {
// Check if we have any pending errors. // Check if we have any pending errors.
let pe = self.pending_errors.read().await; let pe = self.pending_errors.read().await;
if !pe.is_empty() { if !pe.is_empty() {
return Err(KclError::Engine(KclErrorDetails::new( return Err(KclError::new_engine(KclErrorDetails::new(
pe.join(", ").to_string(), pe.join(", ").to_string(),
vec![source_range], vec![source_range],
))); )));
} else { } else {
return Err(KclError::Engine(KclErrorDetails::new( return Err(KclError::new_engine(KclErrorDetails::new(
"Modeling command failed: websocket closed early".to_string(), "Modeling command failed: websocket closed early".to_string(),
vec![source_range], vec![source_range],
))); )));
@ -543,7 +542,7 @@ impl EngineManager for EngineConnection {
} }
} }
Err(KclError::Engine(KclErrorDetails::new( Err(KclError::new_engine(KclErrorDetails::new(
format!("Modeling command timed out `{}`", id), format!("Modeling command timed out `{}`", id),
vec![source_range], vec![source_range],
))) )))

View File

@ -80,12 +80,12 @@ impl ResponseContext {
} }
// Add a response to the context. // Add a response to the context.
pub async fn send_response(&self, data: js_sys::Uint8Array) -> Result<(), JsValue> { pub async fn send_response(&self, data: js_sys::Uint8Array) {
let ws_result: WebSocketResponse = match bson::from_slice(&data.to_vec()) { let ws_result: WebSocketResponse = match bson::from_slice(&data.to_vec()) {
Ok(res) => res, Ok(res) => res,
Err(_) => { Err(_) => {
// We don't care about the error if we can't parse it. // We don't care about the error if we can't parse it.
return Ok(()); return;
} }
}; };
@ -96,13 +96,11 @@ impl ResponseContext {
let Some(id) = id else { let Some(id) = id else {
// We only care if we have an id. // We only care if we have an id.
return Ok(()); return;
}; };
// Add this response to our responses. // Add this response to our responses.
self.add(id, ws_result.clone()).await; self.add(id, ws_result.clone()).await;
Ok(())
} }
} }
@ -147,19 +145,19 @@ impl EngineConnection {
id_to_source_range: HashMap<uuid::Uuid, SourceRange>, id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
) -> Result<(), KclError> { ) -> Result<(), KclError> {
let source_range_str = serde_json::to_string(&source_range).map_err(|e| { let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to serialize source range: {:?}", e), format!("Failed to serialize source range: {:?}", e),
vec![source_range], vec![source_range],
)) ))
})?; })?;
let cmd_str = serde_json::to_string(&cmd).map_err(|e| { let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to serialize modeling command: {:?}", e), format!("Failed to serialize modeling command: {:?}", e),
vec![source_range], vec![source_range],
)) ))
})?; })?;
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| { let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to serialize id to source range: {:?}", e), format!("Failed to serialize id to source range: {:?}", e),
vec![source_range], vec![source_range],
)) ))
@ -167,7 +165,7 @@ impl EngineConnection {
self.manager self.manager
.fire_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str) .fire_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?; .map_err(|e| KclError::new_engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
Ok(()) Ok(())
} }
@ -180,19 +178,19 @@ impl EngineConnection {
id_to_source_range: HashMap<uuid::Uuid, SourceRange>, id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
) -> Result<WebSocketResponse, KclError> { ) -> Result<WebSocketResponse, KclError> {
let source_range_str = serde_json::to_string(&source_range).map_err(|e| { let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to serialize source range: {:?}", e), format!("Failed to serialize source range: {:?}", e),
vec![source_range], vec![source_range],
)) ))
})?; })?;
let cmd_str = serde_json::to_string(&cmd).map_err(|e| { let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to serialize modeling command: {:?}", e), format!("Failed to serialize modeling command: {:?}", e),
vec![source_range], vec![source_range],
)) ))
})?; })?;
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| { let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to serialize id to source range: {:?}", e), format!("Failed to serialize id to source range: {:?}", e),
vec![source_range], vec![source_range],
)) ))
@ -201,7 +199,7 @@ impl EngineConnection {
let promise = self let promise = self
.manager .manager
.send_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str) .send_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?; .map_err(|e| KclError::new_engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| { let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| {
// Try to parse the error as an engine error. // Try to parse the error as an engine error.
@ -209,7 +207,7 @@ impl EngineConnection {
if let Ok(kittycad_modeling_cmds::websocket::FailureWebSocketResponse { errors, .. }) = if let Ok(kittycad_modeling_cmds::websocket::FailureWebSocketResponse { errors, .. }) =
serde_json::from_str(&err_str) serde_json::from_str(&err_str)
{ {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"), errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
vec![source_range], vec![source_range],
)) ))
@ -218,7 +216,7 @@ impl EngineConnection {
{ {
if let Some(data) = data.first() { if let Some(data) = data.first() {
// It could also be an array of responses. // It could also be an array of responses.
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
data.errors data.errors
.iter() .iter()
.map(|e| e.message.clone()) .map(|e| e.message.clone())
@ -227,13 +225,13 @@ impl EngineConnection {
vec![source_range], vec![source_range],
)) ))
} else { } else {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
"Received empty response from engine".into(), "Received empty response from engine".into(),
vec![source_range], vec![source_range],
)) ))
} }
} else { } else {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to wait for promise from send modeling command: {:?}", e), format!("Failed to wait for promise from send modeling command: {:?}", e),
vec![source_range], vec![source_range],
)) ))
@ -241,7 +239,7 @@ impl EngineConnection {
})?; })?;
if value.is_null() || value.is_undefined() { if value.is_null() || value.is_undefined() {
return Err(KclError::Engine(KclErrorDetails::new( return Err(KclError::new_engine(KclErrorDetails::new(
"Received null or undefined response from engine".into(), "Received null or undefined response from engine".into(),
vec![source_range], vec![source_range],
))); )));
@ -251,7 +249,7 @@ impl EngineConnection {
let data = js_sys::Uint8Array::from(value); let data = js_sys::Uint8Array::from(value);
let ws_result: WebSocketResponse = bson::from_slice(&data.to_vec()).map_err(|e| { let ws_result: WebSocketResponse = bson::from_slice(&data.to_vec()).map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to deserialize bson response from engine: {:?}", e), format!("Failed to deserialize bson response from engine: {:?}", e),
vec![source_range], vec![source_range],
)) ))
@ -308,10 +306,10 @@ impl crate::engine::EngineManager for EngineConnection {
let promise = self let promise = self
.manager .manager
.start_new_session() .start_new_session()
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?; .map_err(|e| KclError::new_engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
crate::wasm::JsFuture::from(promise).await.map_err(|e| { crate::wasm::JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to wait for promise from start new session: {:?}", e), format!("Failed to wait for promise from start new session: {:?}", e),
vec![source_range], vec![source_range],
)) ))

View File

@ -276,7 +276,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
{ {
let duration = instant::Duration::from_millis(1); let duration = instant::Duration::from_millis(1);
wasm_timer::Delay::new(duration).await.map_err(|err| { wasm_timer::Delay::new(duration).await.map_err(|err| {
KclError::Internal(KclErrorDetails::new( KclError::new_internal(KclErrorDetails::new(
format!("Failed to sleep: {:?}", err), format!("Failed to sleep: {:?}", err),
vec![source_range], vec![source_range],
)) ))
@ -293,7 +293,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
return Ok(response); return Ok(response);
} }
Err(KclError::Engine(KclErrorDetails::new( Err(KclError::new_engine(KclErrorDetails::new(
"async command timed out".to_string(), "async command timed out".to_string(),
vec![source_range], vec![source_range],
))) )))
@ -547,7 +547,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
id_to_source_range.insert(Uuid::from(*cmd_id), *range); id_to_source_range.insert(Uuid::from(*cmd_id), *range);
} }
_ => { _ => {
return Err(KclError::Engine(KclErrorDetails::new( return Err(KclError::new_engine(KclErrorDetails::new(
format!("The request is not a modeling command: {:?}", req), format!("The request is not a modeling command: {:?}", req),
vec![*range], vec![*range],
))); )));
@ -595,7 +595,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
self.parse_batch_responses(last_id.into(), id_to_source_range, responses) self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
} else { } else {
// We should never get here. // We should never get here.
Err(KclError::Engine(KclErrorDetails::new( Err(KclError::new_engine(KclErrorDetails::new(
format!("Failed to get batch response: {:?}", response), format!("Failed to get batch response: {:?}", response),
vec![source_range], vec![source_range],
))) )))
@ -610,7 +610,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// request so we need the original request source range in case the engine returns // request so we need the original request source range in case the engine returns
// an error. // an error.
let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| { let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to get source range for command ID: {:?}", cmd_id), format!("Failed to get source range for command ID: {:?}", cmd_id),
vec![], vec![],
)) ))
@ -620,7 +620,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
.await?; .await?;
self.parse_websocket_response(ws_resp, source_range) self.parse_websocket_response(ws_resp, source_range)
} }
_ => Err(KclError::Engine(KclErrorDetails::new( _ => Err(KclError::new_engine(KclErrorDetails::new(
format!("The final request is not a modeling command: {:?}", final_req), format!("The final request is not a modeling command: {:?}", final_req),
vec![source_range], vec![source_range],
))), ))),
@ -729,7 +729,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
for (name, plane_id, color) in plane_settings { for (name, plane_id, color) in plane_settings {
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| { let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
// We should never get here. // We should never get here.
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to get default plane info for: {:?}", name), format!("Failed to get default plane info for: {:?}", name),
vec![source_range], vec![source_range],
)) ))
@ -763,7 +763,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
WebSocketResponse::Success(success) => Ok(success.resp), WebSocketResponse::Success(success) => Ok(success.resp),
WebSocketResponse::Failure(fail) => { WebSocketResponse::Failure(fail) => {
let _request_id = fail.request_id; let _request_id = fail.request_id;
Err(KclError::Engine(KclErrorDetails::new( Err(KclError::new_engine(KclErrorDetails::new(
fail.errors fail.errors
.iter() .iter()
.map(|e| e.message.clone()) .map(|e| e.message.clone())
@ -805,12 +805,12 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
BatchResponse::Failure { errors } => { BatchResponse::Failure { errors } => {
// Get the source range for the command. // Get the source range for the command.
let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| { let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to get source range for command ID: {:?}", cmd_id), format!("Failed to get source range for command ID: {:?}", cmd_id),
vec![], vec![],
)) ))
})?; })?;
return Err(KclError::Engine(KclErrorDetails::new( return Err(KclError::new_engine(KclErrorDetails::new(
errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"), errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
vec![source_range], vec![source_range],
))); )));
@ -820,7 +820,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Return an error that we did not get an error or the response we wanted. // Return an error that we did not get an error or the response we wanted.
// This should never happen but who knows. // This should never happen but who knows.
Err(KclError::Engine(KclErrorDetails::new( Err(KclError::new_engine(KclErrorDetails::new(
format!("Failed to find response for command ID: {:?}", id), format!("Failed to find response for command ID: {:?}", id),
vec![], vec![],
))) )))

View File

@ -91,30 +91,33 @@ pub enum ConnectionError {
#[ts(export)] #[ts(export)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum KclError { pub enum KclError {
#[error("lexical: {0:?}")] #[error("lexical: {details:?}")]
Lexical(KclErrorDetails), Lexical { details: KclErrorDetails },
#[error("syntax: {0:?}")] #[error("syntax: {details:?}")]
Syntax(KclErrorDetails), Syntax { details: KclErrorDetails },
#[error("semantic: {0:?}")] #[error("semantic: {details:?}")]
Semantic(KclErrorDetails), Semantic { details: KclErrorDetails },
#[error("import cycle: {0:?}")] #[error("import cycle: {details:?}")]
ImportCycle(KclErrorDetails), ImportCycle { details: KclErrorDetails },
#[error("type: {0:?}")] #[error("type: {details:?}")]
Type(KclErrorDetails), Type { details: KclErrorDetails },
#[error("i/o: {0:?}")] #[error("i/o: {details:?}")]
Io(KclErrorDetails), Io { details: KclErrorDetails },
#[error("unexpected: {0:?}")] #[error("unexpected: {details:?}")]
Unexpected(KclErrorDetails), Unexpected { details: KclErrorDetails },
#[error("value already defined: {0:?}")] #[error("value already defined: {details:?}")]
ValueAlreadyDefined(KclErrorDetails), ValueAlreadyDefined { details: KclErrorDetails },
#[error("undefined value: {0:?}")] #[error("undefined value: {details:?}")]
UndefinedValue(KclErrorDetails), UndefinedValue {
#[error("invalid expression: {0:?}")] details: KclErrorDetails,
InvalidExpression(KclErrorDetails), name: Option<String>,
#[error("engine: {0:?}")] },
Engine(KclErrorDetails), #[error("invalid expression: {details:?}")]
#[error("internal error, please report to KittyCAD team: {0:?}")] InvalidExpression { details: KclErrorDetails },
Internal(KclErrorDetails), #[error("engine: {details:?}")]
Engine { details: KclErrorDetails },
#[error("internal error, please report to KittyCAD team: {details:?}")]
Internal { details: KclErrorDetails },
} }
impl From<KclErrorWithOutputs> for KclError { impl From<KclErrorWithOutputs> for KclError {
@ -296,18 +299,18 @@ pub struct ReportWithOutputs {
impl miette::Diagnostic for ReportWithOutputs { impl miette::Diagnostic for ReportWithOutputs {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> { fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
let family = match self.error.error { let family = match self.error.error {
KclError::Lexical(_) => "Lexical", KclError::Lexical { .. } => "Lexical",
KclError::Syntax(_) => "Syntax", KclError::Syntax { .. } => "Syntax",
KclError::Semantic(_) => "Semantic", KclError::Semantic { .. } => "Semantic",
KclError::ImportCycle(_) => "ImportCycle", KclError::ImportCycle { .. } => "ImportCycle",
KclError::Type(_) => "Type", KclError::Type { .. } => "Type",
KclError::Io(_) => "I/O", KclError::Io { .. } => "I/O",
KclError::Unexpected(_) => "Unexpected", KclError::Unexpected { .. } => "Unexpected",
KclError::ValueAlreadyDefined(_) => "ValueAlreadyDefined", KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
KclError::UndefinedValue(_) => "UndefinedValue", KclError::UndefinedValue { .. } => "UndefinedValue",
KclError::InvalidExpression(_) => "InvalidExpression", KclError::InvalidExpression { .. } => "InvalidExpression",
KclError::Engine(_) => "Engine", KclError::Engine { .. } => "Engine",
KclError::Internal(_) => "Internal", KclError::Internal { .. } => "Internal",
}; };
let error_string = format!("KCL {family} error"); let error_string = format!("KCL {family} error");
Some(Box::new(error_string)) Some(Box::new(error_string))
@ -346,18 +349,18 @@ pub struct Report {
impl miette::Diagnostic for Report { impl miette::Diagnostic for Report {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> { fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
let family = match self.error { let family = match self.error {
KclError::Lexical(_) => "Lexical", KclError::Lexical { .. } => "Lexical",
KclError::Syntax(_) => "Syntax", KclError::Syntax { .. } => "Syntax",
KclError::Semantic(_) => "Semantic", KclError::Semantic { .. } => "Semantic",
KclError::ImportCycle(_) => "ImportCycle", KclError::ImportCycle { .. } => "ImportCycle",
KclError::Type(_) => "Type", KclError::Type { .. } => "Type",
KclError::Io(_) => "I/O", KclError::Io { .. } => "I/O",
KclError::Unexpected(_) => "Unexpected", KclError::Unexpected { .. } => "Unexpected",
KclError::ValueAlreadyDefined(_) => "ValueAlreadyDefined", KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
KclError::UndefinedValue(_) => "UndefinedValue", KclError::UndefinedValue { .. } => "UndefinedValue",
KclError::InvalidExpression(_) => "InvalidExpression", KclError::InvalidExpression { .. } => "InvalidExpression",
KclError::Engine(_) => "Engine", KclError::Engine { .. } => "Engine",
KclError::Internal(_) => "Internal", KclError::Internal { .. } => "Internal",
}; };
let error_string = format!("KCL {family} error"); let error_string = format!("KCL {family} error");
Some(Box::new(error_string)) Some(Box::new(error_string))
@ -410,11 +413,53 @@ impl KclErrorDetails {
impl KclError { impl KclError {
pub fn internal(message: String) -> KclError { pub fn internal(message: String) -> KclError {
KclError::Internal(KclErrorDetails { KclError::Internal {
source_ranges: Default::default(), details: KclErrorDetails {
backtrace: Default::default(), source_ranges: Default::default(),
message, backtrace: Default::default(),
}) message,
},
}
}
pub fn new_internal(details: KclErrorDetails) -> KclError {
KclError::Internal { details }
}
pub fn new_import_cycle(details: KclErrorDetails) -> KclError {
KclError::ImportCycle { details }
}
pub fn new_semantic(details: KclErrorDetails) -> KclError {
KclError::Semantic { details }
}
pub fn new_value_already_defined(details: KclErrorDetails) -> KclError {
KclError::ValueAlreadyDefined { details }
}
pub fn new_syntax(details: KclErrorDetails) -> KclError {
KclError::Syntax { details }
}
pub fn new_io(details: KclErrorDetails) -> KclError {
KclError::Io { details }
}
pub fn new_engine(details: KclErrorDetails) -> KclError {
KclError::Engine { details }
}
pub fn new_lexical(details: KclErrorDetails) -> KclError {
KclError::Lexical { details }
}
pub fn new_undefined_value(details: KclErrorDetails, name: Option<String>) -> KclError {
KclError::UndefinedValue { details, name }
}
pub fn new_type(details: KclErrorDetails) -> KclError {
KclError::Type { details }
} }
/// Get the error message. /// Get the error message.
@ -424,88 +469,88 @@ impl KclError {
pub fn error_type(&self) -> &'static str { pub fn error_type(&self) -> &'static str {
match self { match self {
KclError::Lexical(_) => "lexical", KclError::Lexical { .. } => "lexical",
KclError::Syntax(_) => "syntax", KclError::Syntax { .. } => "syntax",
KclError::Semantic(_) => "semantic", KclError::Semantic { .. } => "semantic",
KclError::ImportCycle(_) => "import cycle", KclError::ImportCycle { .. } => "import cycle",
KclError::Type(_) => "type", KclError::Type { .. } => "type",
KclError::Io(_) => "i/o", KclError::Io { .. } => "i/o",
KclError::Unexpected(_) => "unexpected", KclError::Unexpected { .. } => "unexpected",
KclError::ValueAlreadyDefined(_) => "value already defined", KclError::ValueAlreadyDefined { .. } => "value already defined",
KclError::UndefinedValue(_) => "undefined value", KclError::UndefinedValue { .. } => "undefined value",
KclError::InvalidExpression(_) => "invalid expression", KclError::InvalidExpression { .. } => "invalid expression",
KclError::Engine(_) => "engine", KclError::Engine { .. } => "engine",
KclError::Internal(_) => "internal", KclError::Internal { .. } => "internal",
} }
} }
pub fn source_ranges(&self) -> Vec<SourceRange> { pub fn source_ranges(&self) -> Vec<SourceRange> {
match &self { match &self {
KclError::Lexical(e) => e.source_ranges.clone(), KclError::Lexical { details: e } => e.source_ranges.clone(),
KclError::Syntax(e) => e.source_ranges.clone(), KclError::Syntax { details: e } => e.source_ranges.clone(),
KclError::Semantic(e) => e.source_ranges.clone(), KclError::Semantic { details: e } => e.source_ranges.clone(),
KclError::ImportCycle(e) => e.source_ranges.clone(), KclError::ImportCycle { details: e } => e.source_ranges.clone(),
KclError::Type(e) => e.source_ranges.clone(), KclError::Type { details: e } => e.source_ranges.clone(),
KclError::Io(e) => e.source_ranges.clone(), KclError::Io { details: e } => e.source_ranges.clone(),
KclError::Unexpected(e) => e.source_ranges.clone(), KclError::Unexpected { details: e } => e.source_ranges.clone(),
KclError::ValueAlreadyDefined(e) => e.source_ranges.clone(), KclError::ValueAlreadyDefined { details: e } => e.source_ranges.clone(),
KclError::UndefinedValue(e) => e.source_ranges.clone(), KclError::UndefinedValue { details: e, .. } => e.source_ranges.clone(),
KclError::InvalidExpression(e) => e.source_ranges.clone(), KclError::InvalidExpression { details: e } => e.source_ranges.clone(),
KclError::Engine(e) => e.source_ranges.clone(), KclError::Engine { details: e } => e.source_ranges.clone(),
KclError::Internal(e) => e.source_ranges.clone(), KclError::Internal { details: e } => e.source_ranges.clone(),
} }
} }
/// Get the inner error message. /// Get the inner error message.
pub fn message(&self) -> &str { pub fn message(&self) -> &str {
match &self { match &self {
KclError::Lexical(e) => &e.message, KclError::Lexical { details: e } => &e.message,
KclError::Syntax(e) => &e.message, KclError::Syntax { details: e } => &e.message,
KclError::Semantic(e) => &e.message, KclError::Semantic { details: e } => &e.message,
KclError::ImportCycle(e) => &e.message, KclError::ImportCycle { details: e } => &e.message,
KclError::Type(e) => &e.message, KclError::Type { details: e } => &e.message,
KclError::Io(e) => &e.message, KclError::Io { details: e } => &e.message,
KclError::Unexpected(e) => &e.message, KclError::Unexpected { details: e } => &e.message,
KclError::ValueAlreadyDefined(e) => &e.message, KclError::ValueAlreadyDefined { details: e } => &e.message,
KclError::UndefinedValue(e) => &e.message, KclError::UndefinedValue { details: e, .. } => &e.message,
KclError::InvalidExpression(e) => &e.message, KclError::InvalidExpression { details: e } => &e.message,
KclError::Engine(e) => &e.message, KclError::Engine { details: e } => &e.message,
KclError::Internal(e) => &e.message, KclError::Internal { details: e } => &e.message,
} }
} }
pub fn backtrace(&self) -> Vec<BacktraceItem> { pub fn backtrace(&self) -> Vec<BacktraceItem> {
match self { match self {
KclError::Lexical(e) KclError::Lexical { details: e }
| KclError::Syntax(e) | KclError::Syntax { details: e }
| KclError::Semantic(e) | KclError::Semantic { details: e }
| KclError::ImportCycle(e) | KclError::ImportCycle { details: e }
| KclError::Type(e) | KclError::Type { details: e }
| KclError::Io(e) | KclError::Io { details: e }
| KclError::Unexpected(e) | KclError::Unexpected { details: e }
| KclError::ValueAlreadyDefined(e) | KclError::ValueAlreadyDefined { details: e }
| KclError::UndefinedValue(e) | KclError::UndefinedValue { details: e, .. }
| KclError::InvalidExpression(e) | KclError::InvalidExpression { details: e }
| KclError::Engine(e) | KclError::Engine { details: e }
| KclError::Internal(e) => e.backtrace.clone(), | KclError::Internal { details: e } => e.backtrace.clone(),
} }
} }
pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self { pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
let mut new = self.clone(); let mut new = self.clone();
match &mut new { match &mut new {
KclError::Lexical(e) KclError::Lexical { details: e }
| KclError::Syntax(e) | KclError::Syntax { details: e }
| KclError::Semantic(e) | KclError::Semantic { details: e }
| KclError::ImportCycle(e) | KclError::ImportCycle { details: e }
| KclError::Type(e) | KclError::Type { details: e }
| KclError::Io(e) | KclError::Io { details: e }
| KclError::Unexpected(e) | KclError::Unexpected { details: e }
| KclError::ValueAlreadyDefined(e) | KclError::ValueAlreadyDefined { details: e }
| KclError::UndefinedValue(e) | KclError::UndefinedValue { details: e, .. }
| KclError::InvalidExpression(e) | KclError::InvalidExpression { details: e }
| KclError::Engine(e) | KclError::Engine { details: e }
| KclError::Internal(e) => { | KclError::Internal { details: e } => {
e.backtrace = source_ranges e.backtrace = source_ranges
.iter() .iter()
.map(|s| BacktraceItem { .map(|s| BacktraceItem {
@ -520,45 +565,21 @@ impl KclError {
new new
} }
pub(crate) fn set_last_backtrace_fn_name(&self, last_fn_name: Option<String>) -> Self {
let mut new = self.clone();
match &mut new {
KclError::Lexical(e)
| KclError::Syntax(e)
| KclError::Semantic(e)
| KclError::ImportCycle(e)
| KclError::Type(e)
| KclError::Io(e)
| KclError::Unexpected(e)
| KclError::ValueAlreadyDefined(e)
| KclError::UndefinedValue(e)
| KclError::InvalidExpression(e)
| KclError::Engine(e)
| KclError::Internal(e) => {
if let Some(item) = e.backtrace.last_mut() {
item.fn_name = last_fn_name;
}
}
}
new
}
pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self { pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
let mut new = self.clone(); let mut new = self.clone();
match &mut new { match &mut new {
KclError::Lexical(e) KclError::Lexical { details: e }
| KclError::Syntax(e) | KclError::Syntax { details: e }
| KclError::Semantic(e) | KclError::Semantic { details: e }
| KclError::ImportCycle(e) | KclError::ImportCycle { details: e }
| KclError::Type(e) | KclError::Type { details: e }
| KclError::Io(e) | KclError::Io { details: e }
| KclError::Unexpected(e) | KclError::Unexpected { details: e }
| KclError::ValueAlreadyDefined(e) | KclError::ValueAlreadyDefined { details: e }
| KclError::UndefinedValue(e) | KclError::UndefinedValue { details: e, .. }
| KclError::InvalidExpression(e) | KclError::InvalidExpression { details: e }
| KclError::Engine(e) | KclError::Engine { details: e }
| KclError::Internal(e) => { | KclError::Internal { details: e } => {
if let Some(item) = e.backtrace.last_mut() { if let Some(item) = e.backtrace.last_mut() {
item.fn_name = last_fn_name; item.fn_name = last_fn_name;
} }
@ -645,7 +666,7 @@ impl From<String> for KclError {
#[cfg(feature = "pyo3")] #[cfg(feature = "pyo3")]
impl From<pyo3::PyErr> for KclError { impl From<pyo3::PyErr> for KclError {
fn from(error: pyo3::PyErr) -> Self { fn from(error: pyo3::PyErr) -> Self {
KclError::Internal(KclErrorDetails { KclError::new_internal(KclErrorDetails {
source_ranges: vec![], source_ranges: vec![],
backtrace: Default::default(), backtrace: Default::default(),
message: error.to_string(), message: error.to_string(),

View File

@ -70,7 +70,7 @@ pub(super) fn expect_properties<'a>(
) -> Result<&'a [Node<ObjectProperty>], KclError> { ) -> Result<&'a [Node<ObjectProperty>], KclError> {
assert_eq!(annotation.name().unwrap(), for_key); assert_eq!(annotation.name().unwrap(), for_key);
Ok(&**annotation.properties.as_ref().ok_or_else(|| { Ok(&**annotation.properties.as_ref().ok_or_else(|| {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!("Empty `{for_key}` annotation"), format!("Empty `{for_key}` annotation"),
vec![annotation.as_source_range()], vec![annotation.as_source_range()],
)) ))
@ -84,7 +84,7 @@ pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
} }
} }
Err(KclError::Semantic(KclErrorDetails::new( Err(KclError::new_semantic(KclErrorDetails::new(
"Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(), "Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
vec![expr.into()], vec![expr.into()],
))) )))
@ -98,7 +98,7 @@ pub(super) fn expect_number(expr: &Expr) -> Result<String, KclError> {
} }
} }
Err(KclError::Semantic(KclErrorDetails::new( Err(KclError::new_semantic(KclErrorDetails::new(
"Unexpected settings value, expected a number, e.g., `1.0`".to_owned(), "Unexpected settings value, expected a number, e.g., `1.0`".to_owned(),
vec![expr.into()], vec![expr.into()],
))) )))
@ -113,7 +113,7 @@ pub(super) fn get_impl(annotations: &[Node<Annotation>], source_range: SourceRan
if &*p.key.name == IMPL { if &*p.key.name == IMPL {
if let Some(s) = p.value.ident_name() { if let Some(s) = p.value.ident_name() {
return Impl::from_str(s).map(Some).map_err(|_| { return Impl::from_str(s).map(Some).map_err(|_| {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"Invalid value for {} attribute, expected one of: {}", "Invalid value for {} attribute, expected one of: {}",
IMPL, IMPL,
@ -139,7 +139,7 @@ impl UnitLen {
"inch" | "in" => Ok(UnitLen::Inches), "inch" | "in" => Ok(UnitLen::Inches),
"ft" => Ok(UnitLen::Feet), "ft" => Ok(UnitLen::Feet),
"yd" => Ok(UnitLen::Yards), "yd" => Ok(UnitLen::Yards),
value => Err(KclError::Semantic(KclErrorDetails::new( value => Err(KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"Unexpected value for length units: `{value}`; expected one of `mm`, `cm`, `m`, `in`, `ft`, `yd`" "Unexpected value for length units: `{value}`; expected one of `mm`, `cm`, `m`, `in`, `ft`, `yd`"
), ),
@ -154,7 +154,7 @@ impl UnitAngle {
match s { match s {
"deg" => Ok(UnitAngle::Degrees), "deg" => Ok(UnitAngle::Degrees),
"rad" => Ok(UnitAngle::Radians), "rad" => Ok(UnitAngle::Radians),
value => Err(KclError::Semantic(KclErrorDetails::new( value => Err(KclError::new_semantic(KclErrorDetails::new(
format!("Unexpected value for angle units: `{value}`; expected one of `deg`, `rad`"), format!("Unexpected value for angle units: `{value}`; expected one of `deg`, `rad`"),
vec![source_range], vec![source_range],
))), ))),

View File

@ -24,7 +24,7 @@ macro_rules! internal_error {
($range:expr, $($rest:tt)*) => {{ ($range:expr, $($rest:tt)*) => {{
let message = format!($($rest)*); let message = format!($($rest)*);
debug_assert!(false, "{}", &message); debug_assert!(false, "{}", &message);
return Err(KclError::Internal(KclErrorDetails::new(message, vec![$range]))); return Err(KclError::new_internal(KclErrorDetails::new(message, vec![$range])));
}}; }};
} }
@ -676,6 +676,7 @@ impl EdgeCut {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ArtifactGraph { pub struct ArtifactGraph {
map: IndexMap<ArtifactId, Artifact>, map: IndexMap<ArtifactId, Artifact>,
pub(super) item_count: usize,
} }
impl ArtifactGraph { impl ArtifactGraph {
@ -711,10 +712,10 @@ pub(super) fn build_artifact_graph(
artifact_commands: &[ArtifactCommand], artifact_commands: &[ArtifactCommand],
responses: &IndexMap<Uuid, WebSocketResponse>, responses: &IndexMap<Uuid, WebSocketResponse>,
ast: &Node<Program>, ast: &Node<Program>,
cached_body_items: usize,
exec_artifacts: &mut IndexMap<ArtifactId, Artifact>, exec_artifacts: &mut IndexMap<ArtifactId, Artifact>,
initial_graph: ArtifactGraph, initial_graph: ArtifactGraph,
) -> Result<ArtifactGraph, KclError> { ) -> Result<ArtifactGraph, KclError> {
let item_count = initial_graph.item_count;
let mut map = initial_graph.into_map(); let mut map = initial_graph.into_map();
let mut path_to_plane_id_map = FnvHashMap::default(); let mut path_to_plane_id_map = FnvHashMap::default();
@ -725,7 +726,7 @@ pub(super) fn build_artifact_graph(
for exec_artifact in exec_artifacts.values_mut() { for exec_artifact in exec_artifacts.values_mut() {
// Note: We only have access to the new AST. So if these artifacts // Note: We only have access to the new AST. So if these artifacts
// somehow came from cached AST, this won't fill in anything. // somehow came from cached AST, this won't fill in anything.
fill_in_node_paths(exec_artifact, ast, cached_body_items); fill_in_node_paths(exec_artifact, ast, item_count);
} }
for artifact_command in artifact_commands { for artifact_command in artifact_commands {
@ -752,7 +753,7 @@ pub(super) fn build_artifact_graph(
&flattened_responses, &flattened_responses,
&path_to_plane_id_map, &path_to_plane_id_map,
ast, ast,
cached_body_items, item_count,
exec_artifacts, exec_artifacts,
)?; )?;
for artifact in artifact_updates { for artifact in artifact_updates {
@ -765,7 +766,10 @@ pub(super) fn build_artifact_graph(
merge_artifact_into_map(&mut map, exec_artifact.clone()); merge_artifact_into_map(&mut map, exec_artifact.clone());
} }
Ok(ArtifactGraph { map }) Ok(ArtifactGraph {
map,
item_count: item_count + ast.body.len(),
})
} }
/// These may have been created with placeholder `CodeRef`s because we didn't /// These may have been created with placeholder `CodeRef`s because we didn't
@ -949,7 +953,7 @@ fn artifacts_to_update(
ModelingCmd::StartPath(_) => { ModelingCmd::StartPath(_) => {
let mut return_arr = Vec::new(); let mut return_arr = Vec::new();
let current_plane_id = path_to_plane_id_map.get(&artifact_command.cmd_id).ok_or_else(|| { let current_plane_id = path_to_plane_id_map.get(&artifact_command.cmd_id).ok_or_else(|| {
KclError::Internal(KclErrorDetails::new( KclError::new_internal(KclErrorDetails::new(
format!("Expected a current plane ID when processing StartPath command, but we have none: {id:?}"), format!("Expected a current plane ID when processing StartPath command, but we have none: {id:?}"),
vec![range], vec![range],
)) ))
@ -1137,7 +1141,7 @@ fn artifacts_to_update(
// TODO: Using the first one. Make sure to revisit this // TODO: Using the first one. Make sure to revisit this
// choice, don't think it matters for now. // choice, don't think it matters for now.
path_id: ArtifactId::new(*loft_cmd.section_ids.first().ok_or_else(|| { path_id: ArtifactId::new(*loft_cmd.section_ids.first().ok_or_else(|| {
KclError::Internal(KclErrorDetails::new( KclError::new_internal(KclErrorDetails::new(
format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"), format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"),
vec![range], vec![range],
)) ))
@ -1180,7 +1184,7 @@ fn artifacts_to_update(
}; };
last_path = Some(path); last_path = Some(path);
let path_sweep_id = path.sweep_id.ok_or_else(|| { let path_sweep_id = path.sweep_id.ok_or_else(|| {
KclError::Internal(KclErrorDetails::new( KclError::new_internal(KclErrorDetails::new(
format!( format!(
"Expected a sweep ID on the path when processing Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}" "Expected a sweep ID on the path when processing Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
), ),
@ -1234,7 +1238,7 @@ fn artifacts_to_update(
continue; continue;
}; };
let path_sweep_id = path.sweep_id.ok_or_else(|| { let path_sweep_id = path.sweep_id.ok_or_else(|| {
KclError::Internal(KclErrorDetails::new( KclError::new_internal(KclErrorDetails::new(
format!( format!(
"Expected a sweep ID on the path when processing last path's Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}" "Expected a sweep ID on the path when processing last path's Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
), ),

View File

@ -6,25 +6,31 @@ use itertools::{EitherOrBoth, Itertools};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::{ use crate::{
execution::{annotations, memory::Stack, state::ModuleInfoMap, EnvironmentRef, ExecState, ExecutorSettings}, execution::{
annotations,
memory::Stack,
state::{self as exec_state, ModuleInfoMap},
EnvironmentRef, ExecutorSettings,
},
parsing::ast::types::{Annotation, Node, Program}, parsing::ast::types::{Annotation, Node, Program},
walk::Node as WalkNode, walk::Node as WalkNode,
ExecOutcome, ExecutorContext,
}; };
lazy_static::lazy_static! { lazy_static::lazy_static! {
/// A static mutable lock for updating the last successful execution state for the cache. /// A static mutable lock for updating the last successful execution state for the cache.
static ref OLD_AST: Arc<RwLock<Option<OldAstState>>> = Default::default(); static ref OLD_AST: Arc<RwLock<Option<GlobalState>>> = Default::default();
// The last successful run's memory. Not cleared after an unssuccessful run. // The last successful run's memory. Not cleared after an unssuccessful run.
static ref PREV_MEMORY: Arc<RwLock<Option<(Stack, ModuleInfoMap)>>> = Default::default(); static ref PREV_MEMORY: Arc<RwLock<Option<(Stack, ModuleInfoMap)>>> = Default::default();
} }
/// Read the old ast memory from the lock. /// Read the old ast memory from the lock.
pub(crate) async fn read_old_ast() -> Option<OldAstState> { pub(super) async fn read_old_ast() -> Option<GlobalState> {
let old_ast = OLD_AST.read().await; let old_ast = OLD_AST.read().await;
old_ast.clone() old_ast.clone()
} }
pub(super) async fn write_old_ast(old_state: OldAstState) { pub(super) async fn write_old_ast(old_state: GlobalState) {
let mut old_ast = OLD_AST.write().await; let mut old_ast = OLD_AST.write().await;
*old_ast = Some(old_state); *old_ast = Some(old_state);
} }
@ -34,7 +40,7 @@ pub(crate) async fn read_old_memory() -> Option<(Stack, ModuleInfoMap)> {
old_mem.clone() old_mem.clone()
} }
pub(super) async fn write_old_memory(mem: (Stack, ModuleInfoMap)) { pub(crate) async fn write_old_memory(mem: (Stack, ModuleInfoMap)) {
let mut old_mem = PREV_MEMORY.write().await; let mut old_mem = PREV_MEMORY.write().await;
*old_mem = Some(mem); *old_mem = Some(mem);
} }
@ -56,16 +62,73 @@ pub struct CacheInformation<'a> {
pub settings: &'a ExecutorSettings, pub settings: &'a ExecutorSettings,
} }
/// The old ast and program memory. /// The cached state of the whole program.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct OldAstState { pub(super) struct GlobalState {
/// The ast. pub(super) main: ModuleState,
pub ast: Node<Program>,
/// The exec state. /// The exec state.
pub exec_state: ExecState, pub(super) exec_state: exec_state::GlobalState,
/// The last settings used for execution. /// The last settings used for execution.
pub settings: crate::execution::ExecutorSettings, pub(super) settings: ExecutorSettings,
pub result_env: EnvironmentRef, }
impl GlobalState {
pub fn new(
state: exec_state::ExecState,
settings: ExecutorSettings,
ast: Node<Program>,
result_env: EnvironmentRef,
) -> Self {
Self {
main: ModuleState {
ast,
exec_state: state.mod_local,
result_env,
},
exec_state: state.global,
settings,
}
}
pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
self.settings = settings;
self
}
pub fn reconstitute_exec_state(&self) -> exec_state::ExecState {
exec_state::ExecState {
global: self.exec_state.clone(),
mod_local: self.main.exec_state.clone(),
}
}
pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState.
ExecOutcome {
variables: self.main.exec_state.variables(self.main.result_env),
filenames: self.exec_state.filenames(),
#[cfg(feature = "artifact-graph")]
operations: self.exec_state.artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_commands: self.exec_state.artifacts.commands,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.exec_state.artifacts.graph,
errors: self.exec_state.errors,
default_planes: ctx.engine.get_default_planes().read().await.clone(),
}
}
}
/// Per-module cached state
#[derive(Debug, Clone)]
pub(super) struct ModuleState {
/// The AST of the module.
pub(super) ast: Node<Program>,
/// The ExecState of the module.
pub(super) exec_state: exec_state::ModuleState,
/// The memory env for the module.
pub(super) result_env: EnvironmentRef,
} }
/// The result of a cache check. /// The result of a cache check.
@ -79,9 +142,6 @@ pub(super) enum CacheResult {
reapply_settings: bool, reapply_settings: bool,
/// The program that needs to be executed. /// The program that needs to be executed.
program: Node<Program>, program: Node<Program>,
/// The number of body items that were cached and omitted from the
/// program that needs to be executed. Used to compute [`crate::NodePath`].
cached_body_items: usize,
}, },
/// Check only the imports, and not the main program. /// Check only the imports, and not the main program.
/// Before sending this we already checked the main program and it is the same. /// Before sending this we already checked the main program and it is the same.
@ -146,7 +206,6 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf
// We know they have the same imports because the ast is the same. // We know they have the same imports because the ast is the same.
// If we have no imports, we can skip this. // If we have no imports, we can skip this.
if !old.ast.has_import_statements() { if !old.ast.has_import_statements() {
println!("No imports, no need to check.");
return CacheResult::NoAction(reapply_settings); return CacheResult::NoAction(reapply_settings);
} }
@ -194,7 +253,6 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf
clear_scene: true, clear_scene: true,
reapply_settings: true, reapply_settings: true,
program: new.ast.clone(), program: new.ast.clone(),
cached_body_items: 0,
}; };
} }
@ -223,7 +281,6 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
clear_scene: true, clear_scene: true,
reapply_settings, reapply_settings,
program: new_ast, program: new_ast,
cached_body_items: 0,
}; };
} }
@ -244,7 +301,6 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
clear_scene: true, clear_scene: true,
reapply_settings, reapply_settings,
program: new_ast, program: new_ast,
cached_body_items: 0,
} }
} }
std::cmp::Ordering::Greater => { std::cmp::Ordering::Greater => {
@ -261,7 +317,6 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
clear_scene: false, clear_scene: false,
reapply_settings, reapply_settings,
program: new_ast, program: new_ast,
cached_body_items: old_ast.body.len(),
} }
} }
std::cmp::Ordering::Equal => { std::cmp::Ordering::Equal => {
@ -600,7 +655,6 @@ startSketchOn(XY)
clear_scene: true, clear_scene: true,
reapply_settings: true, reapply_settings: true,
program: new_program.ast, program: new_program.ast,
cached_body_items: 0,
} }
); );
} }
@ -639,7 +693,6 @@ startSketchOn(XY)
clear_scene: true, clear_scene: true,
reapply_settings: true, reapply_settings: true,
program: new_program.ast, program: new_program.ast,
cached_body_items: 0,
} }
); );
} }

View File

@ -1,13 +1,12 @@
use indexmap::IndexMap; use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::Serialize; use serde::Serialize;
use super::{types::NumericType, ArtifactId, KclValue}; use super::{types::NumericType, ArtifactId, KclValue};
use crate::{ModuleId, SourceRange}; use crate::{ModuleId, NodePath, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations /// A CAD modeling operation for display in the feature tree, AKA operations
/// timeline. /// timeline.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")] #[ts(export_to = "Operation.ts")]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum Operation { pub enum Operation {
@ -18,6 +17,8 @@ pub enum Operation {
unlabeled_arg: Option<OpArg>, unlabeled_arg: Option<OpArg>,
/// The labeled keyword arguments to the function. /// The labeled keyword arguments to the function.
labeled_args: IndexMap<String, OpArg>, labeled_args: IndexMap<String, OpArg>,
/// The node path of the operation in the source code.
node_path: NodePath,
/// The source range of the operation in the source code. /// The source range of the operation in the source code.
source_range: SourceRange, source_range: SourceRange,
/// True if the operation resulted in an error. /// True if the operation resulted in an error.
@ -28,6 +29,8 @@ pub enum Operation {
GroupBegin { GroupBegin {
/// The details of the group. /// The details of the group.
group: Group, group: Group,
/// The node path of the operation in the source code.
node_path: NodePath,
/// The source range of the operation in the source code. /// The source range of the operation in the source code.
source_range: SourceRange, source_range: SourceRange,
}, },
@ -64,7 +67,7 @@ impl Operation {
} }
} }
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")] #[ts(export_to = "Operation.ts")]
#[serde(tag = "type")] #[serde(tag = "type")]
#[expect(clippy::large_enum_variant)] #[expect(clippy::large_enum_variant)]
@ -95,7 +98,7 @@ pub enum Group {
} }
/// An argument to a CAD modeling operation. /// An argument to a CAD modeling operation.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")] #[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct OpArg { pub struct OpArg {
@ -119,7 +122,7 @@ fn is_false(b: &bool) -> bool {
/// A KCL value used in Operations. `ArtifactId`s are used to refer to the /// A KCL value used in Operations. `ArtifactId`s are used to refer to the
/// actual scene objects. Any data not needed in the UI may be omitted. /// actual scene objects. Any data not needed in the UI may be omitted.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")] #[ts(export_to = "Operation.ts")]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum OpKclValue { pub enum OpKclValue {
@ -177,21 +180,21 @@ pub enum OpKclValue {
pub type OpKclObjectFields = IndexMap<String, OpKclValue>; pub type OpKclObjectFields = IndexMap<String, OpKclValue>;
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")] #[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct OpSketch { pub struct OpSketch {
artifact_id: ArtifactId, artifact_id: ArtifactId,
} }
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")] #[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct OpSolid { pub struct OpSolid {
artifact_id: ArtifactId, artifact_id: ArtifactId,
} }
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")] #[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct OpHelix { pub struct OpHelix {

View File

@ -19,8 +19,8 @@ use crate::{
parsing::ast::types::{ parsing::ast::types::{
Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryOperator, Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryOperator,
BinaryPart, BodyItem, Expr, IfExpression, ImportPath, ImportSelector, ItemVisibility, LiteralIdentifier, BinaryPart, BodyItem, Expr, IfExpression, ImportPath, ImportSelector, ItemVisibility, LiteralIdentifier,
LiteralValue, MemberExpression, MemberObject, Name, Node, NodeRef, ObjectExpression, PipeExpression, Program, LiteralValue, MemberExpression, Name, Node, NodeRef, ObjectExpression, PipeExpression, Program, TagDeclarator,
TagDeclarator, Type, UnaryExpression, UnaryOperator, Type, UnaryExpression, UnaryOperator,
}, },
source_range::SourceRange, source_range::SourceRange,
std::args::TyF64, std::args::TyF64,
@ -131,7 +131,7 @@ impl ExecutorContext {
match statement { match statement {
BodyItem::ImportStatement(import_stmt) => { BodyItem::ImportStatement(import_stmt) => {
if !matches!(body_type, BodyType::Root) { if !matches!(body_type, BodyType::Root) {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
"Imports are only supported at the top-level of a file.".to_owned(), "Imports are only supported at the top-level of a file.".to_owned(),
vec![import_stmt.into()], vec![import_stmt.into()],
))); )));
@ -164,15 +164,18 @@ impl ExecutorContext {
let mut mod_value = mem.get_from(&mod_name, env_ref, import_item.into(), 0).cloned(); let mut mod_value = mem.get_from(&mod_name, env_ref, import_item.into(), 0).cloned();
if value.is_err() && ty.is_err() && mod_value.is_err() { if value.is_err() && ty.is_err() && mod_value.is_err() {
return Err(KclError::UndefinedValue(KclErrorDetails::new( return Err(KclError::new_undefined_value(
format!("{} is not defined in module", import_item.name.name), KclErrorDetails::new(
vec![SourceRange::from(&import_item.name)], format!("{} is not defined in module", import_item.name.name),
))); vec![SourceRange::from(&import_item.name)],
),
None,
));
} }
// Check that the item is allowed to be imported (in at least one namespace). // Check that the item is allowed to be imported (in at least one namespace).
if value.is_ok() && !module_exports.contains(&import_item.name.name) { if value.is_ok() && !module_exports.contains(&import_item.name.name) {
value = Err(KclError::Semantic(KclErrorDetails::new( value = Err(KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.", "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
import_item.name.name import_item.name.name
@ -182,7 +185,7 @@ impl ExecutorContext {
} }
if ty.is_ok() && !module_exports.contains(&ty_name) { if ty.is_ok() && !module_exports.contains(&ty_name) {
ty = Err(KclError::Semantic(KclErrorDetails::new(format!( ty = Err(KclError::new_semantic(KclErrorDetails::new(format!(
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.", "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
import_item.name.name import_item.name.name
), ),
@ -190,7 +193,7 @@ impl ExecutorContext {
} }
if mod_value.is_ok() && !module_exports.contains(&mod_name) { if mod_value.is_ok() && !module_exports.contains(&mod_name) {
mod_value = Err(KclError::Semantic(KclErrorDetails::new(format!( mod_value = Err(KclError::new_semantic(KclErrorDetails::new(format!(
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.", "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
import_item.name.name import_item.name.name
), ),
@ -253,7 +256,7 @@ impl ExecutorContext {
.memory .memory
.get_from(name, env_ref, source_range, 0) .get_from(name, env_ref, source_range, 0)
.map_err(|_err| { .map_err(|_err| {
KclError::Internal(KclErrorDetails::new( KclError::new_internal(KclErrorDetails::new(
format!("{} is not defined in module (but was exported?)", name), format!("{} is not defined in module (but was exported?)", name),
vec![source_range], vec![source_range],
)) ))
@ -301,7 +304,12 @@ impl ExecutorContext {
let annotations = &variable_declaration.outer_attrs; let annotations = &variable_declaration.outer_attrs;
let value = self // During the evaluation of the variable's RHS, set context that this is all happening inside a variable
// declaration, for the given name. This helps improve user-facing error messages.
let lhs = variable_declaration.inner.name().to_owned();
let prev_being_declared = exec_state.mod_local.being_declared.take();
exec_state.mod_local.being_declared = Some(lhs);
let rhs_result = self
.execute_expr( .execute_expr(
&variable_declaration.declaration.init, &variable_declaration.declaration.init,
exec_state, exec_state,
@ -309,10 +317,14 @@ impl ExecutorContext {
annotations, annotations,
StatementKind::Declaration { name: &var_name }, StatementKind::Declaration { name: &var_name },
) )
.await?; .await;
// Declaration over, so unset this context.
exec_state.mod_local.being_declared = prev_being_declared;
let rhs = rhs_result?;
exec_state exec_state
.mut_stack() .mut_stack()
.add(var_name.clone(), value.clone(), source_range)?; .add(var_name.clone(), rhs.clone(), source_range)?;
// Track exports. // Track exports.
if let ItemVisibility::Export = variable_declaration.visibility { if let ItemVisibility::Export = variable_declaration.visibility {
@ -326,7 +338,7 @@ impl ExecutorContext {
} }
} }
// Variable declaration can be the return value of a module. // Variable declaration can be the return value of a module.
last_expr = matches!(body_type, BodyType::Root).then_some(value); last_expr = matches!(body_type, BodyType::Root).then_some(rhs);
} }
BodyItem::TypeDeclaration(ty) => { BodyItem::TypeDeclaration(ty) => {
let metadata = Metadata::from(&**ty); let metadata = Metadata::from(&**ty);
@ -336,7 +348,7 @@ impl ExecutorContext {
let std_path = match &exec_state.mod_local.path { let std_path = match &exec_state.mod_local.path {
ModulePath::Std { value } => value, ModulePath::Std { value } => value,
ModulePath::Local { .. } | ModulePath::Main => { ModulePath::Local { .. } | ModulePath::Main => {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
"User-defined types are not yet supported.".to_owned(), "User-defined types are not yet supported.".to_owned(),
vec![metadata.source_range], vec![metadata.source_range],
))); )));
@ -352,7 +364,7 @@ impl ExecutorContext {
.mut_stack() .mut_stack()
.add(name_in_mem.clone(), value, metadata.source_range) .add(name_in_mem.clone(), value, metadata.source_range)
.map_err(|_| { .map_err(|_| {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!("Redefinition of type {}.", ty.name.name), format!("Redefinition of type {}.", ty.name.name),
vec![metadata.source_range], vec![metadata.source_range],
)) ))
@ -373,7 +385,7 @@ impl ExecutorContext {
exec_state, exec_state,
metadata.source_range, metadata.source_range,
) )
.map_err(|e| KclError::Semantic(e.into()))?, .map_err(|e| KclError::new_semantic(e.into()))?,
), ),
meta: vec![metadata], meta: vec![metadata],
}; };
@ -382,7 +394,7 @@ impl ExecutorContext {
.mut_stack() .mut_stack()
.add(name_in_mem.clone(), value, metadata.source_range) .add(name_in_mem.clone(), value, metadata.source_range)
.map_err(|_| { .map_err(|_| {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!("Redefinition of type {}.", ty.name.name), format!("Redefinition of type {}.", ty.name.name),
vec![metadata.source_range], vec![metadata.source_range],
)) ))
@ -393,7 +405,7 @@ impl ExecutorContext {
} }
} }
None => { None => {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
"User-defined types are not yet supported.".to_owned(), "User-defined types are not yet supported.".to_owned(),
vec![metadata.source_range], vec![metadata.source_range],
))) )))
@ -407,7 +419,7 @@ impl ExecutorContext {
let metadata = Metadata::from(return_statement); let metadata = Metadata::from(return_statement);
if matches!(body_type, BodyType::Root) { if matches!(body_type, BodyType::Root) {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
"Cannot return from outside a function.".to_owned(), "Cannot return from outside a function.".to_owned(),
vec![metadata.source_range], vec![metadata.source_range],
))); )));
@ -426,7 +438,7 @@ impl ExecutorContext {
.mut_stack() .mut_stack()
.add(memory::RETURN_NAME.to_owned(), value, metadata.source_range) .add(memory::RETURN_NAME.to_owned(), value, metadata.source_range)
.map_err(|_| { .map_err(|_| {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
"Multiple returns from a single function.".to_owned(), "Multiple returns from a single function.".to_owned(),
vec![metadata.source_range], vec![metadata.source_range],
)) ))
@ -531,7 +543,7 @@ impl ExecutorContext {
*cache = Some((val, er, items.clone())); *cache = Some((val, er, items.clone()));
(er, items) (er, items)
}), }),
ModuleRepr::Foreign(geom, _) => Err(KclError::Semantic(KclErrorDetails::new( ModuleRepr::Foreign(geom, _) => Err(KclError::new_semantic(KclErrorDetails::new(
"Cannot import items from foreign modules".to_owned(), "Cannot import items from foreign modules".to_owned(),
vec![geom.source_range], vec![geom.source_range],
))), ))),
@ -605,12 +617,12 @@ impl ExecutorContext {
exec_state.global.mod_loader.leave_module(path); exec_state.global.mod_loader.leave_module(path);
result.map_err(|err| { result.map_err(|err| {
if let KclError::ImportCycle(_) = err { if let KclError::ImportCycle { .. } = err {
// It was an import cycle. Keep the original message. // It was an import cycle. Keep the original message.
err.override_source_ranges(vec![source_range]) err.override_source_ranges(vec![source_range])
} else { } else {
// TODO would be great to have line/column for the underlying error here // TODO would be great to have line/column for the underlying error here
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"Error loading imported file ({path}). Open it to view more details.\n {}", "Error loading imported file ({path}). Open it to view more details.\n {}",
err.message() err.message()
@ -635,7 +647,12 @@ impl ExecutorContext {
Expr::Literal(literal) => KclValue::from_literal((**literal).clone(), exec_state), Expr::Literal(literal) => KclValue::from_literal((**literal).clone(), exec_state),
Expr::TagDeclarator(tag) => tag.execute(exec_state).await?, Expr::TagDeclarator(tag) => tag.execute(exec_state).await?,
Expr::Name(name) => { Expr::Name(name) => {
let value = name.get_result(exec_state, self).await?.clone(); let being_declared = exec_state.mod_local.being_declared.clone();
let value = name
.get_result(exec_state, self)
.await
.map_err(|e| var_in_own_ref_err(e, &being_declared))?
.clone();
if let KclValue::Module { value: module_id, meta } = value { if let KclValue::Module { value: module_id, meta } = value {
self.exec_module_for_result( self.exec_module_for_result(
module_id, module_id,
@ -677,7 +694,7 @@ impl ExecutorContext {
meta: vec![metadata.to_owned()], meta: vec![metadata.to_owned()],
} }
} else { } else {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
"Rust implementation of functions is restricted to the standard library".to_owned(), "Rust implementation of functions is restricted to the standard library".to_owned(),
vec![metadata.source_range], vec![metadata.source_range],
))); )));
@ -704,7 +721,7 @@ impl ExecutorContext {
"you cannot declare variable {name} as %, because % can only be used in function calls" "you cannot declare variable {name} as %, because % can only be used in function calls"
); );
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
message, message,
vec![pipe_substitution.into()], vec![pipe_substitution.into()],
))); )));
@ -712,7 +729,7 @@ impl ExecutorContext {
StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() { StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() {
Some(x) => x, Some(x) => x,
None => { None => {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
"cannot use % outside a pipe expression".to_owned(), "cannot use % outside a pipe expression".to_owned(),
vec![pipe_substitution.into()], vec![pipe_substitution.into()],
))); )));
@ -722,7 +739,7 @@ impl ExecutorContext {
Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?, Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?,
Expr::ArrayRangeExpression(range_expression) => range_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::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?,
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?, Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state, self).await?,
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?, Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?, Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
Expr::LabelledExpression(expr) => { Expr::LabelledExpression(expr) => {
@ -741,6 +758,24 @@ impl ExecutorContext {
} }
} }
/// If the error is about an undefined name, and that name matches the name being defined,
/// make the error message more specific.
fn var_in_own_ref_err(e: KclError, being_declared: &Option<String>) -> KclError {
let KclError::UndefinedValue { name, mut details } = e else {
return e;
};
// TODO after June 26th: replace this with a let-chain,
// which will be available in Rust 1.88
// https://rust-lang.github.io/rfcs/2497-if-let-chains.html
match (&being_declared, &name) {
(Some(name0), Some(name1)) if name0 == name1 => {
details.message = format!("You can't use `{name0}` because you're currently trying to define it. Use a different variable here instead.");
}
_ => {}
}
KclError::UndefinedValue { details, name }
}
impl Node<AscribedExpression> { impl Node<AscribedExpression> {
#[async_recursion] #[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
@ -761,7 +796,7 @@ fn apply_ascription(
source_range: SourceRange, source_range: SourceRange,
) -> Result<KclValue, KclError> { ) -> Result<KclValue, KclError> {
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into()) let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into())
.map_err(|e| KclError::Semantic(e.into()))?; .map_err(|e| KclError::new_semantic(e.into()))?;
value.coerce(&ty, false, exec_state).map_err(|_| { value.coerce(&ty, false, exec_state).map_err(|_| {
let suggestion = if ty == RuntimeType::length() { let suggestion = if ty == RuntimeType::length() {
@ -771,7 +806,7 @@ fn apply_ascription(
} else { } else {
"" ""
}; };
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"could not coerce value of type {} to type {ty}{suggestion}", "could not coerce value of type {} to type {ty}{suggestion}",
value.human_friendly_type() value.human_friendly_type()
@ -790,7 +825,7 @@ impl BinaryPart {
BinaryPart::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, ctx).await, BinaryPart::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, ctx).await,
BinaryPart::CallExpressionKw(call_expression) => call_expression.execute(exec_state, ctx).await, BinaryPart::CallExpressionKw(call_expression) => call_expression.execute(exec_state, ctx).await,
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, ctx).await, BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, ctx).await,
BinaryPart::MemberExpression(member_expression) => member_expression.get_result(exec_state), BinaryPart::MemberExpression(member_expression) => member_expression.get_result(exec_state, ctx).await,
BinaryPart::IfExpression(e) => e.get_result(exec_state, ctx).await, BinaryPart::IfExpression(e) => e.get_result(exec_state, ctx).await,
BinaryPart::AscribedExpression(e) => e.get_result(exec_state, ctx).await, BinaryPart::AscribedExpression(e) => e.get_result(exec_state, ctx).await,
} }
@ -802,9 +837,20 @@ impl Node<Name> {
&self, &self,
exec_state: &'a mut ExecState, exec_state: &'a mut ExecState,
ctx: &ExecutorContext, ctx: &ExecutorContext,
) -> Result<&'a KclValue, KclError> {
let being_declared = exec_state.mod_local.being_declared.clone();
self.get_result_inner(exec_state, ctx)
.await
.map_err(|e| var_in_own_ref_err(e, &being_declared))
}
async fn get_result_inner<'a>(
&self,
exec_state: &'a mut ExecState,
ctx: &ExecutorContext,
) -> Result<&'a KclValue, KclError> { ) -> Result<&'a KclValue, KclError> {
if self.abs_path { if self.abs_path {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
"Absolute paths (names beginning with `::` are not yet supported)".to_owned(), "Absolute paths (names beginning with `::` are not yet supported)".to_owned(),
self.as_source_ranges(), self.as_source_ranges(),
))); )));
@ -825,7 +871,7 @@ impl Node<Name> {
let value = match mem_spec { let value = match mem_spec {
Some((env, exports)) => { Some((env, exports)) => {
if !exports.contains(&p.name) { if !exports.contains(&p.name) {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!("Item {} not found in module's exported items", p.name), format!("Item {} not found in module's exported items", p.name),
p.as_source_ranges(), p.as_source_ranges(),
))); )));
@ -842,7 +888,7 @@ impl Node<Name> {
}; };
let KclValue::Module { value: module_id, .. } = value else { let KclValue::Module { value: module_id, .. } = value else {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"Identifier in path must refer to a module, found {}", "Identifier in path must refer to a module, found {}",
value.human_friendly_type() value.human_friendly_type()
@ -888,7 +934,7 @@ impl Node<Name> {
// Either item or module is defined, but not exported. // Either item or module is defined, but not exported.
debug_assert!((item_value.is_ok() && !item_exported) || (mod_value.is_ok() && !mod_exported)); debug_assert!((item_value.is_ok() && !item_exported) || (mod_value.is_ok() && !mod_exported));
Err(KclError::Semantic(KclErrorDetails::new( Err(KclError::new_semantic(KclErrorDetails::new(
format!("Item {} not found in module's exported items", self.name.name), format!("Item {} not found in module's exported items", self.name.name),
self.name.as_source_ranges(), self.name.as_source_ranges(),
))) )))
@ -896,16 +942,14 @@ impl Node<Name> {
} }
impl Node<MemberExpression> { impl Node<MemberExpression> {
fn get_result(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> { async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let property = Property::try_from(self.computed, self.property.clone(), exec_state, self.into())?; let property = Property::try_from(self.computed, self.property.clone(), exec_state, self.into())?;
let object = match &self.object { let meta = Metadata {
// TODO: Don't use recursion here, use a loop. source_range: SourceRange::from(self),
MemberObject::MemberExpression(member_expr) => member_expr.get_result(exec_state)?,
MemberObject::Identifier(identifier) => {
let value = exec_state.stack().get(&identifier.name, identifier.into())?;
value.clone()
}
}; };
let object = ctx
.execute_expr(&self.object, exec_state, &meta, &[], StatementKind::Expression)
.await?;
// Check the property and object match -- e.g. ints for arrays, strs for objects. // Check the property and object match -- e.g. ints for arrays, strs for objects.
match (object, property, self.computed) { match (object, property, self.computed) {
@ -913,14 +957,17 @@ impl Node<MemberExpression> {
if let Some(value) = map.get(&property) { if let Some(value) = map.get(&property) {
Ok(value.to_owned()) Ok(value.to_owned())
} else { } else {
Err(KclError::UndefinedValue(KclErrorDetails::new( Err(KclError::new_undefined_value(
format!("Property '{property}' not found in object"), KclErrorDetails::new(
vec![self.clone().into()], format!("Property '{property}' not found in object"),
))) vec![self.clone().into()],
),
None,
))
} }
} }
(KclValue::Object { .. }, Property::String(property), true) => { (KclValue::Object { .. }, Property::String(property), true) => {
Err(KclError::Semantic(KclErrorDetails::new( Err(KclError::new_semantic(KclErrorDetails::new(
format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"), format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"),
vec![self.clone().into()], vec![self.clone().into()],
))) )))
@ -928,7 +975,7 @@ impl Node<MemberExpression> {
(KclValue::Object { .. }, p, _) => { (KclValue::Object { .. }, p, _) => {
let t = p.type_name(); let t = p.type_name();
let article = article_for(t); let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails::new( Err(KclError::new_semantic(KclErrorDetails::new(
format!("Only strings can be used as the property of an object, but you're using {article} {t}",), format!("Only strings can be used as the property of an object, but you're using {article} {t}",),
vec![self.clone().into()], vec![self.clone().into()],
))) )))
@ -938,10 +985,13 @@ impl Node<MemberExpression> {
if let Some(value) = value_of_arr { if let Some(value) = value_of_arr {
Ok(value.to_owned()) Ok(value.to_owned())
} else { } else {
Err(KclError::UndefinedValue(KclErrorDetails::new( Err(KclError::new_undefined_value(
format!("The array doesn't have any item at index {index}"), KclErrorDetails::new(
vec![self.clone().into()], format!("The array doesn't have any item at index {index}"),
))) vec![self.clone().into()],
),
None,
))
} }
} }
// Singletons and single-element arrays should be interchangeable, but only indexing by 0 should work. // Singletons and single-element arrays should be interchangeable, but only indexing by 0 should work.
@ -950,7 +1000,7 @@ impl Node<MemberExpression> {
(KclValue::HomArray { .. }, p, _) => { (KclValue::HomArray { .. }, p, _) => {
let t = p.type_name(); let t = p.type_name();
let article = article_for(t); let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails::new( Err(KclError::new_semantic(KclErrorDetails::new(
format!("Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",), format!("Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",),
vec![self.clone().into()], vec![self.clone().into()],
))) )))
@ -971,7 +1021,7 @@ impl Node<MemberExpression> {
(being_indexed, _, _) => { (being_indexed, _, _) => {
let t = being_indexed.human_friendly_type(); let t = being_indexed.human_friendly_type();
let article = article_for(&t); let article = article_for(&t);
Err(KclError::Semantic(KclErrorDetails::new( Err(KclError::new_semantic(KclErrorDetails::new(
format!("Only arrays can be indexed, but you're trying to index {article} {t}"), format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
vec![self.clone().into()], vec![self.clone().into()],
))) )))
@ -1049,7 +1099,7 @@ impl Node<BinaryExpression> {
meta: _, meta: _,
} = left_value } = left_value
else { else {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"Cannot apply logical operator to non-boolean value: {}", "Cannot apply logical operator to non-boolean value: {}",
left_value.human_friendly_type() left_value.human_friendly_type()
@ -1062,7 +1112,7 @@ impl Node<BinaryExpression> {
meta: _, meta: _,
} = right_value } = right_value
else { else {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"Cannot apply logical operator to non-boolean value: {}", "Cannot apply logical operator to non-boolean value: {}",
right_value.human_friendly_type() right_value.human_friendly_type()
@ -1168,7 +1218,7 @@ impl Node<UnaryExpression> {
meta: _, meta: _,
} = value } = value
else { else {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"Cannot apply unary operator ! to non-boolean value: {}", "Cannot apply unary operator ! to non-boolean value: {}",
value.human_friendly_type() value.human_friendly_type()
@ -1189,7 +1239,7 @@ impl Node<UnaryExpression> {
let value = &self.argument.get_result(exec_state, ctx).await?; let value = &self.argument.get_result(exec_state, ctx).await?;
let err = || { let err = || {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"You can only negate numbers, planes, or lines, but this is a {}", "You can only negate numbers, planes, or lines, but this is a {}",
value.human_friendly_type() value.human_friendly_type()
@ -1292,7 +1342,7 @@ pub(crate) async fn execute_pipe_body(
ctx: &ExecutorContext, ctx: &ExecutorContext,
) -> Result<KclValue, KclError> { ) -> Result<KclValue, KclError> {
let Some((first, body)) = body.split_first() else { let Some((first, body)) = body.split_first() else {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
"Pipe expressions cannot be empty".to_owned(), "Pipe expressions cannot be empty".to_owned(),
vec![source_range], vec![source_range],
))); )));
@ -1311,7 +1361,7 @@ pub(crate) async fn execute_pipe_body(
// Now that we've evaluated the first child expression in the pipeline, following child expressions // Now that we've evaluated the first child expression in the pipeline, following child expressions
// should use the previous child expression for %. // should use the previous child expression for %.
// This means there's no more need for the previous pipe_value from the parent AST node above this one. // This means there's no more need for the previous pipe_value from the parent AST node above this one.
let previous_pipe_value = std::mem::replace(&mut exec_state.mod_local.pipe_value, Some(output)); let previous_pipe_value = exec_state.mod_local.pipe_value.replace(output);
// Evaluate remaining elements. // Evaluate remaining elements.
let result = inner_execute_pipe_body(exec_state, body, ctx).await; let result = inner_execute_pipe_body(exec_state, body, ctx).await;
// Restore the previous pipe value. // Restore the previous pipe value.
@ -1330,7 +1380,7 @@ async fn inner_execute_pipe_body(
) -> Result<KclValue, KclError> { ) -> Result<KclValue, KclError> {
for expression in body { for expression in body {
if let Expr::TagDeclarator(_) = expression { if let Expr::TagDeclarator(_) = expression {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!("This cannot be in a PipeExpression: {:?}", expression), format!("This cannot be in a PipeExpression: {:?}", expression),
vec![expression.into()], vec![expression.into()],
))); )));
@ -1404,7 +1454,7 @@ impl Node<ArrayRangeExpression> {
.await?; .await?;
let (start, start_ty) = start_val let (start, start_ty) = start_val
.as_int_with_ty() .as_int_with_ty()
.ok_or(KclError::Semantic(KclErrorDetails::new( .ok_or(KclError::new_semantic(KclErrorDetails::new(
format!("Expected int but found {}", start_val.human_friendly_type()), format!("Expected int but found {}", start_val.human_friendly_type()),
vec![self.into()], vec![self.into()],
)))?; )))?;
@ -1412,24 +1462,26 @@ impl Node<ArrayRangeExpression> {
let end_val = ctx let end_val = ctx
.execute_expr(&self.end_element, exec_state, &metadata, &[], StatementKind::Expression) .execute_expr(&self.end_element, exec_state, &metadata, &[], StatementKind::Expression)
.await?; .await?;
let (end, end_ty) = end_val.as_int_with_ty().ok_or(KclError::Semantic(KclErrorDetails::new( let (end, end_ty) = end_val
format!("Expected int but found {}", end_val.human_friendly_type()), .as_int_with_ty()
vec![self.into()], .ok_or(KclError::new_semantic(KclErrorDetails::new(
)))?; format!("Expected int but found {}", end_val.human_friendly_type()),
vec![self.into()],
)))?;
if start_ty != end_ty { if start_ty != end_ty {
let start = start_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: start_ty }); let start = start_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: start_ty });
let start = fmt::human_display_number(start.n, start.ty); let start = fmt::human_display_number(start.n, start.ty);
let end = end_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: end_ty }); let end = end_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: end_ty });
let end = fmt::human_display_number(end.n, end.ty); let end = fmt::human_display_number(end.n, end.ty);
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!("Range start and end must be of the same type, but found {start} and {end}"), format!("Range start and end must be of the same type, but found {start} and {end}"),
vec![self.into()], vec![self.into()],
))); )));
} }
if end < start { if end < start {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!("Range start is greater than range end: {start} .. {end}"), format!("Range start is greater than range end: {start} .. {end}"),
vec![self.into()], vec![self.into()],
))); )));
@ -1493,7 +1545,7 @@ fn article_for<S: AsRef<str>>(s: S) -> &'static str {
fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> { fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
v.as_ty_f64().ok_or_else(|| { v.as_ty_f64().ok_or_else(|| {
let actual_type = v.human_friendly_type(); let actual_type = v.human_friendly_type();
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!("Expected a number, but found {actual_type}",), format!("Expected a number, but found {actual_type}",),
vec![source_range], vec![source_range],
)) ))
@ -1585,13 +1637,13 @@ impl Property {
if let Some(x) = crate::try_f64_to_usize(value) { if let Some(x) = crate::try_f64_to_usize(value) {
Ok(Property::UInt(x)) Ok(Property::UInt(x))
} else { } else {
Err(KclError::Semantic(KclErrorDetails::new( Err(KclError::new_semantic(KclErrorDetails::new(
format!("{value} is not a valid index, indices must be whole numbers >= 0"), format!("{value} is not a valid index, indices must be whole numbers >= 0"),
property_sr, property_sr,
))) )))
} }
} }
_ => Err(KclError::Semantic(KclErrorDetails::new( _ => Err(KclError::new_semantic(KclErrorDetails::new(
"Only numbers (>= 0) can be indexes".to_owned(), "Only numbers (>= 0) can be indexes".to_owned(),
vec![sr], vec![sr],
))), ))),
@ -1602,7 +1654,8 @@ impl Property {
} }
fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> { fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> {
let make_err = |message: String| Err::<Property, _>(KclError::Semantic(KclErrorDetails::new(message, property_sr))); let make_err =
|message: String| Err::<Property, _>(KclError::new_semantic(KclErrorDetails::new(message, property_sr)));
match value { match value {
KclValue::Number{value: num, .. } => { KclValue::Number{value: num, .. } => {
let num = *num; let num = *num;
@ -1846,7 +1899,7 @@ d = b + c
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
.await .await
.map_err(|err| { .map_err(|err| {
KclError::Internal(KclErrorDetails::new( KclError::new_internal(KclErrorDetails::new(
format!("Failed to create mock engine connection: {}", err), format!("Failed to create mock engine connection: {}", err),
vec![SourceRange::default()], vec![SourceRange::default()],
)) ))
@ -1858,7 +1911,6 @@ d = b + c
project_directory: Some(crate::TypedPath(tmpdir.path().into())), project_directory: Some(crate::TypedPath(tmpdir.path().into())),
..Default::default() ..Default::default()
}, },
stdlib: Arc::new(crate::std::StdLib::new()),
context_type: ContextType::Mock, context_type: ContextType::Mock,
}; };
let mut exec_state = ExecState::new(&exec_ctxt); let mut exec_state = ExecState::new(&exec_ctxt);

View File

@ -2,7 +2,6 @@ use async_recursion::async_recursion;
use indexmap::IndexMap; use indexmap::IndexMap;
use crate::{ use crate::{
docs::StdLibFn,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
cad_op::{Group, OpArg, OpKclValue, Operation}, cad_op::{Group, OpArg, OpKclValue, Operation},
@ -15,7 +14,7 @@ use crate::{
parsing::ast::types::{CallExpressionKw, DefaultParamVal, FunctionExpression, Node, Program, Type}, parsing::ast::types::{CallExpressionKw, DefaultParamVal, FunctionExpression, Node, Program, Type},
source_range::SourceRange, source_range::SourceRange,
std::StdFn, std::StdFn,
CompilationError, CompilationError, NodePath,
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -184,40 +183,6 @@ impl<'a> From<&'a FunctionSource> for FunctionDefinition<'a> {
} }
} }
impl From<&dyn StdLibFn> for FunctionDefinition<'static> {
fn from(value: &dyn StdLibFn) -> Self {
let mut input_arg = None;
let mut named_args = IndexMap::new();
for a in value.args(false) {
if !a.label_required {
input_arg = Some((a.name.clone(), None));
continue;
}
named_args.insert(
a.name.clone(),
(
if a.required {
None
} else {
Some(DefaultParamVal::none())
},
None,
),
);
}
FunctionDefinition {
input_arg,
named_args,
return_type: None,
deprecated: value.deprecated(),
include_in_feature_tree: value.feature_tree_operation(),
is_std: true,
body: FunctionBody::Rust(value.std_lib_fn()),
}
}
}
impl Node<CallExpressionKw> { impl Node<CallExpressionKw> {
#[async_recursion] #[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
@ -274,59 +239,44 @@ impl Node<CallExpressionKw> {
exec_state.pipe_value().map(|v| Arg::new(v.clone(), callsite)), exec_state.pipe_value().map(|v| Arg::new(v.clone(), callsite)),
); );
match ctx.stdlib.get_rust_function(fn_name) { // Clone the function so that we can use a mutable reference to
Some(func) => { // exec_state.
let def: FunctionDefinition = (&*func).into(); let func = fn_name.get_result(exec_state, ctx).await?.clone();
// All std lib functions return a value, so the unwrap is safe.
def.call_kw(Some(func.name()), exec_state, ctx, args, callsite)
.await
.map(Option::unwrap)
.map_err(|e| {
// This is used for the backtrace display. We don't add
// another location the way we do for user-defined
// functions because the error uses the Args, which
// already points here.
e.set_last_backtrace_fn_name(Some(func.name()))
})
}
None => {
// Clone the function so that we can use a mutable reference to
// exec_state.
let func = fn_name.get_result(exec_state, ctx).await?.clone();
let Some(fn_src) = func.as_function() else { let Some(fn_src) = func.as_function() else {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
"cannot call this because it isn't a function".to_string(), "cannot call this because it isn't a function".to_string(),
vec![callsite], vec![callsite],
))); )));
}; };
let return_value = fn_src let return_value = fn_src
.call_kw(Some(fn_name.to_string()), exec_state, ctx, args, callsite) .call_kw(Some(fn_name.to_string()), exec_state, ctx, args, callsite)
.await .await
.map_err(|e| { .map_err(|e| {
// Add the call expression to the source ranges. // Add the call expression to the source ranges.
// //
// TODO: Use the name that the function was defined // TODO: Use the name that the function was defined
// with, not the identifier it was used with. // with, not the identifier it was used with.
e.add_unwind_location(Some(fn_name.name.name.clone()), callsite) e.add_unwind_location(Some(fn_name.name.name.clone()), callsite)
})?; })?;
let result = return_value.ok_or_else(move || { let result = return_value.ok_or_else(move || {
let mut source_ranges: Vec<SourceRange> = vec![callsite]; let mut source_ranges: Vec<SourceRange> = vec![callsite];
// We want to send the source range of the original function. // We want to send the source range of the original function.
if let KclValue::Function { meta, .. } = func { if let KclValue::Function { meta, .. } = func {
source_ranges = meta.iter().map(|m| m.source_range).collect(); source_ranges = meta.iter().map(|m| m.source_range).collect();
}; };
KclError::UndefinedValue(KclErrorDetails::new( KclError::new_undefined_value(
format!("Result of user-defined function {} is undefined", fn_name), KclErrorDetails::new(
source_ranges, format!("Result of user-defined function {} is undefined", fn_name),
)) source_ranges,
})?; ),
None,
)
})?;
Ok(result) Ok(result)
}
}
} }
} }
@ -372,6 +322,7 @@ impl FunctionDefinition<'_> {
.unlabeled_kw_arg_unconverted() .unlabeled_kw_arg_unconverted()
.map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)), .map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
labeled_args: op_labeled_args, labeled_args: op_labeled_args,
node_path: NodePath::placeholder(),
source_range: callsite, source_range: callsite,
is_error: false, is_error: false,
}) })
@ -387,6 +338,7 @@ impl FunctionDefinition<'_> {
.map(|arg| OpArg::new(OpKclValue::from(&arg.1.value), arg.1.source_range)), .map(|arg| OpArg::new(OpKclValue::from(&arg.1.value), arg.1.source_range)),
labeled_args: op_labeled_args, labeled_args: op_labeled_args,
}, },
node_path: NodePath::placeholder(),
source_range: callsite, source_range: callsite,
}); });
@ -500,7 +452,7 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
let tag_id = if let Some(t) = value.sketch.tags.get(&tag.name) { let tag_id = if let Some(t) = value.sketch.tags.get(&tag.name) {
let mut t = t.clone(); let mut t = t.clone();
let Some(info) = t.get_cur_info() else { let Some(info) = t.get_cur_info() else {
return Err(KclError::Internal(KclErrorDetails::new( return Err(KclError::new_internal(KclErrorDetails::new(
format!("Tag {} does not have path info", tag.name), format!("Tag {} does not have path info", tag.name),
vec![tag.into()], vec![tag.into()],
))); )));
@ -600,30 +552,33 @@ fn type_check_params_kw(
for (label, arg) in &mut args.labeled { for (label, arg) in &mut args.labeled {
match fn_def.named_args.get(label) { match fn_def.named_args.get(label) {
Some((_, ty)) => { Some((def, ty)) => {
if let Some(ty) = ty { // For optional args, passing None should be the same as not passing an arg.
arg.value = arg if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) {
.value if let Some(ty) = ty {
.coerce( arg.value = arg
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::Semantic(e.into()))?, .value
true, .coerce(
exec_state, &RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::new_semantic(e.into()))?,
) true,
.map_err(|e| { exec_state,
let mut message = format!( )
"{label} requires a value with type `{}`, but found {}", .map_err(|e| {
ty, let mut message = format!(
arg.value.human_friendly_type(), "{label} requires a value with type `{}`, but found {}",
); ty,
if let Some(ty) = e.explicit_coercion { arg.value.human_friendly_type(),
// TODO if we have access to the AST for the argument we could choose which example to suggest. );
message = format!("{message}\n\nYou may need to add information about the type of the argument, for example:\n using a numeric suffix: `42{ty}`\n or using type ascription: `foo(): number({ty})`"); if let Some(ty) = e.explicit_coercion {
} // TODO if we have access to the AST for the argument we could choose which example to suggest.
KclError::Semantic(KclErrorDetails::new( message = format!("{message}\n\nYou may need to add information about the type of the argument, for example:\n using a numeric suffix: `42{ty}`\n or using type ascription: `foo(): number({ty})`");
message, }
vec![arg.source_range], KclError::new_semantic(KclErrorDetails::new(
)) message,
})?; vec![arg.source_range],
))
})?;
}
} }
} }
None => { None => {
@ -670,7 +625,7 @@ fn type_check_params_kw(
let first = errors.next().unwrap(); let first = errors.next().unwrap();
errors.for_each(|e| exec_state.err(e)); errors.for_each(|e| exec_state.err(e));
return Err(KclError::Semantic(first.into())); return Err(KclError::new_semantic(first.into()));
} }
if let Some(arg) = &mut args.unlabeled { if let Some(arg) = &mut args.unlabeled {
@ -680,12 +635,12 @@ fn type_check_params_kw(
.value .value
.coerce( .coerce(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range) &RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
.map_err(|e| KclError::Semantic(e.into()))?, .map_err(|e| KclError::new_semantic(e.into()))?,
true, true,
exec_state, exec_state,
) )
.map_err(|_| { .map_err(|_| {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"The input argument of {} requires a value with type `{}`, but found {}", "The input argument of {} requires a value with type `{}`, but found {}",
fn_name fn_name
@ -703,7 +658,7 @@ fn type_check_params_kw(
exec_state.err(CompilationError::err( exec_state.err(CompilationError::err(
arg.source_range, arg.source_range,
format!( format!(
"{} expects an unlabeled first parameter (`@{name}`), but it is labelled in the call", "{} expects an unlabeled first argument (`@{name}`), but it is labelled in the call",
fn_name fn_name
.map(|n| format!("The function `{}`", n)) .map(|n| format!("The function `{}`", n))
.unwrap_or_else(|| "This function".to_owned()), .unwrap_or_else(|| "This function".to_owned()),
@ -742,7 +697,7 @@ fn assign_args_to_params_kw(
.add(name.clone(), value, default_val.source_range())?; .add(name.clone(), value, default_val.source_range())?;
} }
None => { None => {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"This function requires a parameter {}, but you haven't passed it one.", "This function requires a parameter {}, but you haven't passed it one.",
name name
@ -759,12 +714,12 @@ fn assign_args_to_params_kw(
let Some(unlabeled) = unlabelled else { let Some(unlabeled) = unlabelled else {
return Err(if args.kw_args.labeled.contains_key(param_name) { return Err(if args.kw_args.labeled.contains_key(param_name) {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"), format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"),
source_ranges, source_ranges,
)) ))
} else { } else {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
"This function expects an unlabeled first parameter, but you haven't passed it one.".to_owned(), "This function expects an unlabeled first parameter, but you haven't passed it one.".to_owned(),
source_ranges, source_ranges,
)) ))
@ -788,9 +743,9 @@ fn coerce_result_type(
if let Ok(Some(val)) = result { if let Ok(Some(val)) = result {
if let Some(ret_ty) = &fn_def.return_type { if let Some(ret_ty) = &fn_def.return_type {
let ty = RuntimeType::from_parsed(ret_ty.inner.clone(), exec_state, ret_ty.as_source_range()) let ty = RuntimeType::from_parsed(ret_ty.inner.clone(), exec_state, ret_ty.as_source_range())
.map_err(|e| KclError::Semantic(e.into()))?; .map_err(|e| KclError::new_semantic(e.into()))?;
let val = val.coerce(&ty, true, exec_state).map_err(|_| { let val = val.coerce(&ty, true, exec_state).map_err(|_| {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"This function requires its result to be of type `{}`, but found {}", "This function requires its result to be of type `{}`, but found {}",
ty.human_friendly_type(), ty.human_friendly_type(),
@ -874,7 +829,7 @@ mod test {
"all params required, none given, should error", "all params required, none given, should error",
vec![req_param("x")], vec![req_param("x")],
vec![], vec![],
Err(KclError::Semantic(KclErrorDetails::new( Err(KclError::new_semantic(KclErrorDetails::new(
"This function requires a parameter x, but you haven't passed it one.".to_owned(), "This function requires a parameter x, but you haven't passed it one.".to_owned(),
vec![SourceRange::default()], vec![SourceRange::default()],
))), ))),
@ -889,7 +844,7 @@ mod test {
"mixed params, too few given", "mixed params, too few given",
vec![req_param("x"), opt_param("y")], vec![req_param("x"), opt_param("y")],
vec![], vec![],
Err(KclError::Semantic(KclErrorDetails::new( Err(KclError::new_semantic(KclErrorDetails::new(
"This function requires a parameter x, but you haven't passed it one.".to_owned(), "This function requires a parameter x, but you haven't passed it one.".to_owned(),
vec![SourceRange::default()], vec![SourceRange::default()],
))), ))),
@ -937,7 +892,6 @@ mod test {
crate::engine::conn_mock::EngineConnection::new().await.unwrap(), crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
)), )),
fs: Arc::new(crate::fs::FileManager::new()), fs: Arc::new(crate::fs::FileManager::new()),
stdlib: Arc::new(crate::std::StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: ContextType::Mock, context_type: ContextType::Mock,
}; };

View File

@ -24,6 +24,7 @@ type Point3D = kcmc::shared::Point3d<f64>;
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum Geometry { pub enum Geometry {
Sketch(Sketch), Sketch(Sketch),
Solid(Solid), Solid(Solid),
@ -52,6 +53,7 @@ impl Geometry {
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum GeometryWithImportedGeometry { pub enum GeometryWithImportedGeometry {
Sketch(Sketch), Sketch(Sketch),
Solid(Solid), Solid(Solid),
@ -469,7 +471,7 @@ impl TryFrom<PlaneData> for PlaneInfo {
PlaneData::NegYZ => PlaneName::NegYz, PlaneData::NegYZ => PlaneName::NegYz,
PlaneData::Plane(_) => { PlaneData::Plane(_) => {
// We will never get here since we already checked for PlaneData::Plane. // We will never get here since we already checked for PlaneData::Plane.
return Err(KclError::Internal(KclErrorDetails::new( return Err(KclError::new_internal(KclErrorDetails::new(
format!("PlaneData {:?} not found", value), format!("PlaneData {:?} not found", value),
Default::default(), Default::default(),
))); )));
@ -477,7 +479,7 @@ impl TryFrom<PlaneData> for PlaneInfo {
}; };
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| { let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
KclError::Internal(KclErrorDetails::new( KclError::new_internal(KclErrorDetails::new(
format!("Plane {} not found", name), format!("Plane {} not found", name),
Default::default(), Default::default(),
)) ))

View File

@ -37,25 +37,25 @@ pub async fn import_foreign(
) -> Result<PreImportedGeometry, KclError> { ) -> Result<PreImportedGeometry, KclError> {
// Make sure the file exists. // Make sure the file exists.
if !ctxt.fs.exists(file_path, source_range).await? { if !ctxt.fs.exists(file_path, source_range).await? {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!("File `{}` does not exist.", file_path.display()), format!("File `{}` does not exist.", file_path.display()),
vec![source_range], vec![source_range],
))); )));
} }
let ext_format = get_import_format_from_extension(file_path.extension().ok_or_else(|| { let ext_format = get_import_format_from_extension(file_path.extension().ok_or_else(|| {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!("No file extension found for `{}`", file_path.display()), format!("No file extension found for `{}`", file_path.display()),
vec![source_range], vec![source_range],
)) ))
})?) })?)
.map_err(|e| KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?; .map_err(|e| KclError::new_semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?;
// Get the format type from the extension of the file. // Get the format type from the extension of the file.
let format = if let Some(format) = format { let format = if let Some(format) = format {
// Validate the given format with the extension format. // Validate the given format with the extension format.
validate_extension_format(ext_format, format.clone()) validate_extension_format(ext_format, format.clone())
.map_err(|e| KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?; .map_err(|e| KclError::new_semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?;
format format
} else { } else {
ext_format ext_format
@ -66,11 +66,11 @@ pub async fn import_foreign(
.fs .fs
.read(file_path, source_range) .read(file_path, source_range)
.await .await
.map_err(|e| KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?; .map_err(|e| KclError::new_semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?;
// We want the file_path to be without the parent. // We want the file_path to be without the parent.
let file_name = file_path.file_name().map(|p| p.to_string()).ok_or_else(|| { let file_name = file_path.file_name().map(|p| p.to_string()).ok_or_else(|| {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!("Could not get the file name from the path `{}`", file_path.display()), format!("Could not get the file name from the path `{}`", file_path.display()),
vec![source_range], vec![source_range],
)) ))
@ -87,7 +87,7 @@ pub async fn import_foreign(
// file. // file.
if !file_contents.starts_with(b"glTF") { if !file_contents.starts_with(b"glTF") {
let json = gltf_json::Root::from_slice(&file_contents) let json = gltf_json::Root::from_slice(&file_contents)
.map_err(|e| KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?; .map_err(|e| KclError::new_semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?;
// Read the gltf file and check if there is a bin file. // Read the gltf file and check if there is a bin file.
for buffer in json.buffers.iter() { for buffer in json.buffers.iter() {
@ -95,16 +95,15 @@ pub async fn import_foreign(
if !uri.starts_with("data:") { if !uri.starts_with("data:") {
// We want this path relative to the file_path given. // We want this path relative to the file_path given.
let bin_path = file_path.parent().map(|p| p.join(uri)).ok_or_else(|| { let bin_path = file_path.parent().map(|p| p.join(uri)).ok_or_else(|| {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!("Could not get the parent path of the file `{}`", file_path.display()), format!("Could not get the parent path of the file `{}`", file_path.display()),
vec![source_range], vec![source_range],
)) ))
})?; })?;
let bin_contents = let bin_contents = ctxt.fs.read(&bin_path, source_range).await.map_err(|e| {
ctxt.fs.read(&bin_path, source_range).await.map_err(|e| { KclError::new_semantic(KclErrorDetails::new(e.to_string(), vec![source_range]))
KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])) })?;
})?;
import_files.push(ImportFile { import_files.push(ImportFile {
path: uri.to_string(), path: uri.to_string(),
@ -141,7 +140,7 @@ pub(super) fn format_from_annotations(
if p.key.name == annotations::IMPORT_FORMAT { if p.key.name == annotations::IMPORT_FORMAT {
result = Some( result = Some(
get_import_format_from_extension(annotations::expect_ident(&p.value)?).map_err(|_| { get_import_format_from_extension(annotations::expect_ident(&p.value)?).map_err(|_| {
KclError::Semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"Unknown format for import, expected one of: {}", "Unknown format for import, expected one of: {}",
crate::IMPORT_FILE_EXTENSIONS.join(", ") crate::IMPORT_FILE_EXTENSIONS.join(", ")
@ -159,7 +158,7 @@ pub(super) fn format_from_annotations(
path.extension() path.extension()
.and_then(|ext| get_import_format_from_extension(ext).ok()) .and_then(|ext| get_import_format_from_extension(ext).ok())
}) })
.ok_or(KclError::Semantic(KclErrorDetails::new( .ok_or(KclError::new_semantic(KclErrorDetails::new(
"Unknown or missing extension, and no specified format for imported file".to_owned(), "Unknown or missing extension, and no specified format for imported file".to_owned(),
vec![import_source_range], vec![import_source_range],
)))?; )))?;
@ -174,7 +173,7 @@ pub(super) fn format_from_annotations(
} }
annotations::IMPORT_FORMAT => {} annotations::IMPORT_FORMAT => {}
_ => { _ => {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"Unexpected annotation for import, expected one of: {}, {}, {}", "Unexpected annotation for import, expected one of: {}, {}, {}",
annotations::IMPORT_FORMAT, annotations::IMPORT_FORMAT,
@ -199,7 +198,7 @@ fn set_coords(fmt: &mut InputFormat3d, coords_str: &str, source_range: SourceRan
} }
let Some(coords) = coords else { let Some(coords) = coords else {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"Unknown coordinate system: {coords_str}, expected one of: {}", "Unknown coordinate system: {coords_str}, expected one of: {}",
annotations::IMPORT_COORDS_VALUES annotations::IMPORT_COORDS_VALUES
@ -217,7 +216,7 @@ fn set_coords(fmt: &mut InputFormat3d, coords_str: &str, source_range: SourceRan
InputFormat3d::Ply(opts) => opts.coords = coords, InputFormat3d::Ply(opts) => opts.coords = coords,
InputFormat3d::Stl(opts) => opts.coords = coords, InputFormat3d::Stl(opts) => opts.coords = coords,
_ => { _ => {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"`{}` option cannot be applied to the specified format", "`{}` option cannot be applied to the specified format",
annotations::IMPORT_COORDS annotations::IMPORT_COORDS
@ -238,7 +237,7 @@ fn set_length_unit(fmt: &mut InputFormat3d, units_str: &str, source_range: Sourc
InputFormat3d::Ply(opts) => opts.units = units.into(), InputFormat3d::Ply(opts) => opts.units = units.into(),
InputFormat3d::Stl(opts) => opts.units = units.into(), InputFormat3d::Stl(opts) => opts.units = units.into(),
_ => { _ => {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"`{}` option cannot be applied to the specified format", "`{}` option cannot be applied to the specified format",
annotations::IMPORT_LENGTH_UNIT annotations::IMPORT_LENGTH_UNIT

View File

@ -31,7 +31,7 @@ pub(crate) type Universe = HashMap<String, DependencyInfo>;
/// run concurrently. Each "stage" is blocking in this model, which will /// run concurrently. Each "stage" is blocking in this model, which will
/// change in the future. Don't use this function widely, yet. /// change in the future. Don't use this function widely, yet.
#[allow(clippy::iter_over_hash_type)] #[allow(clippy::iter_over_hash_type)]
pub fn import_graph(progs: &Universe, ctx: &ExecutorContext) -> Result<Vec<Vec<String>>, KclError> { pub(crate) fn import_graph(progs: &Universe, ctx: &ExecutorContext) -> Result<Vec<Vec<String>>, KclError> {
let mut graph = Graph::new(); let mut graph = Graph::new();
for (name, (_, _, path, repr)) in progs.iter() { for (name, (_, _, path, repr)) in progs.iter() {
@ -96,7 +96,7 @@ fn topsort(all_modules: &[&str], graph: Graph) -> Result<Vec<Vec<String>>, KclEr
if stage_modules.is_empty() { if stage_modules.is_empty() {
waiting_modules.sort(); waiting_modules.sort();
return Err(KclError::ImportCycle(KclErrorDetails::new( return Err(KclError::new_import_cycle(KclErrorDetails::new(
format!("circular import of modules not allowed: {}", waiting_modules.join(", ")), format!("circular import of modules not allowed: {}", waiting_modules.join(", ")),
// TODO: we can get the right import lines from the AST, but we don't // TODO: we can get the right import lines from the AST, but we don't
vec![SourceRange::default()], vec![SourceRange::default()],
@ -120,7 +120,7 @@ fn topsort(all_modules: &[&str], graph: Graph) -> Result<Vec<Vec<String>>, KclEr
type ImportDependencies = Vec<(String, AstNode<ImportStatement>, ModulePath)>; type ImportDependencies = Vec<(String, AstNode<ImportStatement>, ModulePath)>;
pub(crate) fn import_dependencies( fn import_dependencies(
path: &ModulePath, path: &ModulePath,
repr: &ModuleRepr, repr: &ModuleRepr,
ctx: &ExecutorContext, ctx: &ExecutorContext,
@ -146,7 +146,7 @@ pub(crate) fn import_dependencies(
// This is a bit of a hack, but it works for now. // This is a bit of a hack, but it works for now.
ret.lock() ret.lock()
.map_err(|err| { .map_err(|err| {
KclError::Internal(KclErrorDetails::new( KclError::new_internal(KclErrorDetails::new(
format!("Failed to lock mutex: {}", err), format!("Failed to lock mutex: {}", err),
Default::default(), Default::default(),
)) ))
@ -156,7 +156,7 @@ pub(crate) fn import_dependencies(
ImportPath::Foreign { path } => { ImportPath::Foreign { path } => {
ret.lock() ret.lock()
.map_err(|err| { .map_err(|err| {
KclError::Internal(KclErrorDetails::new( KclError::new_internal(KclErrorDetails::new(
format!("Failed to lock mutex: {}", err), format!("Failed to lock mutex: {}", err),
Default::default(), Default::default(),
)) ))
@ -178,7 +178,7 @@ pub(crate) fn import_dependencies(
walk(ret.clone(), prog.into(), path, ctx)?; walk(ret.clone(), prog.into(), path, ctx)?;
let ret = ret.lock().map_err(|err| { let ret = ret.lock().map_err(|err| {
KclError::Internal(KclErrorDetails::new( KclError::new_internal(KclErrorDetails::new(
format!("Failed to lock mutex: {}", err), format!("Failed to lock mutex: {}", err),
Default::default(), Default::default(),
)) ))
@ -223,7 +223,7 @@ pub(crate) async fn import_universe(
let repr = { let repr = {
let Some(module_info) = exec_state.get_module(module_id) else { let Some(module_info) = exec_state.get_module(module_id) else {
return Err(KclError::Internal(KclErrorDetails::new( return Err(KclError::new_internal(KclErrorDetails::new(
format!("Module {} not found", module_id), format!("Module {} not found", module_id),
vec![import_stmt.into()], vec![import_stmt.into()],
))); )));

View File

@ -574,7 +574,7 @@ impl KclValue {
pub fn get_tag_identifier(&self) -> Result<TagIdentifier, KclError> { pub fn get_tag_identifier(&self) -> Result<TagIdentifier, KclError> {
match self { match self {
KclValue::TagIdentifier(t) => Ok(*t.clone()), KclValue::TagIdentifier(t) => Ok(*t.clone()),
_ => Err(KclError::Semantic(KclErrorDetails::new( _ => Err(KclError::new_semantic(KclErrorDetails::new(
format!("Not a tag identifier: {:?}", self), format!("Not a tag identifier: {:?}", self),
self.clone().into(), self.clone().into(),
))), ))),
@ -585,7 +585,7 @@ impl KclValue {
pub fn get_tag_declarator(&self) -> Result<TagNode, KclError> { pub fn get_tag_declarator(&self) -> Result<TagNode, KclError> {
match self { match self {
KclValue::TagDeclarator(t) => Ok((**t).clone()), KclValue::TagDeclarator(t) => Ok((**t).clone()),
_ => Err(KclError::Semantic(KclErrorDetails::new( _ => Err(KclError::new_semantic(KclErrorDetails::new(
format!("Not a tag declarator: {:?}", self), format!("Not a tag declarator: {:?}", self),
self.clone().into(), self.clone().into(),
))), ))),
@ -595,7 +595,7 @@ impl KclValue {
/// If this KCL value is a bool, retrieve it. /// If this KCL value is a bool, retrieve it.
pub fn get_bool(&self) -> Result<bool, KclError> { pub fn get_bool(&self) -> Result<bool, KclError> {
self.as_bool().ok_or_else(|| { self.as_bool().ok_or_else(|| {
KclError::Type(KclErrorDetails::new( KclError::new_type(KclErrorDetails::new(
format!("Expected bool, found {}", self.human_friendly_type()), format!("Expected bool, found {}", self.human_friendly_type()),
self.into(), self.into(),
)) ))

View File

@ -367,10 +367,10 @@ impl ProgramMemory {
let name = var.trim_start_matches(TYPE_PREFIX).trim_start_matches(MODULE_PREFIX); let name = var.trim_start_matches(TYPE_PREFIX).trim_start_matches(MODULE_PREFIX);
Err(KclError::UndefinedValue(KclErrorDetails::new( Err(KclError::new_undefined_value(
format!("`{name}` is not defined"), KclErrorDetails::new(format!("`{name}` is not defined"), vec![source_range]),
vec![source_range], Some(name.to_owned()),
))) ))
} }
/// Iterate over all key/value pairs in the specified environment which satisfy the provided /// Iterate over all key/value pairs in the specified environment which satisfy the provided
@ -488,10 +488,10 @@ impl ProgramMemory {
}; };
} }
Err(KclError::UndefinedValue(KclErrorDetails::new( Err(KclError::new_undefined_value(
format!("`{}` is not defined", var), KclErrorDetails::new(format!("`{}` is not defined", var), vec![]),
vec![], Some(var.to_owned()),
))) ))
} }
} }
@ -646,7 +646,7 @@ impl Stack {
pub fn add(&mut self, key: String, value: KclValue, source_range: SourceRange) -> Result<(), KclError> { pub fn add(&mut self, key: String, value: KclValue, source_range: SourceRange) -> Result<(), KclError> {
let env = self.memory.get_env(self.current_env.index()); let env = self.memory.get_env(self.current_env.index());
if env.contains_key(&key) { if env.contains_key(&key) {
return Err(KclError::ValueAlreadyDefined(KclErrorDetails::new( return Err(KclError::new_value_already_defined(KclErrorDetails::new(
format!("Cannot redefine `{}`", key), format!("Cannot redefine `{}`", key),
vec![source_range], vec![source_range],
))); )));

View File

@ -5,7 +5,7 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketchOnFace, StartSketchOnPlane}; pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketchOnFace, StartSketchOnPlane};
use cache::OldAstState; use cache::GlobalState;
pub use cache::{bust_cache, clear_mem_cache}; pub use cache::{bust_cache, clear_mem_cache};
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub use cad_op::{Group, Operation}; pub use cad_op::{Group, Operation};
@ -27,13 +27,12 @@ use serde::{Deserialize, Serialize};
pub use state::{ExecState, MetaSettings}; pub use state::{ExecState, MetaSettings};
use uuid::Uuid; use uuid::Uuid;
#[cfg(feature = "artifact-graph")]
use crate::execution::artifact::build_artifact_graph;
use crate::{ use crate::{
engine::EngineManager, engine::EngineManager,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
cache::{CacheInformation, CacheResult}, cache::{CacheInformation, CacheResult},
import_graph::{Universe, UniverseMap},
typed_path::TypedPath, typed_path::TypedPath,
types::{UnitAngle, UnitLen}, types::{UnitAngle, UnitLen},
}, },
@ -41,8 +40,6 @@ use crate::{
modules::{ModuleId, ModulePath, ModuleRepr}, modules::{ModuleId, ModulePath, ModuleRepr},
parsing::ast::types::{Expr, ImportPath, NodeRef}, parsing::ast::types::{Expr, ImportPath, NodeRef},
source_range::SourceRange, source_range::SourceRange,
std::StdLib,
walk::{Universe, UniverseMap},
CompilationError, ExecError, KclErrorWithOutputs, CompilationError, ExecError, KclErrorWithOutputs,
}; };
@ -56,6 +53,7 @@ pub mod fn_call;
mod geometry; mod geometry;
mod id_generator; mod id_generator;
mod import; mod import;
mod import_graph;
pub(crate) mod kcl_value; pub(crate) mod kcl_value;
mod memory; mod memory;
mod state; mod state;
@ -273,7 +271,6 @@ pub enum ContextType {
pub struct ExecutorContext { pub struct ExecutorContext {
pub engine: Arc<Box<dyn EngineManager>>, pub engine: Arc<Box<dyn EngineManager>>,
pub fs: Arc<FileManager>, pub fs: Arc<FileManager>,
pub stdlib: Arc<StdLib>,
pub settings: ExecutorSettings, pub settings: ExecutorSettings,
pub context_type: ContextType, pub context_type: ContextType,
} }
@ -412,7 +409,6 @@ impl ExecutorContext {
Ok(Self { Ok(Self {
engine, engine,
fs: Arc::new(FileManager::new()), fs: Arc::new(FileManager::new()),
stdlib: Arc::new(StdLib::new()),
settings, settings,
context_type: ContextType::Live, context_type: ContextType::Live,
}) })
@ -423,7 +419,6 @@ impl ExecutorContext {
ExecutorContext { ExecutorContext {
engine, engine,
fs, fs,
stdlib: Arc::new(StdLib::new()),
settings, settings,
context_type: ContextType::Live, context_type: ContextType::Live,
} }
@ -436,7 +431,6 @@ impl ExecutorContext {
crate::engine::conn_mock::EngineConnection::new().await.unwrap(), crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
)), )),
fs: Arc::new(FileManager::new()), fs: Arc::new(FileManager::new()),
stdlib: Arc::new(StdLib::new()),
settings: settings.unwrap_or_default(), settings: settings.unwrap_or_default(),
context_type: ContextType::Mock, context_type: ContextType::Mock,
} }
@ -447,7 +441,6 @@ impl ExecutorContext {
ExecutorContext { ExecutorContext {
engine, engine,
fs, fs,
stdlib: Arc::new(StdLib::new()),
settings, settings,
context_type: ContextType::Mock, context_type: ContextType::Mock,
} }
@ -458,7 +451,6 @@ impl ExecutorContext {
ExecutorContext { ExecutorContext {
engine, engine,
fs: Arc::new(FileManager::new()), fs: Arc::new(FileManager::new()),
stdlib: Arc::new(StdLib::new()),
settings: Default::default(), settings: Default::default(),
context_type: ContextType::MockCustomForwarded, context_type: ContextType::MockCustomForwarded,
} }
@ -575,7 +567,7 @@ impl ExecutorContext {
// part of the scene). // part of the scene).
exec_state.mut_stack().push_new_env_for_scope(); exec_state.mut_stack().push_new_env_for_scope();
let result = self.inner_run(&program, 0, &mut exec_state, true).await?; let result = self.inner_run(&program, &mut exec_state, true).await?;
// Restore any temporary variables, then save any newly created variables back to // Restore any temporary variables, then save any newly created variables back to
// memory in case another run wants to use them. Note this is just saved to the preserved // memory in case another run wants to use them. Note this is just saved to the preserved
@ -583,7 +575,7 @@ impl ExecutorContext {
let mut mem = exec_state.stack().clone(); let mut mem = exec_state.stack().clone();
let module_infos = exec_state.global.module_infos.clone(); let module_infos = exec_state.global.module_infos.clone();
let outcome = exec_state.to_mock_exec_outcome(result.0).await; let outcome = exec_state.to_mock_exec_outcome(result.0, self).await;
mem.squash_env(result.0); mem.squash_env(result.0);
cache::write_old_memory((mem, module_infos)).await; cache::write_old_memory((mem, module_infos)).await;
@ -594,169 +586,176 @@ impl ExecutorContext {
pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> { pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
assert!(!self.is_mock()); assert!(!self.is_mock());
let (program, mut exec_state, preserve_mem, cached_body_items, imports_info) = if let Some(OldAstState { let (program, exec_state, result) = match cache::read_old_ast().await {
ast: old_ast, Some(mut cached_state) => {
exec_state: mut old_state, let old = CacheInformation {
settings: old_settings, ast: &cached_state.main.ast,
result_env, settings: &cached_state.settings,
}) = };
cache::read_old_ast().await let new = CacheInformation {
{ ast: &program.ast,
let old = CacheInformation { settings: &self.settings,
ast: &old_ast,
settings: &old_settings,
};
let new = CacheInformation {
ast: &program.ast,
settings: &self.settings,
};
// Get the program that actually changed from the old and new information.
let (clear_scene, program, body_items, import_check_info) = match cache::get_changed_program(old, new).await
{
CacheResult::ReExecute {
clear_scene,
reapply_settings,
program: changed_program,
cached_body_items,
} => {
if reapply_settings
&& self
.engine
.reapply_settings(&self.settings, Default::default(), old_state.id_generator())
.await
.is_err()
{
(true, program, cached_body_items, None)
} else {
(
clear_scene,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
cached_body_items,
None,
)
}
}
CacheResult::CheckImportsOnly {
reapply_settings,
ast: changed_program,
} => {
if reapply_settings
&& self
.engine
.reapply_settings(&self.settings, Default::default(), old_state.id_generator())
.await
.is_err()
{
(true, program, old_ast.body.len(), None)
} else {
// We need to check our imports to see if they changed.
let mut new_exec_state = ExecState::new(self);
let (new_universe, new_universe_map) = self.get_universe(&program, &mut new_exec_state).await?;
let mut clear_scene = false;
let mut keys = new_universe.keys().clone().collect::<Vec<_>>();
keys.sort();
for key in keys {
let (_, id, _, _) = &new_universe[key];
if let (Some(source0), Some(source1)) =
(old_state.get_source(*id), new_exec_state.get_source(*id))
{
if source0.source != source1.source {
clear_scene = true;
break;
}
}
}
if !clear_scene {
// Return early we don't need to clear the scene.
let outcome = old_state.to_exec_outcome(result_env).await;
return Ok(outcome);
}
(
clear_scene,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
old_ast.body.len(),
// We only care about this if we are clearing the scene.
if clear_scene {
Some((new_universe, new_universe_map, new_exec_state))
} else {
None
},
)
}
}
CacheResult::NoAction(true) => {
if self
.engine
.reapply_settings(&self.settings, Default::default(), old_state.id_generator())
.await
.is_ok()
{
// We need to update the old ast state with the new settings!!
cache::write_old_ast(OldAstState {
ast: old_ast,
exec_state: old_state.clone(),
settings: self.settings.clone(),
result_env,
})
.await;
let outcome = old_state.to_exec_outcome(result_env).await;
return Ok(outcome);
}
(true, program, old_ast.body.len(), None)
}
CacheResult::NoAction(false) => {
let outcome = old_state.to_exec_outcome(result_env).await;
return Ok(outcome);
}
};
let (exec_state, preserve_mem, universe_info) =
if let Some((new_universe, new_universe_map, mut new_exec_state)) = import_check_info {
// Clear the scene if the imports changed.
self.send_clear_scene(&mut new_exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
(new_exec_state, false, Some((new_universe, new_universe_map)))
} else if clear_scene {
// Pop the execution state, since we are starting fresh.
let mut exec_state = old_state;
exec_state.reset(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
(exec_state, false, None)
} else {
old_state.mut_stack().restore_env(result_env);
(old_state, true, None)
}; };
(program, exec_state, preserve_mem, body_items, universe_info) // Get the program that actually changed from the old and new information.
} else { let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
let mut exec_state = ExecState::new(self); CacheResult::ReExecute {
self.send_clear_scene(&mut exec_state, Default::default()) clear_scene,
.await reapply_settings,
.map_err(KclErrorWithOutputs::no_outputs)?; program: changed_program,
(program, exec_state, false, 0, None) } => {
}; if reapply_settings
&& self
.engine
.reapply_settings(
&self.settings,
Default::default(),
&mut cached_state.main.exec_state.id_generator,
)
.await
.is_err()
{
(true, program, None)
} else {
(
clear_scene,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
None,
)
}
}
CacheResult::CheckImportsOnly {
reapply_settings,
ast: changed_program,
} => {
if reapply_settings
&& self
.engine
.reapply_settings(
&self.settings,
Default::default(),
&mut cached_state.main.exec_state.id_generator,
)
.await
.is_err()
{
(true, program, None)
} else {
// We need to check our imports to see if they changed.
let mut new_exec_state = ExecState::new(self);
let (new_universe, new_universe_map) =
self.get_universe(&program, &mut new_exec_state).await?;
let result = self let clear_scene = new_universe.keys().any(|key| {
.run_concurrent(&program, cached_body_items, &mut exec_state, imports_info, preserve_mem) let id = new_universe[key].1;
.await; match (
cached_state.exec_state.get_source(id),
new_exec_state.global.get_source(id),
) {
(Some(s0), Some(s1)) => s0.source != s1.source,
_ => false,
}
});
if !clear_scene {
// Return early we don't need to clear the scene.
return Ok(cached_state.into_exec_outcome(self).await);
}
(
true,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
Some((new_universe, new_universe_map, new_exec_state)),
)
}
}
CacheResult::NoAction(true) => {
if self
.engine
.reapply_settings(
&self.settings,
Default::default(),
&mut cached_state.main.exec_state.id_generator,
)
.await
.is_ok()
{
// We need to update the old ast state with the new settings!!
cache::write_old_ast(GlobalState::with_settings(
cached_state.clone(),
self.settings.clone(),
))
.await;
return Ok(cached_state.into_exec_outcome(self).await);
}
(true, program, None)
}
CacheResult::NoAction(false) => {
return Ok(cached_state.into_exec_outcome(self).await);
}
};
let (exec_state, result) = match import_check_info {
Some((new_universe, new_universe_map, mut new_exec_state)) => {
// Clear the scene if the imports changed.
self.send_clear_scene(&mut new_exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let result = self
.run_concurrent(
&program,
&mut new_exec_state,
Some((new_universe, new_universe_map)),
false,
)
.await;
(new_exec_state, result)
}
None if clear_scene => {
// Pop the execution state, since we are starting fresh.
let mut exec_state = cached_state.reconstitute_exec_state();
exec_state.reset(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
(exec_state, result)
}
None => {
let mut exec_state = cached_state.reconstitute_exec_state();
exec_state.mut_stack().restore_env(cached_state.main.result_env);
let result = self.run_concurrent(&program, &mut exec_state, None, true).await;
(exec_state, result)
}
};
(program, exec_state, result)
}
None => {
let mut exec_state = ExecState::new(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
(program, exec_state, result)
}
};
if result.is_err() { if result.is_err() {
cache::bust_cache().await; cache::bust_cache().await;
@ -766,15 +765,15 @@ impl ExecutorContext {
let result = result?; let result = result?;
// Save this as the last successful execution to the cache. // Save this as the last successful execution to the cache.
cache::write_old_ast(OldAstState { cache::write_old_ast(GlobalState::new(
ast: program.ast, exec_state.clone(),
exec_state: exec_state.clone(), self.settings.clone(),
settings: self.settings.clone(), program.ast,
result_env: result.0, result.0,
}) ))
.await; .await;
let outcome = exec_state.to_exec_outcome(result.0).await; let outcome = exec_state.to_exec_outcome(result.0, self).await;
Ok(outcome) Ok(outcome)
} }
@ -789,11 +788,11 @@ impl ExecutorContext {
program: &crate::Program, program: &crate::Program,
exec_state: &mut ExecState, exec_state: &mut ExecState,
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> { ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
self.run_concurrent(program, 0, exec_state, None, false).await self.run_concurrent(program, exec_state, None, false).await
} }
/// Perform the execution of a program using a concurrent /// Perform the execution of a program using a concurrent
/// execution model. This has the same signature as [Self::run]. /// execution model.
/// ///
/// You can optionally pass in some initialization memory for partial /// You can optionally pass in some initialization memory for partial
/// execution. /// execution.
@ -802,13 +801,12 @@ impl ExecutorContext {
pub async fn run_concurrent( pub async fn run_concurrent(
&self, &self,
program: &crate::Program, program: &crate::Program,
cached_body_items: usize,
exec_state: &mut ExecState, exec_state: &mut ExecState,
universe_info: Option<(Universe, UniverseMap)>, universe_info: Option<(Universe, UniverseMap)>,
preserve_mem: bool, preserve_mem: bool,
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> { ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
// Reuse our cached universe if we have one. // Reuse our cached universe if we have one.
#[allow(unused_variables)]
let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info { let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
(universe, universe_map) (universe, universe_map)
} else { } else {
@ -822,29 +820,8 @@ impl ExecutorContext {
.await .await
.map_err(KclErrorWithOutputs::no_outputs)?; .map_err(KclErrorWithOutputs::no_outputs)?;
for modules in crate::walk::import_graph(&universe, self) for modules in import_graph::import_graph(&universe, self)
.map_err(|err| { .map_err(|err| exec_state.error_with_outputs(err, default_planes.clone()))?
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
err,
exec_state.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
exec_state.global.operations.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_commands.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes.clone(),
)
})?
.into_iter() .into_iter()
{ {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -858,7 +835,7 @@ impl ExecutorContext {
for module in modules { for module in modules {
let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else { let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
return Err(KclErrorWithOutputs::no_outputs(KclError::Internal( return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()), KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
))); )));
}; };
@ -866,32 +843,14 @@ impl ExecutorContext {
let module_path = module_path.clone(); let module_path = module_path.clone();
let source_range = SourceRange::from(import_stmt); let source_range = SourceRange::from(import_stmt);
#[cfg(feature = "artifact-graph")] self.add_import_module_ops(
match &module_path { exec_state,
ModulePath::Main => { program,
// This should never happen. module_id,
} &module_path,
ModulePath::Local { value, .. } => { source_range,
// We only want to display the top-level module imports in &universe_map,
// the Feature Tree, not transitive imports. );
if universe_map.contains_key(value) {
exec_state.global.operations.push(Operation::GroupBegin {
group: Group::ModuleInstance {
name: value.file_name().unwrap_or_default(),
module_id,
},
source_range,
});
// Due to concurrent execution, we cannot easily
// group operations by module. So we leave the
// group empty and close it immediately.
exec_state.global.operations.push(Operation::GroupEnd);
}
}
ModulePath::Std { .. } => {
// We don't want to display stdlib in the Feature Tree.
}
}
let repr = repr.clone(); let repr = repr.clone();
let exec_state = exec_state.clone(); let exec_state = exec_state.clone();
@ -920,7 +879,7 @@ impl ExecutorContext {
result.map(|val| ModuleRepr::Foreign(geom.clone(), val)) result.map(|val| ModuleRepr::Foreign(geom.clone(), val))
} }
ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::Internal(KclErrorDetails::new( ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
format!("Module {module_path} not found in universe"), format!("Module {module_path} not found in universe"),
vec![source_range], vec![source_range],
))), ))),
@ -930,7 +889,6 @@ impl ExecutorContext {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
wasm_bindgen_futures::spawn_local(async move { wasm_bindgen_futures::spawn_local(async move {
//set.spawn(async move {
let mut exec_state = exec_state; let mut exec_state = exec_state;
let exec_ctxt = exec_ctxt; let exec_ctxt = exec_ctxt;
@ -1000,33 +958,13 @@ impl ExecutorContext {
exec_state.global.module_infos[&module_id].restore_repr(repr); exec_state.global.module_infos[&module_id].restore_repr(repr);
} }
Err(e) => { Err(e) => {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state return Err(exec_state.error_with_outputs(e, default_planes));
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
return Err(KclErrorWithOutputs::new(
e,
exec_state.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
exec_state.global.operations.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_commands.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes,
));
} }
} }
} }
} }
self.inner_run(program, cached_body_items, exec_state, preserve_mem) self.inner_run(program, exec_state, preserve_mem).await
.await
} }
/// Get the universe & universe map of the program. /// Get the universe & universe map of the program.
@ -1042,7 +980,7 @@ impl ExecutorContext {
let default_planes = self.engine.get_default_planes().read().await.clone(); let default_planes = self.engine.get_default_planes().read().await.clone();
let root_imports = crate::walk::import_universe( let root_imports = import_graph::import_universe(
self, self,
&ModulePath::Main, &ModulePath::Main,
&ModuleRepr::Kcl(program.ast.clone(), None), &ModuleRepr::Kcl(program.ast.clone(), None),
@ -1050,39 +988,77 @@ impl ExecutorContext {
exec_state, exec_state,
) )
.await .await
.map_err(|err| { .map_err(|err| exec_state.error_with_outputs(err, default_planes))?;
println!("Error: {err:?}");
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
err,
exec_state.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
exec_state.global.operations.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_commands.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes,
)
})?;
Ok((universe, root_imports)) Ok((universe, root_imports))
} }
#[cfg(feature = "artifact-graph")]
fn add_import_module_ops(
&self,
exec_state: &mut ExecState,
program: &crate::Program,
module_id: ModuleId,
module_path: &ModulePath,
source_range: SourceRange,
universe_map: &UniverseMap,
) {
match module_path {
ModulePath::Main => {
// This should never happen.
}
ModulePath::Local { value, .. } => {
// We only want to display the top-level module imports in
// the Feature Tree, not transitive imports.
if universe_map.contains_key(value) {
use crate::NodePath;
let node_path = if source_range.is_top_level_module() {
let cached_body_items = exec_state.global.artifacts.cached_body_items();
NodePath::from_range(&program.ast, cached_body_items, source_range).unwrap_or_default()
} else {
// The frontend doesn't care about paths in
// files other than the top-level module.
NodePath::placeholder()
};
exec_state.push_op(Operation::GroupBegin {
group: Group::ModuleInstance {
name: value.file_name().unwrap_or_default(),
module_id,
},
node_path,
source_range,
});
// Due to concurrent execution, we cannot easily
// group operations by module. So we leave the
// group empty and close it immediately.
exec_state.push_op(Operation::GroupEnd);
}
}
ModulePath::Std { .. } => {
// We don't want to display stdlib in the Feature Tree.
}
}
}
#[cfg(not(feature = "artifact-graph"))]
fn add_import_module_ops(
&self,
_exec_state: &mut ExecState,
_program: &crate::Program,
_module_id: ModuleId,
_module_path: &ModulePath,
_source_range: SourceRange,
_universe_map: &UniverseMap,
) {
}
/// Perform the execution of a program. Accept all possible parameters and /// Perform the execution of a program. Accept all possible parameters and
/// output everything. /// output everything.
async fn inner_run( async fn inner_run(
&self, &self,
program: &crate::Program, program: &crate::Program,
cached_body_items: usize,
exec_state: &mut ExecState, exec_state: &mut ExecState,
preserve_mem: bool, preserve_mem: bool,
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> { ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
@ -1096,7 +1072,7 @@ impl ExecutorContext {
let default_planes = self.engine.get_default_planes().read().await.clone(); let default_planes = self.engine.get_default_planes().read().await.clone();
let result = self let result = self
.execute_and_build_graph(&program.ast, cached_body_items, exec_state, preserve_mem) .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
.await; .await;
crate::log::log(format!( crate::log::log(format!(
@ -1105,28 +1081,7 @@ impl ExecutorContext {
)); ));
crate::log::log(format!("Engine stats: {:?}", self.engine.stats())); crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
let env_ref = result.map_err(|e| { let env_ref = result.map_err(|e| exec_state.error_with_outputs(e, default_planes))?;
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
e,
exec_state.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
exec_state.global.operations.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_commands.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes.clone(),
)
})?;
if !self.is_mock() { if !self.is_mock() {
let mut mem = exec_state.stack().deep_clone(); let mut mem = exec_state.stack().deep_clone();
@ -1143,13 +1098,17 @@ impl ExecutorContext {
async fn execute_and_build_graph( async fn execute_and_build_graph(
&self, &self,
program: NodeRef<'_, crate::parsing::ast::types::Program>, program: NodeRef<'_, crate::parsing::ast::types::Program>,
#[cfg_attr(not(feature = "artifact-graph"), expect(unused))] cached_body_items: usize,
exec_state: &mut ExecState, exec_state: &mut ExecState,
preserve_mem: bool, preserve_mem: bool,
) -> Result<EnvironmentRef, KclError> { ) -> Result<EnvironmentRef, KclError> {
// Don't early return! We need to build other outputs regardless of // Don't early return! We need to build other outputs regardless of
// whether execution failed. // whether execution failed.
// Because of execution caching, we may start with operations from a
// previous run.
#[cfg(feature = "artifact-graph")]
let start_op = exec_state.global.artifacts.operations.len();
self.eval_prelude(exec_state, SourceRange::from(program).start_as_range()) self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
.await?; .await?;
@ -1163,6 +1122,29 @@ impl ExecutorContext {
) )
.await; .await;
#[cfg(feature = "artifact-graph")]
{
// Fill in NodePath for operations.
let cached_body_items = exec_state.global.artifacts.cached_body_items();
for op in exec_state.global.artifacts.operations.iter_mut().skip(start_op) {
match op {
Operation::StdLibCall {
node_path,
source_range,
..
}
| Operation::GroupBegin {
node_path,
source_range,
..
} => {
node_path.fill_placeholder(program, cached_body_items, *source_range);
}
Operation::GroupEnd => {}
}
}
}
// Ensure all the async commands completed. // Ensure all the async commands completed.
self.engine.ensure_async_commands_completed().await?; self.engine.ensure_async_commands_completed().await?;
@ -1170,40 +1152,9 @@ impl ExecutorContext {
// and should be dropped. // and should be dropped.
self.engine.clear_queues().await; self.engine.clear_queues().await;
#[cfg(feature = "artifact-graph")] match exec_state.build_artifact_graph(&self.engine, program).await {
{ Ok(_) => exec_result.map(|(_, env_ref, _)| env_ref),
let new_commands = self.engine.take_artifact_commands().await; Err(err) => exec_result.and(Err(err)),
let new_responses = self.engine.take_responses().await;
let initial_graph = exec_state.global.artifact_graph.clone();
// Build the artifact graph.
let graph_result = build_artifact_graph(
&new_commands,
&new_responses,
program,
cached_body_items,
&mut exec_state.global.artifacts,
initial_graph,
);
// Move the artifact commands and responses into ExecState to
// simplify cache management and error creation.
exec_state.global.artifact_commands.extend(new_commands);
exec_state.global.artifact_responses.extend(new_responses);
match graph_result {
Ok(artifact_graph) => {
exec_state.global.artifact_graph = artifact_graph;
exec_result.map(|(_, env_ref, _)| env_ref)
}
Err(err) => {
// Prefer the exec error.
exec_result.and(Err(err))
}
}
}
#[cfg(not(feature = "artifact-graph"))]
{
exec_result.map(|(_, env_ref, _)| env_ref)
} }
} }
@ -1283,7 +1234,7 @@ impl ExecutorContext {
.await?; .await?;
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else { let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
return Err(KclError::Internal(crate::errors::KclErrorDetails::new( return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
format!("Expected Export response, got {resp:?}",), format!("Expected Export response, got {resp:?}",),
vec![SourceRange::default()], vec![SourceRange::default()],
))); )));
@ -1303,7 +1254,7 @@ impl ExecutorContext {
coords: *kittycad_modeling_cmds::coord::KITTYCAD, coords: *kittycad_modeling_cmds::coord::KITTYCAD,
created: if deterministic_time { created: if deterministic_time {
Some("2021-01-01T00:00:00Z".parse().map_err(|e| { Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
KclError::Internal(crate::errors::KclErrorDetails::new( KclError::new_internal(crate::errors::KclErrorDetails::new(
format!("Failed to parse date: {}", e), format!("Failed to parse date: {}", e),
vec![SourceRange::default()], vec![SourceRange::default()],
)) ))
@ -1383,14 +1334,13 @@ pub(crate) async fn parse_execute_with_project_dir(
let exec_ctxt = ExecutorContext { let exec_ctxt = ExecutorContext {
engine: Arc::new(Box::new( engine: Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| { crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
KclError::Internal(crate::errors::KclErrorDetails::new( KclError::new_internal(crate::errors::KclErrorDetails::new(
format!("Failed to create mock engine connection: {}", err), format!("Failed to create mock engine connection: {}", err),
vec![SourceRange::default()], vec![SourceRange::default()],
)) ))
})?, })?,
)), )),
fs: Arc::new(crate::fs::FileManager::new()), fs: Arc::new(crate::fs::FileManager::new()),
stdlib: Arc::new(crate::std::StdLib::new()),
settings: ExecutorSettings { settings: ExecutorSettings {
project_directory, project_directory,
..Default::default() ..Default::default()
@ -1799,7 +1749,7 @@ foo
let err = result.unwrap_err(); let err = result.unwrap_err();
assert_eq!( assert_eq!(
err, err,
KclError::Syntax(KclErrorDetails::new( KclError::new_syntax(KclErrorDetails::new(
"Unexpected token: #".to_owned(), "Unexpected token: #".to_owned(),
vec![SourceRange::new(14, 15, ModuleId::default())], vec![SourceRange::new(14, 15, ModuleId::default())],
)), )),
@ -2058,7 +2008,7 @@ notTagIdentifier = !myTag";
// TODO: We don't currently parse this, but we should. It should be // TODO: We don't currently parse this, but we should. It should be
// a runtime error instead. // a runtime error instead.
parse_execute(code10).await.unwrap_err(), parse_execute(code10).await.unwrap_err(),
KclError::Syntax(KclErrorDetails::new( KclError::new_syntax(KclErrorDetails::new(
"Unexpected token: !".to_owned(), "Unexpected token: !".to_owned(),
vec![SourceRange::new(10, 11, ModuleId::default())], vec![SourceRange::new(10, 11, ModuleId::default())],
)) ))
@ -2071,8 +2021,8 @@ notPipeSub = 1 |> identity(!%))";
// TODO: We don't currently parse this, but we should. It should be // TODO: We don't currently parse this, but we should. It should be
// a runtime error instead. // a runtime error instead.
parse_execute(code11).await.unwrap_err(), parse_execute(code11).await.unwrap_err(),
KclError::Syntax(KclErrorDetails::new( KclError::new_syntax(KclErrorDetails::new(
"There was an unexpected !. Try removing it.".to_owned(), "There was an unexpected `!`. Try removing it.".to_owned(),
vec![SourceRange::new(56, 57, ModuleId::default())], vec![SourceRange::new(56, 57, ModuleId::default())],
)) ))
); );
@ -2206,7 +2156,7 @@ w = f() + f()
} }
// Get the id_generator from the first execution. // Get the id_generator from the first execution.
let id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator; let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
let code = r#"sketch001 = startSketchOn(XZ) let code = r#"sketch001 = startSketchOn(XZ)
|> startProfile(at = [62.74, 206.13]) |> startProfile(at = [62.74, 206.13])
@ -2227,7 +2177,7 @@ w = f() + f()
// Execute the program. // Execute the program.
ctx.run_with_caching(program).await.unwrap(); ctx.run_with_caching(program).await.unwrap();
let new_id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator; let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
assert_eq!(id_generator, new_id_generator); assert_eq!(id_generator, new_id_generator);
} }

View File

@ -12,19 +12,19 @@ use uuid::Uuid;
use crate::execution::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId}; use crate::execution::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails, Severity}, errors::{KclError, KclErrorDetails, Severity},
exec::DefaultPlanes,
execution::{ execution::{
annotations, annotations,
cad_op::Operation, cad_op::Operation,
id_generator::IdGenerator, id_generator::IdGenerator,
memory::{ProgramMemory, Stack}, memory::{ProgramMemory, Stack},
types, types::{self, NumericType},
types::NumericType,
EnvironmentRef, ExecOutcome, ExecutorSettings, KclValue, UnitAngle, UnitLen, EnvironmentRef, ExecOutcome, ExecutorSettings, KclValue, UnitAngle, UnitLen,
}, },
modules::{ModuleId, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr, ModuleSource}, modules::{ModuleId, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr, ModuleSource},
parsing::ast::types::Annotation, parsing::ast::types::{Annotation, NodeRef},
source_range::SourceRange, source_range::SourceRange,
CompilationError, CompilationError, EngineManager, ExecutorContext, KclErrorWithOutputs,
}; };
/// State for executing a program. /// State for executing a program.
@ -32,7 +32,6 @@ use crate::{
pub struct ExecState { pub struct ExecState {
pub(super) global: GlobalState, pub(super) global: GlobalState,
pub(super) mod_local: ModuleState, pub(super) mod_local: ModuleState,
pub(super) exec_context: Option<super::ExecutorContext>,
} }
pub type ModuleInfoMap = IndexMap<ModuleId, ModuleInfo>; pub type ModuleInfoMap = IndexMap<ModuleId, ModuleInfo>;
@ -45,33 +44,39 @@ pub(super) struct GlobalState {
pub id_to_source: IndexMap<ModuleId, ModuleSource>, pub id_to_source: IndexMap<ModuleId, ModuleSource>,
/// Map from module ID to module info. /// Map from module ID to module info.
pub module_infos: ModuleInfoMap, pub module_infos: ModuleInfoMap,
/// Output map of UUIDs to artifacts.
#[cfg(feature = "artifact-graph")]
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.
#[cfg(feature = "artifact-graph")]
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.
#[cfg(feature = "artifact-graph")]
pub artifact_responses: IndexMap<Uuid, WebSocketResponse>,
/// Output artifact graph.
#[cfg(feature = "artifact-graph")]
pub artifact_graph: ArtifactGraph,
/// Operations that have been performed in execution order, for display in
/// the Feature Tree.
#[cfg(feature = "artifact-graph")]
pub operations: Vec<Operation>,
/// Module loader. /// Module loader.
pub mod_loader: ModuleLoader, pub mod_loader: ModuleLoader,
/// Errors and warnings. /// Errors and warnings.
pub errors: Vec<CompilationError>, pub errors: Vec<CompilationError>,
#[allow(dead_code)]
pub artifacts: ArtifactState,
} }
#[cfg(feature = "artifact-graph")]
#[derive(Debug, Clone, Default)]
pub(super) struct ArtifactState {
/// 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 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 responses: IndexMap<Uuid, WebSocketResponse>,
/// Output artifact graph.
pub graph: ArtifactGraph,
/// Operations that have been performed in execution order, for display in
/// the Feature Tree.
pub operations: Vec<Operation>,
}
#[cfg(not(feature = "artifact-graph"))]
#[derive(Debug, Clone, Default)]
pub(super) struct ArtifactState {}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(super) struct ModuleState { pub(super) struct ModuleState {
/// The id generator for this module. /// The id generator for this module.
@ -80,6 +85,11 @@ pub(super) struct ModuleState {
/// The current value of the pipe operator returned from the previous /// The current value of the pipe operator returned from the previous
/// expression. If we're not currently in a pipeline, this will be None. /// expression. If we're not currently in a pipeline, this will be None.
pub pipe_value: Option<KclValue>, pub pipe_value: Option<KclValue>,
/// The closest variable declaration being executed in any parent node in the AST.
/// This is used to provide better error messages, e.g. noticing when the user is trying
/// to use the variable `length` inside the RHS of its own definition, like `length = tan(length)`.
/// TODO: Make this a reference.
pub being_declared: Option<String>,
/// Identifiers that have been exported from the current module. /// Identifiers that have been exported from the current module.
pub module_exports: Vec<String>, pub module_exports: Vec<String>,
/// Settings specified from annotations. /// Settings specified from annotations.
@ -93,7 +103,6 @@ impl ExecState {
ExecState { ExecState {
global: GlobalState::new(&exec_context.settings), global: GlobalState::new(&exec_context.settings),
mod_local: ModuleState::new(ModulePath::Main, ProgramMemory::new(), Default::default()), mod_local: ModuleState::new(ModulePath::Main, ProgramMemory::new(), Default::default()),
exec_context: Some(exec_context.clone()),
} }
} }
@ -103,7 +112,6 @@ impl ExecState {
*self = ExecState { *self = ExecState {
global, global,
mod_local: ModuleState::new(self.mod_local.path.clone(), ProgramMemory::new(), Default::default()), mod_local: ModuleState::new(self.mod_local.path.clone(), ProgramMemory::new(), Default::default()),
exec_context: Some(exec_context.clone()),
}; };
} }
@ -125,45 +133,26 @@ impl ExecState {
/// Convert to execution outcome when running in WebAssembly. We want to /// Convert to execution outcome when running in WebAssembly. We want to
/// reduce the amount of data that crosses the WASM boundary as much as /// reduce the amount of data that crosses the WASM boundary as much as
/// possible. /// possible.
pub async fn to_exec_outcome(self, main_ref: EnvironmentRef) -> ExecOutcome { pub async fn to_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal // Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState. // state when we add more to ExecState.
ExecOutcome { ExecOutcome {
variables: self variables: self.mod_local.variables(main_ref),
.stack() filenames: self.global.filenames(),
.find_all_in_env(main_ref)
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
operations: self.global.operations, operations: self.global.artifacts.operations,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_commands: self.global.artifact_commands, artifact_commands: self.global.artifacts.commands,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_graph: self.global.artifact_graph, artifact_graph: self.global.artifacts.graph,
errors: self.global.errors, errors: self.global.errors,
filenames: self default_planes: ctx.engine.get_default_planes().read().await.clone(),
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect(),
default_planes: if let Some(ctx) = &self.exec_context {
ctx.engine.get_default_planes().read().await.clone()
} else {
None
},
} }
} }
pub async fn to_mock_exec_outcome(self, main_ref: EnvironmentRef) -> ExecOutcome { pub async fn to_mock_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState.
ExecOutcome { ExecOutcome {
variables: self variables: self.mod_local.variables(main_ref),
.stack()
.find_all_in_env(main_ref)
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
operations: Default::default(), operations: Default::default(),
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
@ -172,11 +161,7 @@ impl ExecState {
artifact_graph: Default::default(), artifact_graph: Default::default(),
errors: self.global.errors, errors: self.global.errors,
filenames: Default::default(), filenames: Default::default(),
default_planes: if let Some(ctx) = &self.exec_context { default_planes: ctx.engine.get_default_planes().read().await.clone(),
ctx.engine.get_default_planes().read().await.clone()
} else {
None
},
} }
} }
@ -199,12 +184,12 @@ impl ExecState {
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub(crate) fn add_artifact(&mut self, artifact: Artifact) { pub(crate) fn add_artifact(&mut self, artifact: Artifact) {
let id = artifact.id(); let id = artifact.id();
self.global.artifacts.insert(id, artifact); self.global.artifacts.artifacts.insert(id, artifact);
} }
pub(crate) fn push_op(&mut self, op: Operation) { pub(crate) fn push_op(&mut self, op: Operation) {
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
self.global.operations.push(op); self.global.artifacts.operations.push(op);
#[cfg(not(feature = "artifact-graph"))] #[cfg(not(feature = "artifact-graph"))]
drop(op); drop(op);
} }
@ -246,10 +231,6 @@ impl ExecState {
self.global.id_to_source.insert(id, source.clone()); self.global.id_to_source.insert(id, source.clone());
} }
pub(super) fn get_source(&self, id: ModuleId) -> Option<&ModuleSource> {
self.global.id_to_source.get(&id)
}
pub(super) fn add_module(&mut self, id: ModuleId, path: ModulePath, repr: ModuleRepr) { pub(super) fn add_module(&mut self, id: ModuleId, path: ModulePath, repr: ModuleRepr) {
debug_assert!(self.global.path_to_source_id.contains_key(&path)); debug_assert!(self.global.path_to_source_id.contains_key(&path));
let module_info = ModuleInfo { id, repr, path }; let module_info = ModuleInfo { id, repr, path };
@ -276,7 +257,7 @@ impl ExecState {
} }
pub(super) fn circular_import_error(&self, path: &ModulePath, source_range: SourceRange) -> KclError { pub(super) fn circular_import_error(&self, path: &ModulePath, source_range: SourceRange) -> KclError {
KclError::ImportCycle(KclErrorDetails::new( KclError::new_import_cycle(KclErrorDetails::new(
format!( format!(
"circular import of modules is not allowed: {} -> {}", "circular import of modules is not allowed: {} -> {}",
self.global self.global
@ -295,6 +276,71 @@ impl ExecState {
pub(crate) fn pipe_value(&self) -> Option<&KclValue> { pub(crate) fn pipe_value(&self) -> Option<&KclValue> {
self.mod_local.pipe_value.as_ref() self.mod_local.pipe_value.as_ref()
} }
pub(crate) fn error_with_outputs(
&self,
error: KclError,
default_planes: Option<DefaultPlanes>,
) -> KclErrorWithOutputs {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = self
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
error,
self.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
self.global.artifacts.operations.clone(),
#[cfg(feature = "artifact-graph")]
self.global.artifacts.commands.clone(),
#[cfg(feature = "artifact-graph")]
self.global.artifacts.graph.clone(),
module_id_to_module_path,
self.global.id_to_source.clone(),
default_planes,
)
}
#[cfg(feature = "artifact-graph")]
pub(crate) async fn build_artifact_graph(
&mut self,
engine: &Arc<Box<dyn EngineManager>>,
program: NodeRef<'_, crate::parsing::ast::types::Program>,
) -> Result<(), KclError> {
let new_commands = engine.take_artifact_commands().await;
let new_responses = engine.take_responses().await;
let initial_graph = self.global.artifacts.graph.clone();
// Build the artifact graph.
let graph_result = crate::execution::artifact::build_artifact_graph(
&new_commands,
&new_responses,
program,
&mut self.global.artifacts.artifacts,
initial_graph,
);
// Move the artifact commands and responses into ExecState to
// simplify cache management and error creation.
self.global.artifacts.commands.extend(new_commands);
self.global.artifacts.responses.extend(new_responses);
let artifact_graph = graph_result?;
self.global.artifacts.graph = artifact_graph;
Ok(())
}
#[cfg(not(feature = "artifact-graph"))]
pub(crate) async fn build_artifact_graph(
&mut self,
_engine: &Arc<Box<dyn EngineManager>>,
_program: NodeRef<'_, crate::parsing::ast::types::Program>,
) -> Result<(), KclError> {
Ok(())
}
} }
impl GlobalState { impl GlobalState {
@ -302,16 +348,7 @@ impl GlobalState {
let mut global = GlobalState { let mut global = GlobalState {
path_to_source_id: Default::default(), path_to_source_id: Default::default(),
module_infos: Default::default(), module_infos: Default::default(),
#[cfg(feature = "artifact-graph")]
artifacts: Default::default(), artifacts: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_commands: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_responses: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_graph: Default::default(),
#[cfg(feature = "artifact-graph")]
operations: Default::default(),
mod_loader: Default::default(), mod_loader: Default::default(),
errors: Default::default(), errors: Default::default(),
id_to_source: Default::default(), id_to_source: Default::default(),
@ -334,6 +371,21 @@ impl GlobalState {
.insert(ModulePath::Local { value: root_path }, root_id); .insert(ModulePath::Local { value: root_path }, root_id);
global global
} }
pub(super) fn filenames(&self) -> IndexMap<ModuleId, ModulePath> {
self.path_to_source_id.iter().map(|(k, v)| ((*v), k.clone())).collect()
}
pub(super) fn get_source(&self, id: ModuleId) -> Option<&ModuleSource> {
self.id_to_source.get(&id)
}
}
#[cfg(feature = "artifact-graph")]
impl ArtifactState {
pub fn cached_body_items(&self) -> usize {
self.graph.item_count
}
} }
impl ModuleState { impl ModuleState {
@ -342,6 +394,7 @@ impl ModuleState {
id_generator: IdGenerator::new(module_id), id_generator: IdGenerator::new(module_id),
stack: memory.new_stack(), stack: memory.new_stack(),
pipe_value: Default::default(), pipe_value: Default::default(),
being_declared: Default::default(),
module_exports: Default::default(), module_exports: Default::default(),
explicit_length_units: false, explicit_length_units: false,
path, path,
@ -352,6 +405,13 @@ impl ModuleState {
}, },
} }
} }
pub(super) fn variables(&self, main_ref: EnvironmentRef) -> IndexMap<String, KclValue> {
self.stack
.find_all_in_env(main_ref)
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
} }
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)] #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
@ -389,7 +449,7 @@ impl MetaSettings {
self.kcl_version = value; self.kcl_version = value;
} }
name => { name => {
return Err(KclError::Semantic(KclErrorDetails::new( return Err(KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"Unexpected settings key: `{name}`; expected one of `{}`, `{}`", "Unexpected settings key: `{name}`; expected one of `{}`, `{}`",
annotations::SETTINGS_UNIT_LENGTH, annotations::SETTINGS_UNIT_LENGTH,

View File

@ -220,6 +220,7 @@ impl schemars::JsonSchema for TypedPath {
/// ///
/// * Does **not** touch `..` or symlinks call `canonicalize()` if you need that. /// * Does **not** touch `..` or symlinks call `canonicalize()` if you need that.
/// * Returns an owned `PathBuf` only when normalisation was required. /// * Returns an owned `PathBuf` only when normalisation was required.
#[cfg(not(target_arch = "wasm32"))]
fn normalise_import<S: AsRef<str>>(raw: S) -> std::path::PathBuf { fn normalise_import<S: AsRef<str>>(raw: S) -> std::path::PathBuf {
let s = raw.as_ref(); let s = raw.as_ref();
// On Unix we need to swap `\` → `/`. On Windows we leave it alone. // On Unix we need to swap `\` → `/`. On Windows we leave it alone.

View File

@ -187,7 +187,7 @@ impl RuntimeType {
}; };
RuntimeType::Primitive(PrimitiveType::Number(ty)) RuntimeType::Primitive(PrimitiveType::Number(ty))
} }
AstPrimitiveType::Named(name) => Self::from_alias(&name.name, exec_state, source_range)?, AstPrimitiveType::Named { id } => Self::from_alias(&id.name, exec_state, source_range)?,
AstPrimitiveType::Tag => RuntimeType::Primitive(PrimitiveType::Tag), AstPrimitiveType::Tag => RuntimeType::Primitive(PrimitiveType::Tag),
AstPrimitiveType::ImportedGeometry => RuntimeType::Primitive(PrimitiveType::ImportedGeometry), AstPrimitiveType::ImportedGeometry => RuntimeType::Primitive(PrimitiveType::ImportedGeometry),
AstPrimitiveType::Function(_) => RuntimeType::Primitive(PrimitiveType::Function), AstPrimitiveType::Function(_) => RuntimeType::Primitive(PrimitiveType::Function),

View File

@ -28,7 +28,7 @@ impl Default for FileManager {
impl FileSystem for FileManager { impl FileSystem for FileManager {
async fn read(&self, path: &TypedPath, source_range: SourceRange) -> Result<Vec<u8>, KclError> { async fn read(&self, path: &TypedPath, source_range: SourceRange) -> Result<Vec<u8>, KclError> {
tokio::fs::read(&path.0).await.map_err(|e| { tokio::fs::read(&path.0).await.map_err(|e| {
KclError::Io(KclErrorDetails::new( KclError::new_io(KclErrorDetails::new(
format!("Failed to read file `{}`: {}", path.display(), e), format!("Failed to read file `{}`: {}", path.display(), e),
vec![source_range], vec![source_range],
)) ))
@ -37,7 +37,7 @@ impl FileSystem for FileManager {
async fn read_to_string(&self, path: &TypedPath, source_range: SourceRange) -> Result<String, KclError> { async fn read_to_string(&self, path: &TypedPath, source_range: SourceRange) -> Result<String, KclError> {
tokio::fs::read_to_string(&path.0).await.map_err(|e| { tokio::fs::read_to_string(&path.0).await.map_err(|e| {
KclError::Io(KclErrorDetails::new( KclError::new_io(KclErrorDetails::new(
format!("Failed to read file `{}`: {}", path.display(), e), format!("Failed to read file `{}`: {}", path.display(), e),
vec![source_range], vec![source_range],
)) ))
@ -49,7 +49,7 @@ impl FileSystem for FileManager {
if e.kind() == std::io::ErrorKind::NotFound { if e.kind() == std::io::ErrorKind::NotFound {
Ok(false) Ok(false)
} else { } else {
Err(KclError::Io(KclErrorDetails::new( Err(KclError::new_io(KclErrorDetails::new(
format!("Failed to check if file `{}` exists: {}", path.display(), e), format!("Failed to check if file `{}` exists: {}", path.display(), e),
vec![source_range], vec![source_range],
))) )))
@ -71,7 +71,7 @@ impl FileSystem for FileManager {
} }
let mut read_dir = tokio::fs::read_dir(&path).await.map_err(|e| { let mut read_dir = tokio::fs::read_dir(&path).await.map_err(|e| {
KclError::Io(KclErrorDetails::new( KclError::new_io(KclErrorDetails::new(
format!("Failed to read directory `{}`: {}", path.display(), e), format!("Failed to read directory `{}`: {}", path.display(), e),
vec![source_range], vec![source_range],
)) ))

View File

@ -49,10 +49,10 @@ impl FileSystem for FileManager {
let promise = self let promise = self
.manager .manager
.read_file(path.to_string_lossy()) .read_file(path.to_string_lossy())
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?; .map_err(|e| KclError::new_engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
let value = JsFuture::from(promise).await.map_err(|e| { let value = JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to wait for promise from engine: {:?}", e), format!("Failed to wait for promise from engine: {:?}", e),
vec![source_range], vec![source_range],
)) ))
@ -67,7 +67,7 @@ impl FileSystem for FileManager {
async fn read_to_string(&self, path: &TypedPath, source_range: SourceRange) -> Result<String, KclError> { async fn read_to_string(&self, path: &TypedPath, source_range: SourceRange) -> Result<String, KclError> {
let bytes = self.read(path, source_range).await?; let bytes = self.read(path, source_range).await?;
let string = String::from_utf8(bytes).map_err(|e| { let string = String::from_utf8(bytes).map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to convert bytes to string: {:?}", e), format!("Failed to convert bytes to string: {:?}", e),
vec![source_range], vec![source_range],
)) ))
@ -80,17 +80,17 @@ impl FileSystem for FileManager {
let promise = self let promise = self
.manager .manager
.exists(path.to_string_lossy()) .exists(path.to_string_lossy())
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?; .map_err(|e| KclError::new_engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
let value = JsFuture::from(promise).await.map_err(|e| { let value = JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to wait for promise from engine: {:?}", e), format!("Failed to wait for promise from engine: {:?}", e),
vec![source_range], vec![source_range],
)) ))
})?; })?;
let it_exists = value.as_bool().ok_or_else(|| { let it_exists = value.as_bool().ok_or_else(|| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
"Failed to convert value to bool".to_string(), "Failed to convert value to bool".to_string(),
vec![source_range], vec![source_range],
)) ))
@ -107,24 +107,24 @@ impl FileSystem for FileManager {
let promise = self let promise = self
.manager .manager
.get_all_files(path.to_string_lossy()) .get_all_files(path.to_string_lossy())
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?; .map_err(|e| KclError::new_engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
let value = JsFuture::from(promise).await.map_err(|e| { let value = JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to wait for promise from javascript: {:?}", e), format!("Failed to wait for promise from javascript: {:?}", e),
vec![source_range], vec![source_range],
)) ))
})?; })?;
let s = value.as_string().ok_or_else(|| { let s = value.as_string().ok_or_else(|| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to get string from response from javascript: `{:?}`", value), format!("Failed to get string from response from javascript: `{:?}`", value),
vec![source_range], vec![source_range],
)) ))
})?; })?;
let files: Vec<String> = serde_json::from_str(&s).map_err(|e| { let files: Vec<String> = serde_json::from_str(&s).map_err(|e| {
KclError::Engine(KclErrorDetails::new( KclError::new_engine(KclErrorDetails::new(
format!("Failed to parse json from javascript: `{}` `{:?}`", s, e), format!("Failed to parse json from javascript: `{}` `{:?}`", s, e),
vec![source_range], vec![source_range],
)) ))

View File

@ -247,17 +247,6 @@ impl ObjectProperty {
} }
} }
impl MemberObject {
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
match self {
MemberObject::MemberExpression(member_expression) => {
member_expression.get_hover_value_for_position(pos, code, opts)
}
MemberObject::Identifier(_identifier) => None,
}
}
}
impl MemberExpression { impl MemberExpression {
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> { fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
let object_source_range: SourceRange = self.object.clone().into(); let object_source_range: SourceRange = self.object.clone().into();

Some files were not shown because too many files have changed in this diff Show More