Compare commits

...

29 Commits

Author SHA1 Message Date
3630696848 KCL: Unlabeled first param defaults to % (#4817)
Part of #4600

KCL functions can declare one special argument that doesn't require a label on its parameter when called.

This PR will default that arg to % (the current pipeline) if not given.
2024-12-16 21:01:23 -06:00
f165d19fda Annotations syntax and per-file default units preparatory work (#4822)
* Parse annotations

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

* Propagate settings from annotations to exec_state

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-17 15:23:00 +13:00
3dd98ae1d5 Implements boolean logical and/or in kcl (#4678)
* redoing bool logic impl on latest main

* adding snapshot tests (removing .new)

* removing accidental change smh:(

* accepting client side scene snapshot

* accepting png snapshot and triggering ci

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

* accepting png again?

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* accepting grid visibility snapshot

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* accepting png snapshot

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* accepting png snapshot

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

* accepting png snapshot

* rerunning simtest creation to get ops.snap files

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-12-16 17:33:08 -05:00
a46e0a0fe7 Support completions from import statements (#4768)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-17 10:48:38 +13:00
8f9dc06228 Whole module imports (#4767)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-16 20:38:32 +00:00
fa22c14723 Reserve syntax for units of measure (#4783)
* Allow underscores but only for un-referenced names

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

* Support numeric suffixes for UoM types

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

* UoM type arguments

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

* warnings -> non-fatal errors

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

* type ascription

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-17 09:01:51 +13:00
1d39983b08 Change KCL completion to use new object/record syntax (#4815) 2024-12-16 18:29:50 +00:00
da301ba862 Add tracking of operations for the feature tree (#4746)
* Add operations tracking for the timeline

* Change to only track certain stdlib functions as operations

* Update gen files

* Add operations to simulation snapshot tests

* Add tracking of positional function calls

* Fix generated field names to be camel case in TS

* Fix generated TS field names to match and better docs

* Fix order of ops with patternTransform

* Fix sweep to be included

* Add new expected test outputs

* Add tracking for startSketchOn

* Update ops output to include startSketchOn

* Fix serde field name

* Fix output field name

* Add tracking of operations that fail

* Add snapshots of operations even when there's a KCL execution error

* Add ops output for error executions

* Add operations output to executor error

* Update op source ranges

* Remove tracking of circle() and polygon() since they're not needed

* Update output without circle and polygon

* Fix to track patternCircular3d and patternLinear3d

* Remove tracking for mirror2d

* Update ops output

* Fix to track the correct source range of function definitions

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-12-16 13:10:31 -05:00
efe8089b08 Revert multi-profile (#4812)
* Revert "multi-profile follow up. (#4802)"

This reverts commit 2b2ed470c1.

* Revert "multi profile (#4532)"

This reverts commit 04e586d07b.

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

* Re-run CI after snapshots

* Re-run CI after snapshots

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Re-run CI after snapshots

* Add `fixme` to onboarding test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-16 10:34:11 -05:00
49de3b0ac9 get ready to bump (kcl-lib and friends) world (#4794)
get ready to bump world

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-16 18:37:03 +11:00
2b2ed470c1 multi-profile follow up. (#4802)
* multi-profile work

* fix enter sketch on cap

* fix coderef problem for walls and caps

* allow sketch mode entry from circle

* clean up

* update snapshot

* Look at this (photo)Graph *in the voice of Nickelback*

* trigger CI

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

* add test

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

* fix how expression index is corrected, to make compatible with offset planes

* another test

* tweak test

* more test tweaks

* break up test to fix it hopfully

* fix onboarding test

* remove bad comment

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-16 18:36:48 +11:00
96652a0c48 Fix onboarding rendering (#4789)
* fix onboarding rendering

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* empty string

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

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

* empty

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

* empty

* can be off by 20

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* can be off by 20

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-14 01:35:34 +00:00
04e586d07b multi profile (#4532)
* multi-profile work

* another test

* clean up

* cover a quirk with a test

* last of tests

* fix typos

* Fix source range in snap test

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-12-13 17:57:33 -05:00
fe5f574a77 Revert "Fix so that tag declarators can be used as parameters (#4692)" (#4788)
This reverts commit e27840219b.
2024-12-13 21:39:40 +00:00
e787495ad0 add a test to make sure we cant shebang in a fn (#4781)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-13 20:10:33 +00:00
8bb9be7a5e Bump kittycad-modeling-cmds (#4777) 2024-12-13 19:42:41 +00:00
00892464e8 Always run cargo test in CI so that it can be required (#4786) 2024-12-13 14:34:23 -05:00
05ed2a3367 Loft uses kw arguments (#4757)
Part of #4600
2024-12-13 13:07:52 -06:00
10cc5bce59 Fix SourceRange values in OrderedCommands to match the TS type (#4785)
* Fix SourceRange type to match WASM commands

* Update artifact graph test snap

* Update artifact graph test
2024-12-13 19:03:24 +00:00
a32f150fc1 KCL tests: Update engine API snapshot (#4784)
When engine merged their big extrude ID bugfix,
they changed the response for extrudes on face.

Basically there's no longer a start face for
something you extrude from a face, because there
just isn't. There's a hole in a previous face,
it's just a gap.

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-12-13 11:24:29 -06:00
ac60082e67 Fix ids for kurt so front end re-uses same ones on executions (#4780)
* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* working test;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Update src/wasm-lib/tests/executor/main.rs

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* Update src/wasm-lib/tests/executor/main.rs

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* fix race condition

* fix whoopsie

* fix tsc

* for some dumb ass reason the model executes twice on load

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-12-13 02:06:26 +00:00
d44dc1b21a Bump wasm-bindgen from 0.2.91 to 0.2.99 and wasm-bindgen-futures from… (#4732)
* Bump wasm-bindgen from 0.2.91 to 0.2.99 and wasm-bindgen-futures from 0.4.44 to 0.4.49

* Upgrade in the kcl crate also

* Update web-sys version constraint to match lock
2024-12-12 15:37:50 -06:00
813962ea4c Revert "warn on unneccessary brackets (#4769)" (#4776)
This reverts commit 4b6bbbe2c5 (PR #4769) because these tests are failing:

        FAIL [   0.013s] kcl-lib parsing::parser::tests::assign_brackets
        FAIL [   0.012s] kcl-lib parsing::parser::tests::test_arg
2024-12-12 15:37:37 -06:00
738443a6ab add test that ensures we use the cache, from playwright (#4721)
add test that ensures we use the cache;

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-12 15:37:23 -06:00
4b6bbbe2c5 warn on unneccessary brackets (#4769)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-13 08:20:57 +13:00
6ff8addc8b Remove non code from Digests (#4772)
Remove non code from Digests

@jessfraz and I talked it over; for the time being we're going to remove
comments from the AST digest. We already exclude source position, so
this is just increasing the degree to which we're going to ignore things
that are not germane to execution.

Before, we'd digest *some* but not all of the comments in the AST.
Silly, I know, right?

So, this code:

```
firstSketch = startSketchOn('XY')
  |> startProfileAt([-12, 12], %)
  |> line([-24, 0], %) // my thing
  |> close(%)
  |> extrude(6, %)
```

Would digest differently than:

```
firstSketch = startSketchOn('XY')
  |> startProfileAt([-12, 12], %)
  |> line([-24, 0], %)
  |> close(%)
  |> extrude(6, %)
```

Which is wrong. We've fully divested of hashing code comments, so this
will now hash to be the same. Hooray.
2024-12-12 18:55:09 +00:00
da05c38b9e KCL stdlib: Add atan2 function (#4771)
At Lee's request
2024-12-12 18:11:07 +00:00
191b9b71fd KCL: Keyword fn args like "x = 1" not like "x: 1" (#4770)
Aligns with how we're doing objects.
2024-12-12 17:53:35 +00:00
05163fdded Fix KCL warnings in doc comments from let, const, and new fn syntax (#4756)
* Fix KCL warnings in doc comments from let, const, and new fn syntax

* Update docs
2024-12-12 11:33:37 -05:00
229 changed files with 16490 additions and 2988 deletions

View File

@ -2,28 +2,8 @@ on:
push:
branches:
- main
paths:
- 'src/wasm-lib/**.rs'
- 'src/wasm-lib/**.hbs'
- 'src/wasm-lib/**.gen'
- 'src/wasm-lib/**.snap'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'src/wasm-lib/**.kcl'
- .github/workflows/cargo-test.yml
pull_request:
paths:
- 'src/wasm-lib/**.rs'
- 'src/wasm-lib/**.hbs'
- 'src/wasm-lib/**.gen'
- 'src/wasm-lib/**.snap'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'src/wasm-lib/**.kcl'
- .github/workflows/cargo-test.yml
workflow_dispatch:
permissions: read-all
concurrency:

49
docs/kcl/atan2.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -30,6 +30,7 @@ layout: manual
* [`assertLessThan`](kcl/assertLessThan)
* [`assertLessThanOrEq`](kcl/assertLessThanOrEq)
* [`atan`](kcl/atan)
* [`atan2`](kcl/atan2)
* [`bezierCurve`](kcl/bezierCurve)
* [`ceil`](kcl/ceil)
* [`chamfer`](kcl/chamfer)

File diff suppressed because one or more lines are too long

View File

@ -35,7 +35,7 @@ The transform function returns a transform object. All properties of the object
- `rotation.origin` (either "local" i.e. rotate around its own center, "global" i.e. rotate around the scene's center, or a 3D point, defaults to "local")
```js
patternTransform(total_instances: u32, transform_function: FunctionParam, solid_set: SolidSet) -> [Solid]
patternTransform(total_instances: integer, transform_function: FunctionParam, solid_set: SolidSet) -> [Solid]
```
@ -43,7 +43,7 @@ patternTransform(total_instances: u32, transform_function: FunctionParam, solid_
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `total_instances` | `u32` | | Yes |
| `total_instances` | `integer` | | Yes |
| `transform_function` | `FunctionParam` | | Yes |
| `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | A solid or a group of solids. | Yes |

View File

@ -9,7 +9,7 @@ Just like patternTransform, but works on 2D sketches not 3D solids.
```js
patternTransform2d(total_instances: u32, transform_function: FunctionParam, solid_set: SketchSet) -> [Sketch]
patternTransform2d(total_instances: integer, transform_function: FunctionParam, solid_set: SketchSet) -> [Sketch]
```
@ -17,7 +17,7 @@ patternTransform2d(total_instances: u32, transform_function: FunctionParam, soli
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `total_instances` | `u32` | | Yes |
| `total_instances` | `integer` | | Yes |
| `transform_function` | `FunctionParam` | | Yes |
| `solid_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes |

View File

@ -43,7 +43,7 @@ fn sum(arr) {
/* The above is basically like this pseudo-code:
fn sum(arr):
let sumSoFar = 0
sumSoFar = 0
for i in arr:
sumSoFar = add(sumSoFar, i)
return sumSoFar */
@ -96,14 +96,14 @@ fn decagon(radius) {
/* The `decagon` above is basically like this pseudo-code:
fn decagon(radius):
let stepAngle = (1/10) * tau()
let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
stepAngle = (1/10) * tau()
startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
// Here's the reduce part.
let partialDecagon = startOfDecagonSketch
partialDecagon = startOfDecagonSketch
for i in [1..10]:
let x = cos(stepAngle * i) * radius
let y = sin(stepAngle * i) * radius
x = cos(stepAngle * i) * radius
y = sin(stepAngle * i) * radius
partialDecagon = lineTo([x, y], partialDecagon)
fullDecagon = partialDecagon // it's now full
return fullDecagon */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -13,13 +13,18 @@ Data to draw an angled line.
An angle and length with explicitly named parameters
[`PolarCoordsData`](/docs/kcl/types/PolarCoordsData)
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `angle` |`number`| The angle of the line (in degrees). | No |
| `length` |`number`| The length of the line. | No |
----

View File

@ -329,6 +329,23 @@ Data for an imported geometry.
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Module`| | No |
| `value` |[`ModuleId`](/docs/kcl/types/ModuleId)| Any KCL value. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |

View File

@ -0,0 +1,16 @@
---
title: "ModuleId"
excerpt: "Identifier of a source file. Uses a u32 to keep the size small."
layout: manual
---
Identifier of a source file. Uses a u32 to keep the size small.
**Type:** `integer` (`uint32`)

View File

@ -94,6 +94,51 @@ test.describe('Editor tests', () => {
|> close(%)`)
})
test('ensure we use the cache, and do not re-execute', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await page.keyboard.type(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
// Ensure we execute the first time.
await u.openDebugPanel()
await expect(
page.locator('[data-receive-command-type="scene_clear_all"]')
).toHaveCount(2)
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(2)
// Add whitespace to the end of the code.
await u.codeLocator.click()
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('Home')
await page.keyboard.type(' ')
await page.keyboard.press('Enter')
await page.keyboard.type(' ')
// Ensure we don't execute the second time.
await u.openDebugPanel()
// Make sure we didn't clear the scene.
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(3)
await expect(
page.locator('[data-receive-command-type="scene_clear_all"]')
).toHaveCount(2)
})
test('if you click the format button it formats your code and executes so lints are still there', async ({
page,
}) => {

View File

@ -214,23 +214,7 @@ export class SceneFixture {
coords: { x: number; y: number },
diff: number
) => {
let finalValue = colour
await expect
.poll(async () => {
const pixel = (await getPixelRGBs(this.page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
})
.toBeTruthy()
.catch((cause) => {
throw new Error(
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
{ cause }
)
})
await expectPixelColor(this.page, colour, coords, diff)
}
get gizmo() {
@ -246,3 +230,28 @@ export class SceneFixture {
await buttonToTest.click()
}
}
export async function expectPixelColor(
page: Page,
colour: [number, number, number],
coords: { x: number; y: number },
diff: number
) {
let finalValue = colour
await expect
.poll(async () => {
const pixel = (await getPixelRGBs(page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
})
.toBeTruthy()
.catch((cause) => {
throw new Error(
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
{ cause }
)
})
}

View File

@ -19,6 +19,7 @@ import {
TEST_SETTINGS_ONBOARDING_USER_MENU,
} from './storageStates'
import * as TOML from '@iarna/toml'
import { expectPixelColor } from './fixtures/sceneFixture'
test.beforeEach(async ({ context, page }, testInfo) => {
if (testInfo.tags.includes('@electron')) {
@ -45,7 +46,7 @@ test.describe('Onboarding tests', () => {
{ settingsKey: TEST_SETTINGS_KEY }
)
await page.setViewportSize({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart()
@ -54,6 +55,12 @@ test.describe('Onboarding tests', () => {
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8)
})
test(
@ -72,7 +79,7 @@ test.describe('Onboarding tests', () => {
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
const viewportSize = { width: 1200, height: 1000 }
await page.setViewportSize(viewportSize)
await test.step(`Create a project and open to the onboarding`, async () => {
@ -92,6 +99,14 @@ test.describe('Onboarding tests', () => {
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
// TODO: jess make less shit
// Make sure the model loaded
//const XYPlanePoint = { x: 986, y: 522 } as const
//const modelColor: [number, number, number] = [76, 76, 76]
//await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
//await expectPixelColor(page, modelColor, XYPlanePoint, 8)
})
await electronApp.close()
@ -108,7 +123,7 @@ test.describe('Onboarding tests', () => {
}, initialCode)
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart()
// Replay the onboarding
@ -140,6 +155,12 @@ test.describe('Onboarding tests', () => {
return localStorage.getItem('persistCode')
})
).toContain('// Shelf Bracket')
// Make sure the model loaded
const XYPlanePoint = { x: 986, y: 522 } as const
const modelColor: [number, number, number] = [76, 76, 76]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
})
test('Click through each onboarding step', async ({ page }) => {
@ -179,6 +200,17 @@ test.describe('Onboarding tests', () => {
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect(page.url()).not.toContain('onboarding')
await u.openAndClearDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// TODO: jess to fix
// Make sure the model loaded
//const XYPlanePoint = { x: 774, y: 516 } as const
// const modelColor: [number, number, number] = [129, 129, 129]
// await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
// await expectPixelColor(page, modelColor, XYPlanePoint, 20)
})
test('Onboarding redirects and code updating', async ({ page }) => {
@ -393,7 +425,7 @@ test.describe('Onboarding tests', () => {
})
})
test(
test.fixme(
'Restarting onboarding on desktop takes one attempt',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
@ -439,7 +471,7 @@ test(
})
await test.step('Navigate into project', async () => {
await page.setViewportSize({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 1000 })
page.on('console', console.log)
@ -462,7 +494,15 @@ test(
await test.step('Confirm that the onboarding has restarted', async () => {
await expect(tutorialProjectIndicator).toBeVisible()
await expect(tutorialModalText).toBeVisible()
// Make sure the model loaded
const XYPlanePoint = { x: 988, y: 523 } as const
const modelColor: [number, number, number] = [76, 76, 76]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
await tutorialDismissButton.click()
// Make sure model still there.
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
})
await test.step('Clear code and restart onboarding from settings', async () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -119,6 +119,11 @@
"title": "Pipe and Flange Assembly",
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
},
{
"file": "pipe-with-bend.kcl",
"title": "Pipe with bend",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{
"file": "poopy-shoe.kcl",
"title": "Poopy Shoe",

View File

@ -6,6 +6,7 @@ import Tooltip from 'components/Tooltip'
import { CustomIconName } from 'components/CustomIcon'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from 'components/ActionIcon'
import { onboardingPaths } from 'routes/Onboarding/paths'
export interface ModelingPaneProps {
id: string
@ -70,7 +71,7 @@ export const ModelingPane = ({
const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus
const pointerEventsCssClass =
onboardingStatus.current === 'camera'
onboardingStatus.current === onboardingPaths.CAMERA
? 'pointer-events-none '
: 'pointer-events-auto '
return (

View File

@ -19,6 +19,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { onboardingPaths } from 'routes/Onboarding/paths'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -41,7 +42,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const onboardingStatus = settings.context.app.onboardingStatus
const { send, context } = useModelingContext()
const pointerEventsCssClass =
onboardingStatus.current === 'camera' ||
onboardingStatus.current === onboardingPaths.CAMERA ||
context.store?.openPanes.length === 0
? 'pointer-events-none '
: 'pointer-events-auto '

View File

@ -12,6 +12,7 @@ export const kclHighlight = styleTags({
'AddOp MultOp ExpOp': t.arithmeticOperator,
BangOp: t.logicOperator,
CompOp: t.compareOperator,
LogicOp: t.logicOperator,
'Equals Arrow': t.definitionOperator,
PipeOperator: t.controlOperator,
String: t.string,

View File

@ -5,6 +5,7 @@
mult @left
add @left
comp @left
logic @left
pipe @left
range
}
@ -40,7 +41,8 @@ expression[@isGroup=Expression] {
expression !add AddOp expression |
expression !mult MultOp expression |
expression !exp ExpOp expression |
expression !comp CompOp expression
expression !comp CompOp expression |
expression !logic LogicOp expression
} |
UnaryExpression { UnaryOp expression } |
ParenthesizedExpression { "(" expression ")" } |
@ -89,6 +91,7 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
AddOp { "+" | "-" }
MultOp { "/" | "*" | "\\" }
ExpOp { "^" }
LogicOp { "|" | "&" }
BangOp { "!" }
CompOp { "==" | "!=" | "<=" | ">=" | "<" | ">" }
Equals { "=" }

View File

@ -311,8 +311,6 @@ export class KclManager {
// Do not send send scene commands if the program was interrupted, go to clean up
if (!isInterrupted) {
this.addDiagnostics(await lintAst({ ast: ast }))
sceneInfra.modelingSend({ type: 'code edit during sketch' })
setSelectionFilterToDefault(execState.memory, this.engineCommandManager)
if (args.zoomToFit) {
@ -358,7 +356,13 @@ export class KclManager {
this.lastSuccessfulProgramMemory = execState.memory
}
this.ast = { ...ast }
// updateArtifactGraph relies on updated executeState/programMemory
await this.engineCommandManager.updateArtifactGraph(this.ast)
this._executeCallback()
if (!isInterrupted) {
sceneInfra.modelingSend({ type: 'code edit during sketch' })
}
this.engineCommandManager.addCommandLog({
type: 'execution-done',
data: null,

View File

@ -9,6 +9,7 @@ describe('test kclErrToDiagnostic', () => {
kind: 'semantic',
msg: 'Semantic error',
sourceRange: [0, 1, true],
operations: [],
},
{
name: '',
@ -16,6 +17,7 @@ describe('test kclErrToDiagnostic', () => {
kind: 'type',
msg: 'Type error',
sourceRange: [4, 5, true],
operations: [],
},
]
const diagnostics = kclErrorsToDiagnostics(errors)

View File

@ -6,85 +6,94 @@ import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
import { Text } from '@codemirror/state'
import { EditorView } from 'codemirror'
import { SourceRange } from 'lang/wasm'
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
type ExtractKind<T> = T extends { kind: infer K } ? K : never
export class KCLError extends Error {
kind: ExtractKind<RustKclError> | 'name'
sourceRange: SourceRange
msg: string
operations: Operation[]
constructor(
kind: ExtractKind<RustKclError> | 'name',
msg: string,
sourceRange: SourceRange
sourceRange: SourceRange,
operations: Operation[]
) {
super()
this.kind = kind
this.msg = msg
this.sourceRange = sourceRange
this.operations = operations
Object.setPrototypeOf(this, KCLError.prototype)
}
}
export class KCLLexicalError extends KCLError {
constructor(msg: string, sourceRange: SourceRange) {
super('lexical', msg, sourceRange)
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('lexical', msg, sourceRange, operations)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
export class KCLInternalError extends KCLError {
constructor(msg: string, sourceRange: SourceRange) {
super('internal', msg, sourceRange)
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('internal', msg, sourceRange, operations)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
export class KCLSyntaxError extends KCLError {
constructor(msg: string, sourceRange: SourceRange) {
super('syntax', msg, sourceRange)
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('syntax', msg, sourceRange, operations)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
export class KCLSemanticError extends KCLError {
constructor(msg: string, sourceRange: SourceRange) {
super('semantic', msg, sourceRange)
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('semantic', msg, sourceRange, operations)
Object.setPrototypeOf(this, KCLSemanticError.prototype)
}
}
export class KCLTypeError extends KCLError {
constructor(msg: string, sourceRange: SourceRange) {
super('type', msg, sourceRange)
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('type', msg, sourceRange, operations)
Object.setPrototypeOf(this, KCLTypeError.prototype)
}
}
export class KCLUnimplementedError extends KCLError {
constructor(msg: string, sourceRange: SourceRange) {
super('unimplemented', msg, sourceRange)
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('unimplemented', msg, sourceRange, operations)
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
}
}
export class KCLUnexpectedError extends KCLError {
constructor(msg: string, sourceRange: SourceRange) {
super('unexpected', msg, sourceRange)
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
super('unexpected', msg, sourceRange, operations)
Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
}
}
export class KCLValueAlreadyDefined extends KCLError {
constructor(key: string, sourceRange: SourceRange) {
super('name', `Key ${key} was already defined elsewhere`, sourceRange)
constructor(key: string, sourceRange: SourceRange, operations: Operation[]) {
super(
'name',
`Key ${key} was already defined elsewhere`,
sourceRange,
operations
)
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
}
}
export class KCLUndefinedValueError extends KCLError {
constructor(key: string, sourceRange: SourceRange) {
super('name', `Key ${key} has not been defined`, sourceRange)
constructor(key: string, sourceRange: SourceRange, operations: Operation[]) {
super('name', `Key ${key} has not been defined`, sourceRange, operations)
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
}
}
@ -100,11 +109,12 @@ export function lspDiagnosticsToKclErrors(
return diagnostics
.flatMap(
({ range, message }) =>
new KCLError('unexpected', message, [
posToOffset(doc, range.start)!,
posToOffset(doc, range.end)!,
true,
])
new KCLError(
'unexpected',
message,
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, true],
[]
)
)
.sort((a, b) => {
const c = a.sourceRange[0]

View File

@ -480,7 +480,8 @@ const theExtrude = startSketchOn('XY')
new KCLError(
'undefined_value',
'memory item key `myVarZ` is not defined',
[129, 135, true]
[129, 135, true],
[]
)
)
})

View File

@ -66,9 +66,7 @@ export async function executeAst({
? enginelessExecutor(ast, programMemoryOverride)
: _executor(ast, engineCommandManager))
await engineCommandManager.waitForAllCommands(
programMemoryOverride !== undefined
)
await engineCommandManager.waitForAllCommands()
return {
logs: [],

View File

@ -247,7 +247,7 @@ extrude003 = extrude(-15, sketch003)`
selectedSegmentSnippet,
expectedExtrudeSnippet
)
})
}, 5_000)
})
const runModifyAstCloneWithEdgeTreatmentAndTag = async (
@ -477,7 +477,7 @@ extrude001 = extrude(-15, sketch001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
|> chamfer({ length: 5, tags: [seg01] }, %)`
|> chamfer({ length = 5, tags = [seg01] }, %)`
const segmentSnippets = ['line([-20, 0], %)']
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
@ -487,8 +487,8 @@ extrude001 = extrude(-15, sketch001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
|> chamfer({ length: 5, tags: [seg01] }, %)
|> ${edgeTreatmentType}({ ${parameterName}: 3, tags: [seg02] }, %)`
|> chamfer({ length = 5, tags = [seg01] }, %)
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg02] }, %)`
await runModifyAstCloneWithEdgeTreatmentAndTag(
code,

View File

@ -397,6 +397,7 @@ function moreNodePathFromSourceRange(
}
return path
}
return path
}
console.error('not implemented: ' + node.type)

View File

@ -13,7 +13,7 @@ Map {
"range": [
12,
31,
0,
true,
],
},
"id": "UUID",
@ -33,7 +33,7 @@ Map {
"range": [
37,
64,
0,
true,
],
},
"id": "UUID",
@ -60,7 +60,7 @@ Map {
"range": [
70,
86,
0,
true,
],
},
"edgeIds": [
@ -83,7 +83,7 @@ Map {
"range": [
92,
119,
0,
true,
],
},
"edgeCutId": "UUID",
@ -107,7 +107,7 @@ Map {
"range": [
125,
150,
0,
true,
],
},
"edgeIds": [
@ -130,7 +130,7 @@ Map {
"range": [
156,
203,
0,
true,
],
},
"edgeIds": [
@ -153,7 +153,7 @@ Map {
"range": [
209,
217,
0,
true,
],
},
"edgeIds": [],
@ -177,7 +177,7 @@ Map {
"range": [
231,
254,
0,
true,
],
},
"edgeIds": [
@ -320,7 +320,7 @@ Map {
"range": [
260,
299,
0,
true,
],
},
"consumedEdgeId": "UUID",
@ -340,7 +340,7 @@ Map {
"range": [
350,
377,
0,
true,
],
},
"id": "UUID",
@ -366,7 +366,7 @@ Map {
"range": [
383,
398,
0,
true,
],
},
"edgeIds": [
@ -389,7 +389,7 @@ Map {
"range": [
404,
420,
0,
true,
],
},
"edgeIds": [
@ -412,7 +412,7 @@ Map {
"range": [
426,
473,
0,
true,
],
},
"edgeIds": [
@ -435,7 +435,7 @@ Map {
"range": [
479,
487,
0,
true,
],
},
"edgeIds": [],
@ -459,7 +459,7 @@ Map {
"range": [
501,
522,
0,
true,
],
},
"edgeIds": [
@ -478,7 +478,6 @@ Map {
"UUID",
"UUID",
"UUID",
"UUID",
],
"type": "sweep",
},
@ -507,14 +506,6 @@ Map {
"type": "wall",
},
"UUID-34" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"subType": "start",
"sweepId": "UUID",
"type": "cap",
},
"UUID-35" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
@ -522,42 +513,42 @@ Map {
"sweepId": "UUID",
"type": "cap",
},
"UUID-36" => {
"UUID-35" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-36" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-37" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-38" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-39" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-40" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-41" => {
"UUID-40" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",

View File

@ -259,11 +259,13 @@ describe('testing createArtifactGraph', () => {
if (err(extrusion)) throw extrusion
expect(extrusion.type).toBe('sweep')
const firstExtrusionIsACubeIE6Sides = 6
const secondExtrusionIsATriangularPrismIE5Sides = 5
// Each face of the triangular prism (5), but without the bottom cap.
// The engine doesn't generate that.
const secondExtrusionIsATriangularPrism = 4
expect(extrusion.surfaces.length).toBe(
!index
? firstExtrusionIsACubeIE6Sides
: secondExtrusionIsATriangularPrismIE5Sides
: secondExtrusionIsATriangularPrism
)
})
})
@ -659,7 +661,7 @@ describe('testing getArtifactsToUpdate', () => {
sweepId: '',
codeRef: {
pathToNode: [['body', '']],
range: [37, 64, 0],
range: [37, 64, true],
},
},
])
@ -672,7 +674,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: [],
edgeIds: [],
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
@ -683,7 +685,7 @@ describe('testing getArtifactsToUpdate', () => {
planeId: expect.any(String),
sweepId: expect.any(String),
codeRef: {
range: [37, 64, 0],
range: [37, 64, true],
pathToNode: [['body', '']],
},
solid2dId: expect.any(String),
@ -697,7 +699,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: '',
edgeIds: [],
codeRef: {
range: [70, 86, 0],
range: [70, 86, true],
pathToNode: [['body', '']],
},
},
@ -708,7 +710,7 @@ describe('testing getArtifactsToUpdate', () => {
planeId: expect.any(String),
sweepId: expect.any(String),
codeRef: {
range: [37, 64, 0],
range: [37, 64, true],
pathToNode: [['body', '']],
},
solid2dId: expect.any(String),
@ -723,7 +725,7 @@ describe('testing getArtifactsToUpdate', () => {
edgeIds: [],
surfaceId: '',
codeRef: {
range: [260, 299, 0],
range: [260, 299, true],
pathToNode: [['body', '']],
},
},
@ -734,7 +736,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [92, 119, 0],
range: [92, 119, true],
pathToNode: [['body', '']],
},
edgeCutId: expect.any(String),
@ -756,7 +758,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [156, 203, 0],
range: [156, 203, true],
pathToNode: [['body', '']],
},
},
@ -768,7 +770,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
@ -787,7 +789,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [125, 150, 0],
range: [125, 150, true],
pathToNode: [['body', '']],
},
},
@ -799,7 +801,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
@ -818,7 +820,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [92, 119, 0],
range: [92, 119, true],
pathToNode: [['body', '']],
},
edgeCutId: expect.any(String),
@ -831,7 +833,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
@ -850,7 +852,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [70, 86, 0],
range: [70, 86, true],
pathToNode: [['body', '']],
},
},
@ -862,7 +864,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
@ -882,7 +884,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
@ -902,7 +904,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 568 KiB

View File

@ -1,4 +1,11 @@
import { defaultSourceRange, SourceRange } from 'lang/wasm'
import {
defaultRustSourceRange,
defaultSourceRange,
Program,
RustSourceRange,
SourceRange,
sourceRangeFromRust,
} from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
@ -1302,8 +1309,8 @@ export enum EngineCommandManagerEvents {
interface PendingMessage {
command: EngineCommand
range: SourceRange
idToRangeMap: { [key: string]: SourceRange }
range: RustSourceRange
idToRangeMap: { [key: string]: RustSourceRange }
resolve: (data: [Models['WebSocketResponse_type']]) => void
reject: (reason: string) => void
promise: Promise<[Models['WebSocketResponse_type']]>
@ -1993,7 +2000,7 @@ export class EngineCommandManager extends EventTarget {
{
command,
idToRangeMap: {},
range: defaultSourceRange(),
range: defaultRustSourceRange(),
},
true // isSceneCommand
)
@ -2024,9 +2031,9 @@ export class EngineCommandManager extends EventTarget {
return Promise.reject(new Error('rangeStr is undefined'))
if (commandStr === undefined)
return Promise.reject(new Error('commandStr is undefined'))
const range: SourceRange = JSON.parse(rangeStr)
const range: RustSourceRange = JSON.parse(rangeStr)
const command: EngineCommand = JSON.parse(commandStr)
const idToRangeMap: { [key: string]: SourceRange } =
const idToRangeMap: { [key: string]: RustSourceRange } =
JSON.parse(idToRangeStr)
// Current executeAst is stale, going to interrupt, a new executeAst will trigger
@ -2069,10 +2076,14 @@ export class EngineCommandManager extends EventTarget {
if (message.command.type === 'modeling_cmd_req') {
this.orderedCommands.push({
command: message.command,
range: message.range,
range: sourceRangeFromRust(message.range),
})
} else if (message.command.type === 'modeling_cmd_batch_req') {
message.command.requests.forEach((req) => {
const cmdId = req.cmd_id || ''
const range = cmdId
? sourceRangeFromRust(message.idToRangeMap[cmdId])
: defaultSourceRange()
const cmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: req.cmd_id,
@ -2080,7 +2091,7 @@ export class EngineCommandManager extends EventTarget {
}
this.orderedCommands.push({
command: cmd,
range: message.idToRangeMap[req.cmd_id || ''],
range,
})
})
}
@ -2099,30 +2110,23 @@ export class EngineCommandManager extends EventTarget {
* When an execution takes place we want to wait until we've got replies for all of the commands
* When this is done when we build the artifact map synchronously.
*/
async waitForAllCommands(useFakeExecutor = false) {
await Promise.all(Object.values(this.pendingCommands).map((a) => a.promise))
setTimeout(() => {
// the ast is wrong without this one tick timeout.
// an example is `Solids should be select and deletable` e2e test will fail
// because the out of date ast messes with selections
// TODO: race condition
if (!this?.kclManager) return
this.artifactGraph = createArtifactGraph({
orderedCommands: this.orderedCommands,
responseMap: this.responseMap,
ast: this.kclManager.ast,
})
if (useFakeExecutor) {
// mock executions don't produce an artifactGraph, so this will always be empty
// skipping the below logic to wait for the next real execution
return
}
if (this.artifactGraph.size) {
this.deferredArtifactEmptied(null)
} else {
this.deferredArtifactPopulated(null)
}
waitForAllCommands() {
return Promise.all(
Object.values(this.pendingCommands).map((a) => a.promise)
)
}
updateArtifactGraph(ast: Program) {
this.artifactGraph = createArtifactGraph({
orderedCommands: this.orderedCommands,
responseMap: this.responseMap,
ast,
})
// TODO check if these still need to be deferred once e2e tests are working again.
if (this.artifactGraph.size) {
this.deferredArtifactEmptied(null)
} else {
this.deferredArtifactPopulated(null)
}
}
/**

View File

@ -43,6 +43,7 @@ import { Node } from 'wasm-lib/kcl/bindings/Node'
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
@ -65,6 +66,7 @@ export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
export type { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
export type SyntaxType =
| 'Program'
@ -117,6 +119,13 @@ export function defaultSourceRange(): SourceRange {
return [0, 0, true]
}
/**
* Create a default RustSourceRange for testing or as a placeholder.
*/
export function defaultRustSourceRange(): RustSourceRange {
return [0, 0, 0]
}
export const wasmUrl = () => {
// For when we're in electron (file based) or web server (network based)
// For some reason relative paths don't work as expected. Otherwise we would
@ -212,7 +221,8 @@ export const parse = (code: string | Error): ParseResult | Error => {
return new KCLError(
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0])
sourceRangeFromRust(parsed.sourceRanges[0]),
[]
)
}
}
@ -249,7 +259,7 @@ export function emptyExecState(): ExecState {
function execStateFromRaw(raw: RawExecState): ExecState {
return {
memory: ProgramMemory.fromRaw(raw.memory),
memory: ProgramMemory.fromRaw(raw.modLocal.memory),
}
}
@ -531,11 +541,12 @@ export const _executor = async (
return execStateFromRaw(execState)
} catch (e: any) {
console.log(e)
const parsed: RustKclError = JSON.parse(e.toString())
const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
const kclError = new KCLError(
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0])
parsed.error.kind,
parsed.error.msg,
sourceRangeFromRust(parsed.error.sourceRanges[0]),
parsed.operations
)
return Promise.reject(kclError)
@ -594,7 +605,8 @@ export const modifyAstForSketch = async (
const kclError = new KCLError(
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0])
sourceRangeFromRust(parsed.sourceRanges[0]),
[]
)
console.log(kclError)
@ -662,7 +674,8 @@ export function programMemoryInit(): ProgramMemory | Error {
return new KCLError(
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0])
sourceRangeFromRust(parsed.sourceRanges[0]),
[]
)
}
}

View File

@ -15,6 +15,7 @@ import { fileSystemManager } from 'lang/std/fileSystemManager'
import { getProjectInfo } from './desktop'
import { createSettings } from './settings/initialSettings'
import { normalizeLineEndings } from 'lib/codeEditor'
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
// The root loader simply resolves the settings and any errors that
// occurred during the settings load
@ -53,14 +54,15 @@ export const telemetryLoader: LoaderFunction = async ({
// Redirect users to the appropriate onboarding page if they haven't completed it
export const onboardingRedirectLoader: ActionFunction = async (args) => {
const { settings } = await loadAndValidateSettings()
const onboardingStatus = settings.app.onboardingStatus.current || ''
const onboardingStatus: OnboardingStatus =
settings.app.onboardingStatus.current || ''
const notEnRouteToOnboarding = !args.request.url.includes(
PATHS.ONBOARDING.INDEX
)
// '' is the initial state, 'done' and 'dismissed' are the final states
// '' is the initial state, 'completed' and 'dismissed' are the final states
const hasValidOnboardingStatus =
onboardingStatus.length === 0 ||
!(onboardingStatus === 'done' || onboardingStatus === 'dismissed')
!(onboardingStatus === 'completed' || onboardingStatus === 'dismissed')
const shouldRedirectToOnboarding =
notEnRouteToOnboarding && hasValidOnboardingStatus

View File

@ -19,6 +19,7 @@ import Tooltip from 'components/Tooltip'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
/**
* A setting that can be set at the user or project level
@ -189,8 +190,10 @@ export function createSettings() {
inputType: 'boolean',
},
}),
onboardingStatus: new Setting<string>({
onboardingStatus: new Setting<OnboardingStatus>({
defaultValue: '',
// TODO: this could be better but we don't have a TS side real enum
// for this yet
validate: (v) => typeof v === 'string',
hideOnPlatform: 'both',
}),

View File

@ -1,4 +1,6 @@
export const onboardingPaths = {
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
export const onboardingPaths: Record<string, OnboardingStatus> = {
INDEX: '/',
CAMERA: '/camera',
STREAMING: '/streaming',

View File

@ -121,9 +121,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.93"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
dependencies = [
"backtrace",
]
@ -401,9 +401,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.38"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
@ -723,7 +723,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.32"
version = "0.1.33"
dependencies = [
"Inflector",
"anyhow",
@ -1112,7 +1112,7 @@ dependencies = [
"fnv",
"futures-core",
"futures-sink",
"http 1.1.0",
"http 1.2.0",
"indexmap 2.7.0",
"slab",
"tokio",
@ -1215,9 +1215,9 @@ dependencies = [
[[package]]
name = "http"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
dependencies = [
"bytes",
"fnv",
@ -1242,7 +1242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.1.0",
"http 1.2.0",
]
[[package]]
@ -1253,7 +1253,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-util",
"http 1.1.0",
"http 1.2.0",
"http-body 1.0.1",
"pin-project-lite",
]
@ -1303,7 +1303,7 @@ dependencies = [
"futures-channel",
"futures-util",
"h2",
"http 1.1.0",
"http 1.2.0",
"http-body 1.0.1",
"httparse",
"itoa",
@ -1320,7 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http 1.1.0",
"http 1.2.0",
"hyper 1.4.1",
"hyper-util",
"rustls",
@ -1340,7 +1340,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.1.0",
"http 1.2.0",
"http-body 1.0.1",
"hyper 1.4.1",
"pin-project-lite",
@ -1674,16 +1674,17 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.72"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "kcl-lib"
version = "0.2.28"
version = "0.2.29"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1705,7 +1706,7 @@ dependencies = [
"git_rev",
"gltf-json",
"handlebars",
"http 1.1.0",
"http 1.2.0",
"iai",
"image",
"indexmap 2.7.0",
@ -1751,7 +1752,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.18"
version = "0.1.19"
dependencies = [
"anyhow",
"hyper 0.14.30",
@ -1792,7 +1793,7 @@ dependencies = [
"data-encoding",
"format_serde_error",
"futures",
"http 1.1.0",
"http 1.2.0",
"itertools 0.13.0",
"log",
"mime_guess",
@ -1818,9 +1819,9 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.2.77"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b77259b37acafa360d98af27431ac394bc8899eeed7037513832ddbee856811"
checksum = "10a9cab4476455be70ea57643c31444068b056d091bd348cab6044c0d8ad7fcc"
dependencies = [
"anyhow",
"chrono",
@ -1828,7 +1829,7 @@ dependencies = [
"enum-iterator",
"enum-iterator-derive",
"euler",
"http 1.1.0",
"http 1.2.0",
"kittycad-modeling-cmds-macros",
"kittycad-unit-conversion-derive",
"measurements",
@ -2862,7 +2863,7 @@ dependencies = [
"futures-core",
"futures-util",
"h2",
"http 1.1.0",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.4.1",
@ -2904,7 +2905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad7fdf5c0a015763fcd164bee294b13fb7b6f89f1b55961d40f00c3e32d6b"
dependencies = [
"async-trait",
"http 1.1.0",
"http 1.2.0",
"reqwest",
"reqwest-middleware",
]
@ -2917,7 +2918,7 @@ checksum = "d1ccd3b55e711f91a9885a2fa6fbbb2e39db1776420b062efc058c6410f7e5e3"
dependencies = [
"anyhow",
"async-trait",
"http 1.1.0",
"http 1.2.0",
"reqwest",
"serde",
"thiserror 1.0.68",
@ -2934,7 +2935,7 @@ dependencies = [
"async-trait",
"futures",
"getrandom",
"http 1.1.0",
"http 1.2.0",
"hyper 1.4.1",
"parking_lot 0.11.2",
"reqwest",
@ -2955,7 +2956,7 @@ dependencies = [
"anyhow",
"async-trait",
"getrandom",
"http 1.1.0",
"http 1.2.0",
"matchit",
"opentelemetry",
"reqwest",
@ -3199,9 +3200,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.215"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
dependencies = [
"serde_derive",
]
@ -3217,9 +3218,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.215"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
dependencies = [
"proc-macro2",
"quote",
@ -4027,7 +4028,7 @@ dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http 1.1.0",
"http 1.2.0",
"httparse",
"log",
"rand 0.8.5",
@ -4235,9 +4236,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.95"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
dependencies = [
"cfg-if",
"once_cell",
@ -4246,13 +4247,12 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.95"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.87",
@ -4261,22 +4261,23 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.44"
version = "0.4.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65471f79c1022ffa5291d33520cbbb53b7687b01c2f8e83b57d102eed7ed479d"
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
dependencies = [
"cfg-if",
"futures-core",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.95"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -4284,9 +4285,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.95"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [
"proc-macro2",
"quote",
@ -4297,9 +4298,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.95"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
[[package]]
name = "wasm-lib"
@ -4361,9 +4362,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.72"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
dependencies = [
"js-sys",
"wasm-bindgen",

View File

@ -20,8 +20,8 @@ serde_json = "1.0.128"
tokio = { version = "1.41.1", features = ["sync"] }
toml = "0.8.19"
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.44"
wasm-bindgen = "0.2.99"
wasm-bindgen-futures = "0.4.49"
[dev-dependencies]
anyhow = "1"
@ -43,7 +43,7 @@ wasm-bindgen-futures = { version = "0.4.44", features = ["futures-core-03-stream
wasm-streams = "0.4.1"
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
version = "0.3.72"
version = "0.3.76"
features = [
"console",
"HtmlTextAreaElement",
@ -76,7 +76,7 @@ members = [
[workspace.dependencies]
http = "1"
kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.77", features = ["websocket"] }
kittycad-modeling-cmds = { version = "0.2.79", features = ["websocket"] }
[workspace.lints.clippy]
assertions_on_result_states = "warn"

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.32"
version = "0.1.33"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -6,6 +6,8 @@
mod tests;
mod unbox;
use std::collections::HashMap;
use convert_case::Casing;
use inflector::Inflector;
use once_cell::sync::Lazy;
@ -38,6 +40,12 @@ struct StdlibMetadata {
#[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, expects keyword arguments.
/// If false, expects positional arguments.
#[serde(default)]
@ -47,6 +55,10 @@ struct StdlibMetadata {
/// If false, all arguments require labels.
#[serde(default)]
unlabeled_first: bool,
/// Key = argument name, value = argument doc.
#[serde(default)]
arg_docs: HashMap<String, String>,
}
#[proc_macro_attribute]
@ -238,6 +250,12 @@ fn do_stdlib_inner(
quote! { false }
};
let feature_tree_operation = if metadata.feature_tree_operation {
quote! { true }
} else {
quote! { false }
};
let uses_keyword_arguments = if metadata.keywords {
quote! { true }
} else {
@ -282,6 +300,17 @@ fn do_stdlib_inner(
let ty_string = rust_type_to_openapi_type(&ty_string);
let required = !ty_ident.to_string().starts_with("Option <");
let description = if let Some(s) = metadata.arg_docs.get(&arg_name) {
quote! { #s }
} else if metadata.keywords && ty_string != "Args" && ty_string != "ExecState" {
errors.push(Error::new_spanned(
&arg,
"Argument was not documented in the arg_docs block",
));
continue;
} else {
quote! { String::new() }
};
let label_required = !(i == 0 && metadata.unlabeled_first);
if ty_string != "ExecState" && ty_string != "Args" {
let schema = quote! {
@ -294,6 +323,7 @@ fn do_stdlib_inner(
schema: #schema,
required: #required,
label_required: #label_required,
description: #description.to_string(),
}
});
}
@ -355,6 +385,7 @@ fn do_stdlib_inner(
schema,
required: true,
label_required: true,
description: String::new(),
})
}
} else {
@ -451,6 +482,10 @@ fn do_stdlib_inner(
#deprecated
}
fn feature_tree_operation(&self) -> bool {
#feature_tree_operation
}
fn examples(&self) -> Vec<String> {
#code_blocks
}
@ -744,6 +779,8 @@ fn rust_type_to_openapi_type(t: &str) -> String {
if t == "f64" {
return "number".to_string();
} else if t == "u32" {
return "integer".to_string();
} else if t == "str" {
return "string".to_string();
} else {
@ -778,7 +815,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default()).await.unwrap();
ctx.run(program.into(), &mut crate::ExecState::new()).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -116,6 +116,9 @@ fn test_stdlib_line_to() {
let (item, errors) = do_stdlib(
quote! {
name = "lineTo",
arg_docs = {
sketch = "the sketch you're adding the line to"
}
},
quote! {
/// This is some function.

View File

@ -14,7 +14,7 @@ mod test_examples_someFn {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -91,6 +91,7 @@ impl crate::docs::StdLibFn for SomeFn {
schema: generator.root_schema_for::<Foo>(),
required: true,
label_required: true,
description: String::new().to_string(),
}]
}
@ -105,6 +106,7 @@ impl crate::docs::StdLibFn for SomeFn {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -116,6 +118,10 @@ impl crate::docs::StdLibFn for SomeFn {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec!["someFn()"];
code_blocks

View File

@ -14,7 +14,7 @@ mod test_examples_someFn {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -91,6 +91,7 @@ impl crate::docs::StdLibFn for SomeFn {
schema: generator.root_schema_for::<str>(),
required: true,
label_required: true,
description: String::new().to_string(),
}]
}
@ -105,6 +106,7 @@ impl crate::docs::StdLibFn for SomeFn {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -116,6 +118,10 @@ impl crate::docs::StdLibFn for SomeFn {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec!["someFn()"];
code_blocks

View File

@ -15,7 +15,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -52,7 +52,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -129,6 +129,7 @@ impl crate::docs::StdLibFn for Show {
schema: generator.root_schema_for::<[f64; 2usize]>(),
required: true,
label_required: true,
description: String::new().to_string(),
}]
}
@ -143,6 +144,7 @@ impl crate::docs::StdLibFn for Show {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -154,6 +156,10 @@ impl crate::docs::StdLibFn for Show {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec![
"This is another code block.\nyes sirrr.\nshow",

View File

@ -15,7 +15,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -92,6 +92,7 @@ impl crate::docs::StdLibFn for Show {
schema: generator.root_schema_for::<f64>(),
required: true,
label_required: true,
description: String::new().to_string(),
}]
}
@ -106,6 +107,7 @@ impl crate::docs::StdLibFn for Show {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -117,6 +119,10 @@ impl crate::docs::StdLibFn for Show {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec!["This is code.\nIt does other shit.\nshow"];
code_blocks

View File

@ -16,7 +16,7 @@ mod test_examples_my_func {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -53,7 +53,7 @@ mod test_examples_my_func {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -130,6 +130,7 @@ impl crate::docs::StdLibFn for MyFunc {
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
required: false,
label_required: true,
description: String::new().to_string(),
}]
}
@ -144,6 +145,7 @@ impl crate::docs::StdLibFn for MyFunc {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -155,6 +157,10 @@ impl crate::docs::StdLibFn for MyFunc {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec![
"This is another code block.\nyes sirrr.\nmyFunc",

View File

@ -16,7 +16,7 @@ mod test_examples_line_to {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -53,7 +53,7 @@ mod test_examples_line_to {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -131,6 +131,7 @@ impl crate::docs::StdLibFn for LineTo {
schema: generator.root_schema_for::<LineToData>(),
required: true,
label_required: true,
description: String::new().to_string(),
},
crate::docs::StdLibFnArg {
name: "sketch".to_string(),
@ -138,6 +139,7 @@ impl crate::docs::StdLibFn for LineTo {
schema: generator.root_schema_for::<Sketch>(),
required: true,
label_required: true,
description: "the sketch you're adding the line to".to_string(),
},
]
}
@ -153,6 +155,7 @@ impl crate::docs::StdLibFn for LineTo {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -164,6 +167,10 @@ impl crate::docs::StdLibFn for LineTo {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec![
"This is another code block.\nyes sirrr.\nlineTo",

View File

@ -15,7 +15,7 @@ mod test_examples_min {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -52,7 +52,7 @@ mod test_examples_min {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -129,6 +129,7 @@ impl crate::docs::StdLibFn for Min {
schema: generator.root_schema_for::<Vec<f64>>(),
required: true,
label_required: true,
description: String::new().to_string(),
}]
}
@ -143,6 +144,7 @@ impl crate::docs::StdLibFn for Min {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -154,6 +156,10 @@ impl crate::docs::StdLibFn for Min {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec![
"This is another code block.\nyes sirrr.\nmin",

View File

@ -15,7 +15,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -92,6 +92,7 @@ impl crate::docs::StdLibFn for Show {
schema: generator.root_schema_for::<Option<f64>>(),
required: false,
label_required: true,
description: String::new().to_string(),
}]
}
@ -106,6 +107,7 @@ impl crate::docs::StdLibFn for Show {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -117,6 +119,10 @@ impl crate::docs::StdLibFn for Show {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec!["This is code.\nIt does other shit.\nshow"];
code_blocks

View File

@ -15,7 +15,7 @@ mod test_examples_import {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -92,6 +92,7 @@ impl crate::docs::StdLibFn for Import {
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
required: false,
label_required: true,
description: String::new().to_string(),
}]
}
@ -106,6 +107,7 @@ impl crate::docs::StdLibFn for Import {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -117,6 +119,10 @@ impl crate::docs::StdLibFn for Import {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec!["This is code.\nIt does other shit.\nimport"];
code_blocks

View File

@ -15,7 +15,7 @@ mod test_examples_import {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -92,6 +92,7 @@ impl crate::docs::StdLibFn for Import {
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
required: false,
label_required: true,
description: String::new().to_string(),
}]
}
@ -106,6 +107,7 @@ impl crate::docs::StdLibFn for Import {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -117,6 +119,10 @@ impl crate::docs::StdLibFn for Import {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec!["This is code.\nIt does other shit.\nimport"];
code_blocks

View File

@ -15,7 +15,7 @@ mod test_examples_import {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -92,6 +92,7 @@ impl crate::docs::StdLibFn for Import {
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
required: false,
label_required: true,
description: String::new().to_string(),
}]
}
@ -106,6 +107,7 @@ impl crate::docs::StdLibFn for Import {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -117,6 +119,10 @@ impl crate::docs::StdLibFn for Import {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec!["This is code.\nIt does other shit.\nimport"];
code_blocks

View File

@ -15,7 +15,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -92,6 +92,7 @@ impl crate::docs::StdLibFn for Show {
schema: generator.root_schema_for::<Vec<f64>>(),
required: true,
label_required: true,
description: String::new().to_string(),
}]
}
@ -106,6 +107,7 @@ impl crate::docs::StdLibFn for Show {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -117,6 +119,10 @@ impl crate::docs::StdLibFn for Show {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec!["This is code.\nIt does other shit.\nshow"];
code_blocks

View File

@ -14,7 +14,7 @@ mod test_examples_some_function {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
ctx.run(program.into(), &mut crate::ExecState::default())
ctx.run(program.into(), &mut crate::ExecState::new())
.await
.unwrap();
}
@ -99,6 +99,7 @@ impl crate::docs::StdLibFn for SomeFunction {
schema,
required: true,
label_required: true,
description: String::new(),
})
}
@ -110,6 +111,10 @@ impl crate::docs::StdLibFn for SomeFunction {
false
}
fn feature_tree_operation(&self) -> bool {
false
}
fn examples(&self) -> Vec<String> {
let code_blocks = vec!["someFunction()"];
code_blocks

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.18"
version = "0.1.19"
edition = "2021"
license = "MIT"

View File

@ -164,7 +164,7 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
};
eprintln!("Executing {test_name}");
let mut exec_state = ExecState::default();
let mut exec_state = ExecState::new();
// This is a shitty source range, I don't know what else to use for it though.
// There's no actual KCL associated with this reset_scene call.
if let Err(e) = state

View File

@ -189,7 +189,11 @@ impl EngineConnection {
uuid_to_cpp(path_id)
)
}
kcmc::ModelingCmd::Extrude(kcmc::Extrude { distance, target }) => {
kcmc::ModelingCmd::Extrude(kcmc::Extrude {
distance,
target,
faces: _, // Engine team: start using this once the frontend and engine both use it.
}) => {
format!(
r#"
scene->getSceneObject(Utils::UUID("{target}"))->extrudeToSolid3D({} * scaleFactor, true);

View File

@ -16,7 +16,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
let ctx = ExecutorContext::new_forwarded_mock(Arc::new(Box::new(
crate::conn_mock_core::EngineConnection::new(ref_result).await?,
)));
ctx.run(program.into(), &mut ExecState::default()).await?;
ctx.run(program.into(), &mut ExecState::new()).await?;
let result = result.lock().expect("mutex lock").clone();
Ok(result)

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.28"
version = "0.2.29"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -22,7 +22,7 @@ clap = { version = "4.5.21", default-features = false, optional = true, features
] }
convert_case = "0.6.0"
dashmap = "6.1.0"
derive-docs = { version = "0.1.32", path = "../derive-docs" }
derive-docs = { version = "0.1.33", path = "../derive-docs" }
dhat = { version = "0.3", optional = true }
fnv = "1.0.7"
form_urlencoded = "1.2.1"
@ -82,9 +82,9 @@ tokio = { version = "1.41.1", features = ["sync", "time"] }
tower-lsp = { version = "0.20.0", default-features = false, features = [
"runtime-agnostic",
] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.44"
web-sys = { version = "0.3.72", features = ["console"] }
wasm-bindgen = "0.2.99"
wasm-bindgen-futures = "0.4.49"
web-sys = { version = "0.3.76", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
approx = "0.5"

View File

@ -597,29 +597,13 @@ fn clean_function_name(name: &str) -> String {
fn_name = fn_name.replace("seg_", "segment_");
} else if fn_name.starts_with("log_") {
fn_name = fn_name.replace("log_", "log");
} else if fn_name.ends_with("tan_2") {
fn_name = fn_name.replace("tan_2", "tan2");
}
fn_name
}
/// Check if a schema is the same as another schema, but don't check the description.
fn is_same_schema(sa: &schemars::schema::Schema, sb: &schemars::schema::Schema) -> bool {
let schemars::schema::Schema::Object(a) = sa else {
return sa == sb;
};
let schemars::schema::Schema::Object(b) = sb else {
return sa == sb;
};
let mut a = a.clone();
a.metadata = None;
let mut b = b.clone();
b.metadata = None;
a == b
}
/// Recursively create references for types we already know about.
fn recurse_and_create_references(
name: &str,
@ -653,24 +637,6 @@ fn recurse_and_create_references(
return Ok(schemars::schema::Schema::Object(obj));
}
// Check if this is the type we already know about.
for (n, s) in types {
if is_same_schema(schema, s) && name != n && !n.starts_with("[") {
// Return a reference to the type.
let sref = schemars::schema::Schema::new_ref(n.to_string());
// Add the existing metadata to the reference.
let schemars::schema::Schema::Object(ro) = sref else {
return Err(anyhow::anyhow!(
"Failed to get object schema, should have not been a primitive"
));
};
let mut ro = ro.clone();
ro.metadata = o.metadata.clone();
return Ok(schemars::schema::Schema::Object(ro));
}
}
let mut obj = o.clone();
// If we have an object iterate over the properties and recursively create references.

View File

@ -59,6 +59,12 @@ pub struct StdLibFnArg {
pub schema: schemars::schema::RootSchema,
/// If the argument is required.
pub required: bool,
/// Additional information that could be used instead of the type's description.
/// This is helpful if the type is really basic, like "u32" -- that won't tell the user much about
/// how this argument is meant to be used.
/// Empty string means this has no docs.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
/// Even in functions that use keyword arguments, not every parameter requires a label (most do though).
/// Some functions allow one unlabeled parameter, which has to be first in the
/// argument list.
@ -106,6 +112,11 @@ impl StdLibFnArg {
}
pub fn description(&self) -> Option<String> {
// Check if we explicitly gave this stdlib arg a description.
if !self.description.is_empty() {
return Some(self.description.clone());
}
// If not, then try to get something meaningful from the schema.
get_description_string_from_schema(&self.schema.clone())
}
}
@ -154,6 +165,9 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
/// If the function is deprecated.
fn deprecated(&self) -> bool;
/// If the function should appear in the feature tree.
fn feature_tree_operation(&self) -> bool;
/// Any example code blocks.
fn examples(&self) -> Vec<String>;
@ -507,13 +521,13 @@ fn get_autocomplete_snippet_from_schema(
}
if prop_name == "color" {
fn_docs.push_str(&format!("\t{}: ${{{}:\"#ff0000\"}},\n", prop_name, i));
fn_docs.push_str(&format!("\t{} = ${{{}:\"#ff0000\"}},\n", prop_name, i));
i += 1;
continue;
}
if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(prop, i)? {
fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet));
fn_docs.push_str(&format!("\t{} = {},\n", prop_name, snippet));
i = new_index + 1;
}
}
@ -879,8 +893,8 @@ mod tests {
assert_eq!(
snippet,
r#"fillet({
radius: ${0:3.14},
tags: [${1:"tag_or_edge_fn"}],
radius = ${0:3.14},
tags = [${1:"tag_or_edge_fn"}],
}, ${2:%})${}"#
);
}
@ -900,11 +914,11 @@ mod tests {
assert_eq!(
snippet,
r#"patternCircular3d({
instances: ${0:10},
axis: [${1:3.14}, ${2:3.14}, ${3:3.14}],
center: [${4:3.14}, ${5:3.14}, ${6:3.14}],
arcDegrees: ${7:3.14},
rotateDuplicates: ${8:false},
instances = ${0:10},
axis = [${1:3.14}, ${2:3.14}, ${3:3.14}],
center = [${4:3.14}, ${5:3.14}, ${6:3.14}],
arcDegrees = ${7:3.14},
rotateDuplicates = ${8:false},
}, ${9:%})${}"#
);
}
@ -916,7 +930,7 @@ mod tests {
assert_eq!(
snippet,
r#"revolve({
axis: ${0:"X"},
axis = ${0:"X"},
}, ${1:%})${}"#
);
}
@ -928,8 +942,8 @@ mod tests {
assert_eq!(
snippet,
r#"circle({
center: [${0:3.14}, ${1:3.14}],
radius: ${2:3.14},
center = [${0:3.14}, ${1:3.14}],
radius = ${2:3.14},
}, ${3:%})${}"#
);
}
@ -941,9 +955,9 @@ mod tests {
assert_eq!(
snippet,
r#"arc({
angleStart: ${0:3.14},
angleEnd: ${1:3.14},
radius: ${2:3.14},
angleStart = ${0:3.14},
angleEnd = ${1:3.14},
radius = ${2:3.14},
}, ${3:%})${}"#
);
}
@ -962,9 +976,9 @@ mod tests {
assert_eq!(
snippet,
r#"patternLinear2d({
instances: ${0:10},
distance: ${1:3.14},
axis: [${2:3.14}, ${3:3.14}],
instances = ${0:10},
distance = ${1:3.14},
axis = [${2:3.14}, ${3:3.14}],
}, ${4:%})${}"#
);
}
@ -976,7 +990,7 @@ mod tests {
assert_eq!(
snippet,
r#"appearance({
color: ${0:"#
color = ${0:"#
.to_owned()
+ "\"#"
+ r#"ff0000"},
@ -998,7 +1012,7 @@ mod tests {
assert_eq!(
snippet,
r#"sweep({
path: ${0:sketch000},
path = ${0:sketch000},
}, ${1:%})${}"#
);
}

View File

@ -3,6 +3,7 @@ use thiserror::Error;
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
use crate::{
execution::Operation,
lsp::IntoDiagnostic,
source_range::{ModuleId, SourceRange},
};
@ -18,6 +19,48 @@ pub enum ExecError {
BadPng(String),
}
/// How did the KCL execution fail, with extra state.
#[cfg_attr(target_arch = "wasm32", expect(dead_code))]
#[derive(Debug)]
pub struct ExecErrorWithState {
pub error: ExecError,
pub exec_state: crate::ExecState,
}
impl ExecErrorWithState {
#[cfg_attr(target_arch = "wasm32", expect(dead_code))]
pub fn new(error: ExecError, exec_state: crate::ExecState) -> Self {
Self { error, exec_state }
}
}
impl From<ExecError> for ExecErrorWithState {
fn from(error: ExecError) -> Self {
Self {
error,
exec_state: Default::default(),
}
}
}
impl From<KclError> for ExecErrorWithState {
fn from(error: KclError) -> Self {
Self {
error: error.into(),
exec_state: Default::default(),
}
}
}
impl From<ConnectionError> for ExecErrorWithState {
fn from(error: ConnectionError) -> Self {
Self {
error: error.into(),
exec_state: Default::default(),
}
}
}
/// How did KCL client fail to connect to the engine
#[derive(thiserror::Error, Debug)]
pub enum ConnectionError {
@ -57,6 +100,21 @@ pub enum KclError {
Internal(KclErrorDetails),
}
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
#[error("{error}")]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct KclErrorWithOutputs {
pub error: KclError,
pub operations: Vec<Operation>,
}
impl KclErrorWithOutputs {
pub fn new(error: KclError, operations: Vec<Operation>) -> Self {
Self { error, operations }
}
}
#[derive(thiserror::Error, Debug)]
#[error("{}", self.error.get_message())]
pub struct Report {
@ -295,7 +353,6 @@ pub struct CompilationError {
}
impl CompilationError {
#[allow(dead_code)]
pub(crate) fn err(source_range: SourceRange, message: impl ToString) -> CompilationError {
CompilationError {
source_range,

View File

@ -0,0 +1,73 @@
//! Data on available annotations.
use super::kcl_value::{UnitAngle, UnitLen};
use crate::{
errors::KclErrorDetails,
parsing::ast::types::{Expr, Node, NonCodeValue, ObjectProperty},
KclError, SourceRange,
};
pub(super) const SETTINGS: &str = "settings";
pub(super) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
pub(super) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
pub(super) fn expect_properties<'a>(
for_key: &'static str,
annotation: &'a NonCodeValue,
source_range: SourceRange,
) -> Result<&'a [Node<ObjectProperty>], KclError> {
match annotation {
NonCodeValue::Annotation { name, properties } => {
assert_eq!(name.name, for_key);
Ok(&**properties.as_ref().ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Empty `{for_key}` annotation"),
source_ranges: vec![source_range],
})
})?)
}
_ => unreachable!(),
}
}
pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
match expr {
Expr::Identifier(id) => Ok(&id.name),
e => Err(KclError::Semantic(KclErrorDetails {
message: "Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
source_ranges: vec![e.into()],
})),
}
}
impl UnitLen {
pub(super) fn from_str(s: &str, source_range: SourceRange) -> Result<Self, KclError> {
match s {
"mm" => Ok(UnitLen::Mm),
"cm" => Ok(UnitLen::Cm),
"m" => Ok(UnitLen::M),
"inch" | "in" => Ok(UnitLen::Inches),
"ft" => Ok(UnitLen::Feet),
"yd" => Ok(UnitLen::Yards),
value => Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Unexpected settings value: `{value}`; expected one of `mm`, `cm`, `m`, `inch`, `ft`, `yd`"
),
source_ranges: vec![source_range],
})),
}
}
}
impl UnitAngle {
pub(super) fn from_str(s: &str, source_range: SourceRange) -> Result<Self, KclError> {
match s {
"deg" => Ok(UnitAngle::Degrees),
"rad" => Ok(UnitAngle::Radians),
value => Err(KclError::Semantic(KclErrorDetails {
message: format!("Unexpected settings value: `{value}`; expected one of `deg`, `rad`"),
source_ranges: vec![source_range],
})),
}
}
}

View File

@ -0,0 +1,134 @@
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{docs::StdLibFn, std::get_stdlib_fn, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations
/// timeline.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum Operation {
#[serde(rename_all = "camelCase")]
StdLibCall {
/// The standard library function being called.
#[serde(flatten)]
std_lib_fn: StdLibFnRef,
/// The unlabeled argument to the function.
unlabeled_arg: Option<OpArg>,
/// The labeled keyword arguments to the function.
labeled_args: IndexMap<String, OpArg>,
/// The source range of the operation in the source code.
source_range: SourceRange,
/// True if the operation resulted in an error.
#[serde(default, skip_serializing_if = "is_false")]
is_error: bool,
},
#[serde(rename_all = "camelCase")]
UserDefinedFunctionCall {
/// The name of the user-defined function being called. Anonymous
/// functions have no name.
name: Option<String>,
/// The location of the function being called so that there's enough
/// info to go to its definition.
function_source_range: SourceRange,
/// The unlabeled argument to the function.
unlabeled_arg: Option<OpArg>,
/// The labeled keyword arguments to the function.
labeled_args: IndexMap<String, OpArg>,
/// The source range of the operation in the source code.
source_range: SourceRange,
},
UserDefinedFunctionReturn,
}
impl Operation {
/// If the variant is `StdLibCall`, set the `is_error` field.
pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) {
match self {
Self::StdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::UserDefinedFunctionCall { .. } | Self::UserDefinedFunctionReturn => {}
}
}
}
/// An argument to a CAD modeling operation.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct OpArg {
/// The KCL code expression for the argument. This is used in the UI so
/// that the user can edit the expression.
source_range: SourceRange,
}
impl OpArg {
pub(crate) fn new(source_range: SourceRange) -> Self {
Self { source_range }
}
}
/// A reference to a standard library function. This exists to implement
/// `PartialEq` and `Eq` for `Operation`.
#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct StdLibFnRef {
// The following doc comment gets inlined into Operation, overriding what's
// there, in the generated TS. We serialize to its name. Renaming the
// field to "name" allows it to match the other variant.
/// The standard library function being called.
#[serde(
rename = "name",
serialize_with = "std_lib_fn_name",
deserialize_with = "std_lib_fn_from_name"
)]
#[ts(type = "string", rename = "name")]
pub std_lib_fn: Box<dyn StdLibFn>,
}
impl StdLibFnRef {
pub(crate) fn new(std_lib_fn: Box<dyn StdLibFn>) -> Self {
Self { std_lib_fn }
}
}
impl From<&Box<dyn StdLibFn>> for StdLibFnRef {
fn from(std_lib_fn: &Box<dyn StdLibFn>) -> Self {
Self::new(std_lib_fn.clone())
}
}
impl PartialEq for StdLibFnRef {
fn eq(&self, other: &Self) -> bool {
self.std_lib_fn.name() == other.std_lib_fn.name()
}
}
impl Eq for StdLibFnRef {}
#[expect(clippy::borrowed_box, reason = "Explicit Box is needed for serde")]
fn std_lib_fn_name<S>(std_lib_fn: &Box<dyn StdLibFn>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let name = std_lib_fn.name();
serializer.serialize_str(&name)
}
fn std_lib_fn_from_name<'de, D>(deserializer: D) -> Result<Box<dyn StdLibFn>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if let Some(std_lib_fn) = get_stdlib_fn(&s) {
Ok(std_lib_fn)
} else {
Err(serde::de::Error::custom(format!("not a KCL stdlib function: {}", s)))
}
}
fn is_false(b: &bool) -> bool {
!*b
}

View File

@ -19,6 +19,8 @@ use crate::{
},
};
use super::cad_op::{OpArg, Operation};
const FLOAT_TO_INT_MAX_DELTA: f64 = 0.01;
impl BinaryPart {
@ -27,7 +29,7 @@ impl BinaryPart {
match self {
BinaryPart::Literal(literal) => Ok(literal.into()),
BinaryPart::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
let value = exec_state.memory().get(&identifier.name, identifier.into())?;
Ok(value.clone())
}
BinaryPart::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, ctx).await,
@ -45,7 +47,7 @@ impl Node<MemberExpression> {
let array = match &self.object {
MemberObject::MemberExpression(member_expr) => member_expr.get_result(exec_state)?,
MemberObject::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
let value = exec_state.memory().get(&identifier.name, identifier.into())?;
value.clone()
}
};
@ -73,7 +75,7 @@ impl Node<MemberExpression> {
// TODO: Don't use recursion here, use a loop.
MemberObject::MemberExpression(member_expr) => member_expr.get_result(exec_state)?,
MemberObject::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
let value = exec_state.memory().get(&identifier.name, identifier.into())?;
value.clone()
}
};
@ -168,6 +170,42 @@ impl Node<BinaryExpression> {
}
}
// Check if we are doing logical operations on booleans.
if self.operator == BinaryOperator::Or || self.operator == BinaryOperator::And {
let KclValue::Bool {
value: left_value,
meta: _,
} = left_value
else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Cannot apply logical operator to non-boolean value: {}",
left_value.human_friendly_type()
),
source_ranges: vec![self.left.clone().into()],
}));
};
let KclValue::Bool {
value: right_value,
meta: _,
} = right_value
else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Cannot apply logical operator to non-boolean value: {}",
right_value.human_friendly_type()
),
source_ranges: vec![self.right.clone().into()],
}));
};
let raw_value = match self.operator {
BinaryOperator::Or => left_value || right_value,
BinaryOperator::And => left_value && right_value,
_ => unreachable!(),
};
return Ok(KclValue::Bool { value: raw_value, meta });
}
let left = parse_number_as_f64(&left_value, self.left.clone().into())?;
let right = parse_number_as_f64(&right_value, self.right.clone().into())?;
@ -220,6 +258,7 @@ impl Node<BinaryExpression> {
value: left == right,
meta,
},
BinaryOperator::And | BinaryOperator::Or => unreachable!(),
};
Ok(value)
@ -308,11 +347,11 @@ pub(crate) async fn execute_pipe_body(
// Now that we've evaluated the first child expression in the pipeline, following child expressions
// 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.
let previous_pipe_value = std::mem::replace(&mut exec_state.pipe_value, Some(output));
let previous_pipe_value = std::mem::replace(&mut exec_state.mod_local.pipe_value, Some(output));
// Evaluate remaining elements.
let result = inner_execute_pipe_body(exec_state, body, ctx).await;
// Restore the previous pipe value.
exec_state.pipe_value = previous_pipe_value;
exec_state.mod_local.pipe_value = previous_pipe_value;
result
}
@ -338,10 +377,10 @@ async fn inner_execute_pipe_body(
let output = ctx
.execute_expr(expression, exec_state, &metadata, StatementKind::Expression)
.await?;
exec_state.pipe_value = Some(output);
exec_state.mod_local.pipe_value = Some(output);
}
// Safe to unwrap here, because pipe_value always has something pushed in when the `match first` executes.
let final_output = exec_state.pipe_value.take().unwrap();
let final_output = exec_state.mod_local.pipe_value.take().unwrap();
Ok(final_output)
}
@ -353,7 +392,6 @@ impl Node<CallExpressionKw> {
// Build a hashmap from argument labels to the final evaluated values.
let mut fn_args = HashMap::with_capacity(self.arguments.len());
let mut tag_declarator_args = Vec::new();
for arg_expr in &self.arguments {
let source_range = SourceRange::from(arg_expr.arg.clone());
let metadata = Metadata { source_range };
@ -361,12 +399,8 @@ impl Node<CallExpressionKw> {
.execute_expr(&arg_expr.arg, exec_state, &metadata, StatementKind::Expression)
.await?;
fn_args.insert(arg_expr.label.name.clone(), Arg::new(value, source_range));
if let Expr::TagDeclarator(td) = &arg_expr.arg {
tag_declarator_args.push((td.inner.clone(), source_range));
}
}
let fn_args = fn_args; // remove mutability
let tag_declarator_args = tag_declarator_args; // remove mutability
// Evaluate the unlabeled first param, if any exists.
let unlabeled = if let Some(ref arg_expr) = self.unlabeled {
@ -387,23 +421,78 @@ impl Node<CallExpressionKw> {
},
self.into(),
ctx.clone(),
exec_state.mod_local.pipe_value.clone().map(Arg::synthetic),
);
match ctx.stdlib.get_either(fn_name) {
FunctionKind::Core(func) => {
let op = if func.feature_tree_operation() {
let op_labeled_args = args
.kw_args
.labeled
.iter()
.map(|(k, v)| (k.clone(), OpArg::new(v.source_range)))
.collect();
Some(Operation::StdLibCall {
std_lib_fn: (&func).into(),
unlabeled_arg: args.kw_args.unlabeled.as_ref().map(|arg| OpArg::new(arg.source_range)),
labeled_args: op_labeled_args,
source_range: callsite,
is_error: false,
})
} else {
None
};
// Attempt to call the function.
let mut result = func.std_lib_fn()(exec_state, args).await?;
update_memory_for_tags_of_geometry(&mut result, &tag_declarator_args, exec_state)?;
Ok(result)
let result = {
// Don't early-return in this block.
let result = func.std_lib_fn()(exec_state, args).await;
if let Some(mut op) = op {
op.set_std_lib_call_is_error(result.is_err());
// Track call operation. We do this after the call
// since things like patternTransform may call user code
// before running, and we will likely want to use the
// return value. The call takes ownership of the args,
// so we need to build the op before the call.
exec_state.mod_local.operations.push(op);
}
result
};
let mut return_value = result?;
update_memory_for_tags_of_geometry(&mut return_value, exec_state)?;
Ok(return_value)
}
FunctionKind::UserDefined => {
let source_range = SourceRange::from(self);
// Clone the function so that we can use a mutable reference to
// exec_state.
let func = exec_state.memory.get(fn_name, source_range)?.clone();
let fn_dynamic_state = exec_state.dynamic_state.merge(&exec_state.memory);
let func = exec_state.memory().get(fn_name, source_range)?.clone();
let fn_dynamic_state = exec_state.mod_local.dynamic_state.merge(exec_state.memory());
// Track call operation.
let op_labeled_args = args
.kw_args
.labeled
.iter()
.map(|(k, v)| (k.clone(), OpArg::new(v.source_range)))
.collect();
exec_state
.mod_local
.operations
.push(Operation::UserDefinedFunctionCall {
name: Some(fn_name.clone()),
function_source_range: func.function_def_source_range().unwrap_or_default(),
unlabeled_arg: args.kw_args.unlabeled.as_ref().map(|arg| OpArg::new(arg.source_range)),
labeled_args: op_labeled_args,
source_range: callsite,
});
let return_value = {
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
let previous_dynamic_state =
std::mem::replace(&mut exec_state.mod_local.dynamic_state, fn_dynamic_state);
let result = func
.call_fn_kw(args, exec_state, ctx.clone(), callsite)
.await
@ -412,7 +501,7 @@ impl Node<CallExpressionKw> {
// TODO currently ignored by the frontend
e.add_source_ranges(vec![source_range])
});
exec_state.dynamic_state = previous_dynamic_state;
exec_state.mod_local.dynamic_state = previous_dynamic_state;
result?
};
@ -428,6 +517,12 @@ impl Node<CallExpressionKw> {
})
})?;
// Track return operation.
exec_state
.mod_local
.operations
.push(Operation::UserDefinedFunctionReturn);
Ok(result)
}
}
@ -438,9 +533,9 @@ impl Node<CallExpression> {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let fn_name = &self.callee.name;
let callsite = SourceRange::from(self);
let mut fn_args: Vec<Arg> = Vec::with_capacity(self.arguments.len());
let mut tag_declarator_args = Vec::new();
for arg_expr in &self.arguments {
let metadata = Metadata {
@ -450,36 +545,87 @@ impl Node<CallExpression> {
.execute_expr(arg_expr, exec_state, &metadata, StatementKind::Expression)
.await?;
let arg = Arg::new(value, SourceRange::from(arg_expr));
if let Expr::TagDeclarator(td) = arg_expr {
tag_declarator_args.push((td.inner.clone(), arg.source_range));
}
fn_args.push(arg);
}
let tag_declarator_args = tag_declarator_args; // remove mutability
let fn_args = fn_args; // remove mutability
match ctx.stdlib.get_either(fn_name) {
FunctionKind::Core(func) => {
let op = if func.feature_tree_operation() {
let op_labeled_args = func
.args(false)
.iter()
.zip(&fn_args)
.map(|(k, v)| (k.name.clone(), OpArg::new(v.source_range)))
.collect();
Some(Operation::StdLibCall {
std_lib_fn: (&func).into(),
unlabeled_arg: None,
labeled_args: op_labeled_args,
source_range: callsite,
is_error: false,
})
} else {
None
};
// Attempt to call the function.
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
let mut result = func.std_lib_fn()(exec_state, args).await?;
update_memory_for_tags_of_geometry(&mut result, &tag_declarator_args, exec_state)?;
Ok(result)
let args = crate::std::Args::new(
fn_args,
self.into(),
ctx.clone(),
exec_state.mod_local.pipe_value.clone().map(Arg::synthetic),
);
let result = {
// Don't early-return in this block.
let result = func.std_lib_fn()(exec_state, args).await;
if let Some(mut op) = op {
op.set_std_lib_call_is_error(result.is_err());
// Track call operation. We do this after the call
// since things like patternTransform may call user code
// before running, and we will likely want to use the
// return value. The call takes ownership of the args,
// so we need to build the op before the call.
exec_state.mod_local.operations.push(op);
}
result
};
let mut return_value = result?;
update_memory_for_tags_of_geometry(&mut return_value, exec_state)?;
Ok(return_value)
}
FunctionKind::UserDefined => {
let source_range = SourceRange::from(self);
// Clone the function so that we can use a mutable reference to
// exec_state.
let func = exec_state.memory.get(fn_name, source_range)?.clone();
let fn_dynamic_state = exec_state.dynamic_state.merge(&exec_state.memory);
let func = exec_state.memory().get(fn_name, source_range)?.clone();
let fn_dynamic_state = exec_state.mod_local.dynamic_state.merge(exec_state.memory());
// Track call operation.
exec_state
.mod_local
.operations
.push(Operation::UserDefinedFunctionCall {
name: Some(fn_name.clone()),
function_source_range: func.function_def_source_range().unwrap_or_default(),
unlabeled_arg: None,
// TODO: Add the arguments for legacy positional parameters.
labeled_args: Default::default(),
source_range: callsite,
});
let return_value = {
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
let previous_dynamic_state =
std::mem::replace(&mut exec_state.mod_local.dynamic_state, fn_dynamic_state);
let result = func.call_fn(fn_args, exec_state, ctx.clone()).await.map_err(|e| {
// Add the call expression to the source ranges.
// TODO currently ignored by the frontend
e.add_source_ranges(vec![source_range])
});
exec_state.dynamic_state = previous_dynamic_state;
exec_state.mod_local.dynamic_state = previous_dynamic_state;
result?
};
@ -495,30 +641,19 @@ impl Node<CallExpression> {
})
})?;
// Track return operation.
exec_state
.mod_local
.operations
.push(Operation::UserDefinedFunctionReturn);
Ok(result)
}
}
}
}
/// `tag_declarator_args` should only contain tag declarator literals, which
/// will be defined as local variables. Non-literals that evaluate to tag
/// declarators should not be defined.
fn update_memory_for_tags_of_geometry(
result: &mut KclValue,
tag_declarator_args: &[(TagDeclarator, SourceRange)],
exec_state: &mut ExecState,
) -> Result<(), KclError> {
// Define all the tags in the memory.
for (tag_declarator, arg_sr) in tag_declarator_args {
let tag = TagIdentifier {
value: tag_declarator.name.clone(),
info: None,
meta: vec![Metadata { source_range: *arg_sr }],
};
exec_state.memory.add_tag(&tag.value, tag.clone(), *arg_sr)?;
}
fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut ExecState) -> Result<(), KclError> {
// If the return result is a sketch or solid, we want to update the
// memory for the tags of the group.
// TODO: This could probably be done in a better way, but as of now this was my only idea
@ -526,7 +661,7 @@ fn update_memory_for_tags_of_geometry(
match result {
KclValue::Sketch { value: ref mut sketch } => {
for (_, tag) in sketch.tags.iter() {
exec_state.memory.update_tag_if_defined(&tag.value, tag.clone());
exec_state.mut_memory().update_tag(&tag.value, tag.clone())?;
}
}
KclValue::Solid(ref mut solid) => {
@ -564,7 +699,7 @@ fn update_memory_for_tags_of_geometry(
info.sketch = solid.id;
t.info = Some(info);
exec_state.memory.update_tag_if_defined(&tag.name, t.clone());
exec_state.mut_memory().update_tag(&tag.name, t.clone())?;
// update the sketch tags.
solid.sketch.tags.insert(tag.name.clone(), t);
@ -572,11 +707,8 @@ fn update_memory_for_tags_of_geometry(
}
// Find the stale sketch in memory and update it.
if let Some(current_env) = exec_state
.memory
.environments
.get_mut(exec_state.memory.current_env.index())
{
let cur_env_index = exec_state.memory().current_env.index();
if let Some(current_env) = exec_state.mut_memory().environments.get_mut(cur_env_index) {
current_env.update_sketch_tags(&solid.sketch);
}
}
@ -585,6 +717,24 @@ fn update_memory_for_tags_of_geometry(
Ok(())
}
impl Node<TagDeclarator> {
pub async fn execute(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier {
value: self.name.clone(),
info: None,
meta: vec![Metadata {
source_range: self.into(),
}],
}));
exec_state
.mut_memory()
.add(&self.name, memory_item.clone(), self.into())?;
Ok(self.into())
}
}
impl Node<ArrayExpression> {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
@ -774,7 +924,7 @@ impl Property {
Ok(Property::String(name.to_string()))
} else {
// Actually evaluate memory to compute the property.
let prop = exec_state.memory.get(name, property_src)?;
let prop = exec_state.memory().get(name, property_src)?;
jvalue_to_prop(prop, property_sr, name)
}
}

View File

@ -8,9 +8,12 @@ use crate::{
errors::KclErrorDetails,
exec::{ProgramMemory, Sketch},
execution::{Face, ImportedGeometry, MemoryFunction, Metadata, Plane, SketchSet, Solid, SolidSet, TagIdentifier},
parsing::ast::types::{FunctionExpression, KclNone, LiteralValue, TagDeclarator, TagNode},
parsing::{
ast::types::{FunctionExpression, KclNone, LiteralValue, TagDeclarator, TagNode},
token::NumericSuffix,
},
std::{args::Arg, FnAsArg},
ExecState, ExecutorContext, KclError, SourceRange,
ExecState, ExecutorContext, KclError, ModuleId, SourceRange,
};
pub type KclObjectFields = HashMap<String, KclValue>;
@ -84,6 +87,11 @@ pub enum KclValue {
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
Module {
value: ModuleId,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
KclNone {
value: KclNone,
#[serde(rename = "__meta")]
@ -143,6 +151,7 @@ impl From<KclValue> for Vec<SourceRange> {
KclValue::String { meta, .. } => to_vec_sr(&meta),
KclValue::Array { meta, .. } => to_vec_sr(&meta),
KclValue::Object { meta, .. } => to_vec_sr(&meta),
KclValue::Module { meta, .. } => to_vec_sr(&meta),
KclValue::Uuid { meta, .. } => to_vec_sr(&meta),
KclValue::KclNone { meta, .. } => to_vec_sr(&meta),
}
@ -173,6 +182,7 @@ impl From<&KclValue> for Vec<SourceRange> {
KclValue::Uuid { meta, .. } => to_vec_sr(meta),
KclValue::Array { meta, .. } => to_vec_sr(meta),
KclValue::Object { meta, .. } => to_vec_sr(meta),
KclValue::Module { meta, .. } => to_vec_sr(meta),
KclValue::KclNone { meta, .. } => to_vec_sr(meta),
}
}
@ -198,10 +208,20 @@ impl KclValue {
KclValue::Solids { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::ImportedGeometry(x) => x.meta.clone(),
KclValue::Function { meta, .. } => meta.clone(),
KclValue::Module { meta, .. } => meta.clone(),
KclValue::KclNone { meta, .. } => meta.clone(),
}
}
pub(crate) fn function_def_source_range(&self) -> Option<SourceRange> {
let KclValue::Function { expression, .. } = self else {
return None;
};
// TODO: It would be nice if we could extract the source range starting
// at the fn, but that's the variable declaration.
Some(expression.as_source_range())
}
pub(crate) fn get_solid_set(&self) -> Result<SolidSet> {
match self {
KclValue::Solid(e) => Ok(SolidSet::Solid(e.clone())),
@ -254,6 +274,7 @@ impl KclValue {
KclValue::String { .. } => "string (text)",
KclValue::Array { .. } => "array (list)",
KclValue::Object { .. } => "object",
KclValue::Module { .. } => "module",
KclValue::KclNone { .. } => "None",
}
}
@ -543,3 +564,52 @@ impl KclValue {
}
}
}
// TODO called UnitLen so as not to clash with UnitLength in settings)
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)]
#[serde(tag = "type")]
pub enum UnitLen {
Mm,
Cm,
M,
Inches,
Feet,
Yards,
}
impl TryFrom<NumericSuffix> for UnitLen {
type Error = ();
fn try_from(suffix: NumericSuffix) -> std::result::Result<Self, Self::Error> {
match suffix {
NumericSuffix::Mm => Ok(Self::Mm),
NumericSuffix::Cm => Ok(Self::Cm),
NumericSuffix::M => Ok(Self::M),
NumericSuffix::Inch => Ok(Self::Inches),
NumericSuffix::Ft => Ok(Self::Feet),
NumericSuffix::Yd => Ok(Self::Yards),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)]
#[serde(tag = "type")]
pub enum UnitAngle {
Degrees,
Radians,
}
impl TryFrom<NumericSuffix> for UnitAngle {
type Error = ();
fn try_from(suffix: NumericSuffix) -> std::result::Result<Self, Self::Error> {
match suffix {
NumericSuffix::Deg => Ok(Self::Degrees),
NumericSuffix::Rad => Ok(Self::Radians),
_ => Err(()),
}
}
}

View File

@ -22,8 +22,11 @@ type Point3D = kcmc::shared::Point3d<f64>;
pub use function_param::FunctionParam;
pub use kcl_value::{KclObjectFields, KclValue};
use uuid::Uuid;
mod annotations;
pub(crate) mod cache;
mod cad_op;
mod exec_ast;
mod function_param;
mod kcl_value;
@ -34,7 +37,8 @@ use crate::{
execution::cache::{CacheInformation, CacheResult},
fs::{FileManager, FileSystem},
parsing::ast::types::{
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode,
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, NonCodeValue,
Program as AstProgram, TagDeclarator, TagNode,
},
settings::types::UnitLength,
source_range::{ModuleId, SourceRange},
@ -42,15 +46,36 @@ use crate::{
ExecError, Program,
};
// Re-exports.
pub use cad_op::Operation;
/// State for executing a program.
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExecState {
/// Program variable bindings.
pub memory: ProgramMemory,
pub global: GlobalState,
pub mod_local: ModuleState,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct GlobalState {
/// The stable artifact ID generator.
pub id_generator: IdGenerator,
/// Map from source file absolute path to module ID.
pub path_to_source_id: IndexMap<std::path::PathBuf, ModuleId>,
/// Map from module ID to module info.
pub module_infos: IndexMap<ModuleId, ModuleInfo>,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ModuleState {
/// Program variable bindings.
pub memory: ProgramMemory,
/// Dynamic state that follows dynamic flow of the program.
pub dynamic_state: DynamicState,
/// The current value of the pipe operator returned from the previous
@ -61,26 +86,156 @@ pub struct ExecState {
/// The stack of import statements for detecting circular module imports.
/// If this is empty, we're not currently executing an import statement.
pub import_stack: Vec<std::path::PathBuf>,
/// Map from source file absolute path to module ID.
pub path_to_source_id: IndexMap<std::path::PathBuf, ModuleId>,
/// Map from module ID to module info.
pub module_infos: IndexMap<ModuleId, ModuleInfo>,
/// Operations that have been performed in execution order, for display in
/// the Feature Tree.
pub operations: Vec<Operation>,
/// Settings specified from annotations.
pub settings: MetaSettings,
}
impl Default for ExecState {
fn default() -> Self {
Self::new()
}
}
impl ExecState {
fn add_module(&mut self, path: std::path::PathBuf) -> ModuleId {
pub fn new() -> Self {
ExecState {
global: GlobalState::new(),
mod_local: ModuleState::default(),
}
}
fn reset(&mut self) {
let mut id_generator = self.global.id_generator.clone();
// We do not pop the ids, since we want to keep the same id generator.
// This is for the front end to keep track of the ids.
id_generator.next_id = 0;
let mut global = GlobalState::new();
global.id_generator = id_generator;
*self = ExecState {
global,
mod_local: ModuleState::default(),
};
}
pub fn memory(&self) -> &ProgramMemory {
&self.mod_local.memory
}
pub fn mut_memory(&mut self) -> &mut ProgramMemory {
&mut self.mod_local.memory
}
pub fn next_uuid(&mut self) -> Uuid {
self.global.id_generator.next_uuid()
}
async fn add_module(
&mut self,
path: std::path::PathBuf,
ctxt: &ExecutorContext,
source_range: SourceRange,
) -> Result<ModuleId, KclError> {
// Need to avoid borrowing self in the closure.
let new_module_id = ModuleId::from_usize(self.path_to_source_id.len());
let new_module_id = ModuleId::from_usize(self.global.path_to_source_id.len());
let mut is_new = false;
let id = *self.path_to_source_id.entry(path.clone()).or_insert_with(|| {
let id = *self.global.path_to_source_id.entry(path.clone()).or_insert_with(|| {
is_new = true;
new_module_id
});
if is_new {
let module_info = ModuleInfo { id, path };
self.module_infos.insert(id, module_info);
let source = ctxt.fs.read_to_string(&path, source_range).await?;
// TODO handle parsing errors properly
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?;
let module_info = ModuleInfo {
id,
path,
parsed: Some(parsed),
};
self.global.module_infos.insert(id, module_info);
}
id
Ok(id)
}
}
impl GlobalState {
fn new() -> Self {
let mut global = GlobalState {
id_generator: Default::default(),
path_to_source_id: Default::default(),
module_infos: Default::default(),
};
// TODO(#4434): Use the top-level file's path.
let root_path = PathBuf::new();
let root_id = ModuleId::default();
global.module_infos.insert(
root_id,
ModuleInfo {
id: root_id,
path: root_path.clone(),
parsed: None,
},
);
global.path_to_source_id.insert(root_path, root_id);
global
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct MetaSettings {
pub default_length_units: kcl_value::UnitLen,
pub default_angle_units: kcl_value::UnitAngle,
}
impl Default for MetaSettings {
fn default() -> Self {
MetaSettings {
default_length_units: kcl_value::UnitLen::Mm,
default_angle_units: kcl_value::UnitAngle::Degrees,
}
}
}
impl MetaSettings {
fn update_from_annotation(&mut self, annotation: &NonCodeValue, source_range: SourceRange) -> Result<(), KclError> {
let properties = annotations::expect_properties(annotations::SETTINGS, annotation, source_range)?;
for p in properties {
match &*p.inner.key.name {
annotations::SETTINGS_UNIT_LENGTH => {
let value = annotations::expect_ident(&p.inner.value)?;
let value = kcl_value::UnitLen::from_str(value, source_range)?;
self.default_length_units = value;
}
annotations::SETTINGS_UNIT_ANGLE => {
let value = annotations::expect_ident(&p.inner.value)?;
let value = kcl_value::UnitAngle::from_str(value, source_range)?;
self.default_angle_units = value;
}
name => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Unexpected settings key: `{name}`; expected one of `{}`, `{}`",
annotations::SETTINGS_UNIT_LENGTH,
annotations::SETTINGS_UNIT_ANGLE
),
source_ranges: vec![source_range],
}))
}
}
}
Ok(())
}
}
@ -124,16 +279,10 @@ impl ProgramMemory {
Ok(())
}
pub fn add_tag(&mut self, tag: &str, value: TagIdentifier, source_range: SourceRange) -> Result<(), KclError> {
self.add(tag, KclValue::TagIdentifier(Box::new(value)), source_range)
}
pub fn update_tag_if_defined(&mut self, tag: &str, value: TagIdentifier) {
if !self.environments[self.current_env.index()].contains_key(tag) {
// Do nothing if the tag isn't defined.
return;
}
pub fn update_tag(&mut self, tag: &str, value: TagIdentifier) -> Result<(), KclError> {
self.environments[self.current_env.index()].insert(tag.to_string(), KclValue::TagIdentifier(Box::new(value)));
Ok(())
}
/// Get a value from the program memory.
@ -158,6 +307,13 @@ impl ProgramMemory {
}))
}
/// Returns all bindings in the current scope.
#[allow(dead_code)]
fn get_all_cur_scope(&self) -> IndexMap<String, KclValue> {
let env = &self.environments[self.current_env.index()];
env.bindings.clone()
}
/// Find all solids in the memory that are on a specific sketch id.
/// This does not look inside closures. But as long as we do not allow
/// mutation of variables in KCL, closure memory should be a subset of this.
@ -273,18 +429,14 @@ pub struct DynamicState {
}
impl DynamicState {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn merge(&self, memory: &ProgramMemory) -> Self {
fn merge(&self, memory: &ProgramMemory) -> Self {
let mut merged = self.clone();
merged.append(memory);
merged
}
pub fn append(&mut self, memory: &ProgramMemory) {
fn append(&mut self, memory: &ProgramMemory) {
for env in &memory.environments {
for item in env.bindings.values() {
if let KclValue::Solid(eg) = item {
@ -294,7 +446,7 @@ impl DynamicState {
}
}
pub fn edge_cut_ids_on_sketch(&self, sketch_id: uuid::Uuid) -> Vec<uuid::Uuid> {
pub(crate) fn edge_cut_ids_on_sketch(&self, sketch_id: uuid::Uuid) -> Vec<uuid::Uuid> {
self.solid_ids
.iter()
.flat_map(|eg| {
@ -552,7 +704,7 @@ pub struct Plane {
impl Plane {
pub(crate) fn from_plane_data(value: crate::std::sketch::PlaneData, exec_state: &mut ExecState) -> Self {
let id = exec_state.id_generator.next_uuid();
let id = exec_state.global.id_generator.next_uuid();
match value {
crate::std::sketch::PlaneData::XY => Plane {
id,
@ -850,7 +1002,7 @@ impl GetTangentialInfoFromPathsResult {
impl Sketch {
pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path) {
let mut tag_identifier = TagIdentifier::from(tag);
let mut tag_identifier: TagIdentifier = tag.into();
let base = current_path.get_base();
tag_identifier.info = Some(TagEngineInfo {
id: base.geo_meta.id,
@ -1000,13 +1152,14 @@ pub enum BodyType {
/// Info about a module. Right now, this is pretty minimal. We hope to cache
/// modules here in the future.
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct ModuleInfo {
/// The ID of the module.
id: ModuleId,
/// Absolute path of the module's source file.
path: std::path::PathBuf,
parsed: Option<Node<AstProgram>>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)]
@ -1795,7 +1948,7 @@ impl ExecutorContext {
source_range: crate::execution::SourceRange,
) -> Result<(), KclError> {
self.engine
.clear_scene(&mut exec_state.id_generator, source_range)
.clear_scene(&mut exec_state.global.id_generator, source_range)
.await?;
// We do not create the planes here as the post hook in wasm will do that
@ -1895,16 +2048,14 @@ impl ExecutorContext {
};
if cache_result.clear_scene && !self.is_mock() {
// Pop the execution state, since we are starting fresh.
exec_state.reset();
// We don't do this in mock mode since there is no engine connection
// anyways and from the TS side we override memory and don't want to clear it.
self.reset_scene(exec_state, Default::default()).await?;
// Pop the execution state, since we are starting fresh.
*exec_state = Default::default();
}
// TODO: Use the top-level file's path.
exec_state.add_module(std::path::PathBuf::from(""));
// Re-apply the settings, in case the cache was busted.
self.engine.reapply_settings(&self.settings, Default::default()).await?;
@ -1922,17 +2073,35 @@ impl ExecutorContext {
exec_state: &mut ExecState,
body_type: BodyType,
) -> Result<Option<KclValue>, KclError> {
if let Some((annotation, source_range)) = program
.non_code_meta
.start_nodes
.iter()
.filter_map(|n| {
n.annotation(annotations::SETTINGS)
.map(|result| (result, n.as_source_range()))
})
.next()
{
exec_state
.mod_local
.settings
.update_from_annotation(annotation, source_range)?;
}
let mut last_expr = None;
// Iterate over the body of the program.
for statement in &program.body {
match statement {
BodyItem::ImportStatement(import_stmt) => {
let source_range = SourceRange::from(import_stmt);
let (module_memory, module_exports) =
self.open_module(&import_stmt.path, exec_state, source_range).await?;
let module_id = self.open_module(&import_stmt.path, exec_state, source_range).await?;
match &import_stmt.selector {
ImportSelector::List { items } => {
let (_, module_memory, module_exports) = self
.exec_module(module_id, exec_state, ExecutionKind::Isolated, source_range)
.await?;
for import_item in items {
// Extract the item from the module.
let item =
@ -1956,18 +2125,24 @@ impl ExecutorContext {
}
// Add the item to the current module.
exec_state.memory.add(
exec_state.mut_memory().add(
import_item.identifier(),
item.clone(),
SourceRange::from(&import_item.name),
)?;
if let ItemVisibility::Export = import_stmt.visibility {
exec_state.module_exports.push(import_item.identifier().to_owned());
exec_state
.mod_local
.module_exports
.push(import_item.identifier().to_owned());
}
}
}
ImportSelector::Glob(_) => {
let (_, module_memory, module_exports) = self
.exec_module(module_id, exec_state, ExecutionKind::Isolated, source_range)
.await?;
for name in module_exports.iter() {
let item = module_memory.get(name, source_range).map_err(|_err| {
KclError::Internal(KclErrorDetails {
@ -1975,18 +2150,20 @@ impl ExecutorContext {
source_ranges: vec![source_range],
})
})?;
exec_state.memory.add(name, item.clone(), source_range)?;
exec_state.mut_memory().add(name, item.clone(), source_range)?;
if let ItemVisibility::Export = import_stmt.visibility {
exec_state.module_exports.push(name.clone());
exec_state.mod_local.module_exports.push(name.clone());
}
}
}
ImportSelector::None(_) => {
return Err(KclError::Semantic(KclErrorDetails {
message: "Importing whole module is not yet implemented, sorry.".to_owned(),
source_ranges: vec![source_range],
}));
ImportSelector::None { .. } => {
let name = import_stmt.module_name().unwrap();
let item = KclValue::Module {
value: module_id,
meta: vec![source_range.into()],
};
exec_state.mut_memory().add(&name, item, source_range)?;
}
}
last_expr = None;
@ -2016,11 +2193,11 @@ impl ExecutorContext {
StatementKind::Declaration { name: &var_name },
)
.await?;
exec_state.memory.add(&var_name, memory_item, source_range)?;
exec_state.mut_memory().add(&var_name, memory_item, source_range)?;
// Track exports.
if let ItemVisibility::Export = variable_declaration.visibility {
exec_state.module_exports.push(var_name);
exec_state.mod_local.module_exports.push(var_name);
}
last_expr = None;
}
@ -2034,7 +2211,7 @@ impl ExecutorContext {
StatementKind::Expression,
)
.await?;
exec_state.memory.return_ = Some(value);
exec_state.mut_memory().return_ = Some(value);
last_expr = None;
}
}
@ -2060,18 +2237,19 @@ impl ExecutorContext {
path: &str,
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<(ProgramMemory, Vec<String>), KclError> {
) -> Result<ModuleId, KclError> {
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
project_dir.join(path)
} else {
std::path::PathBuf::from(&path)
};
if exec_state.import_stack.contains(&resolved_path) {
if exec_state.mod_local.import_stack.contains(&resolved_path) {
return Err(KclError::ImportCycle(KclErrorDetails {
message: format!(
"circular import of modules is not allowed: {} -> {}",
exec_state
.mod_local
.import_stack
.iter()
.map(|p| p.as_path().to_string_lossy())
@ -2082,31 +2260,44 @@ impl ExecutorContext {
source_ranges: vec![source_range],
}));
}
let module_id = exec_state.add_module(resolved_path.clone());
let source = self.fs.read_to_string(&resolved_path, source_range).await?;
// TODO handle parsing errors properly
let program = crate::parsing::parse_str(&source, module_id).parse_errs_as_err()?;
exec_state.add_module(resolved_path.clone(), self, source_range).await
}
exec_state.import_stack.push(resolved_path.clone());
let original_execution = self.engine.replace_execution_kind(ExecutionKind::Isolated);
let original_memory = std::mem::take(&mut exec_state.memory);
let original_exports = std::mem::take(&mut exec_state.module_exports);
async fn exec_module(
&self,
module_id: ModuleId,
exec_state: &mut ExecState,
exec_kind: ExecutionKind,
source_range: SourceRange,
) -> Result<(Option<KclValue>, ProgramMemory, Vec<String>), KclError> {
// TODO It sucks that we have to clone the whole module AST here
let info = exec_state.global.module_infos[&module_id].clone();
let mut local_state = ModuleState {
import_stack: exec_state.mod_local.import_stack.clone(),
..Default::default()
};
local_state.import_stack.push(info.path.clone());
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
let original_execution = self.engine.replace_execution_kind(exec_kind);
// The unwrap here is safe since we only elide the AST for the top module.
let result = self
.inner_execute(&program, exec_state, crate::execution::BodyType::Root)
.inner_execute(&info.parsed.unwrap(), exec_state, crate::execution::BodyType::Root)
.await;
let module_exports = std::mem::replace(&mut exec_state.module_exports, original_exports);
let module_memory = std::mem::replace(&mut exec_state.memory, original_memory);
self.engine.replace_execution_kind(original_execution);
exec_state.import_stack.pop();
result.map_err(|err| {
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
self.engine.replace_execution_kind(original_execution);
let result = result.map_err(|err| {
if let KclError::ImportCycle(_) = err {
// It was an import cycle. Keep the original message.
err.override_source_ranges(vec![source_range])
} else {
KclError::Semantic(KclErrorDetails {
message: format!(
"Error loading imported file. Open it to view more details. {path}: {}",
"Error loading imported file. Open it to view more details. {}: {}",
info.path.display(),
err.message()
),
source_ranges: vec![source_range],
@ -2114,7 +2305,7 @@ impl ExecutorContext {
}
})?;
Ok((module_memory, module_exports))
Ok((result, local_state.memory, local_state.module_exports))
}
#[async_recursion]
@ -2128,10 +2319,25 @@ impl ExecutorContext {
let item = match init {
Expr::None(none) => KclValue::from(none),
Expr::Literal(literal) => KclValue::from(literal),
Expr::TagDeclarator(tag) => KclValue::from(tag),
Expr::TagDeclarator(tag) => tag.execute(exec_state).await?,
Expr::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
value.clone()
let value = exec_state.memory().get(&identifier.name, identifier.into())?.clone();
if let KclValue::Module { value: module_id, meta } = value {
let (result, _, _) = self
.exec_module(module_id, exec_state, ExecutionKind::Normal, metadata.source_range)
.await?;
result.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!(
"Evaluating module `{}` as part of an assembly did not produce a result",
identifier.name
),
source_ranges: vec![metadata.source_range, meta[0].source_range],
})
})?
} else {
value
}
}
Expr::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, self).await?,
Expr::FunctionExpression(function_expression) => {
@ -2142,7 +2348,7 @@ impl ExecutorContext {
expression: function_expression.clone(),
meta: vec![metadata.to_owned()],
func: None,
memory: Box::new(exec_state.memory.clone()),
memory: Box::new(exec_state.memory().clone()),
}
}
Expr::CallExpression(call_expression) => call_expression.execute(exec_state, self).await?,
@ -2159,7 +2365,7 @@ impl ExecutorContext {
source_ranges: vec![pipe_substitution.into()],
}));
}
StatementKind::Expression => match exec_state.pipe_value.clone() {
StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() {
Some(x) => x,
None => {
return Err(KclError::Semantic(KclErrorDetails {
@ -2179,7 +2385,9 @@ impl ExecutorContext {
let result = self
.execute_expr(&expr.expr, exec_state, metadata, statement_kind)
.await?;
exec_state.memory.add(&expr.label.name, result.clone(), init.into())?;
exec_state
.mut_memory()
.add(&expr.label.name, result.clone(), init.into())?;
// TODO this lets us use the label as a variable name, but not as a tag in most cases
result
}
@ -2364,12 +2572,12 @@ pub(crate) async fn call_user_defined_function(
// Execute the function body using the memory we just created.
let (result, fn_memory) = {
let previous_memory = std::mem::replace(&mut exec_state.memory, fn_memory);
let previous_memory = std::mem::replace(&mut exec_state.mod_local.memory, fn_memory);
let result = ctx
.inner_execute(&function_expression.body, exec_state, BodyType::Block)
.await;
// Restore the previous memory.
let fn_memory = std::mem::replace(&mut exec_state.memory, previous_memory);
let fn_memory = std::mem::replace(&mut exec_state.mod_local.memory, previous_memory);
(result, fn_memory)
};
@ -2394,12 +2602,12 @@ pub(crate) async fn call_user_defined_function_kw(
// Execute the function body using the memory we just created.
let (result, fn_memory) = {
let previous_memory = std::mem::replace(&mut exec_state.memory, fn_memory);
let previous_memory = std::mem::replace(&mut exec_state.mod_local.memory, fn_memory);
let result = ctx
.inner_execute(&function_expression.body, exec_state, BodyType::Block)
.await;
// Restore the previous memory.
let fn_memory = std::mem::replace(&mut exec_state.memory, previous_memory);
let fn_memory = std::mem::replace(&mut exec_state.mod_local.memory, previous_memory);
(result, fn_memory)
};
@ -2424,7 +2632,7 @@ mod tests {
OldAstState,
};
pub async fn parse_execute(code: &str) -> Result<(Program, ExecutorContext, ExecState)> {
async fn parse_execute(code: &str) -> Result<(Program, ExecutorContext, ExecState)> {
let program = Program::parse_no_errs(code)?;
let ctx = ExecutorContext {
@ -2441,6 +2649,7 @@ mod tests {
}
/// Convenience function to get a JSON value from memory and unwrap.
#[track_caller]
fn mem_get_json(memory: &ProgramMemory, name: &str) -> KclValue {
memory.get(name, SourceRange::default()).unwrap().to_owned()
}
@ -2808,6 +3017,28 @@ const answer = returnX()"#;
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_cannot_shebang_in_fn() {
let ast = r#"
fn foo () {
#!hello
return true
}
foo
"#;
let result = parse_execute(ast).await;
let err = result.unwrap_err().downcast::<KclError>().unwrap();
assert_eq!(
err,
KclError::Syntax(KclErrorDetails {
message: "Unexpected token: #".to_owned(),
source_ranges: vec![SourceRange::new(15, 16, ModuleId::default())],
}),
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_pattern_transform_function_cannot_access_future_definitions() {
let ast = r#"
@ -2849,21 +3080,21 @@ let shape = layer() |> patternTransform(10, transform, %)
async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(5.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
assert_eq!(5.0, mem_get_json(exec_state.memory(), "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute() {
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(7.4, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
assert_eq!(7.4, mem_get_json(exec_state.memory(), "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_start_negative() {
let ast = r#"const myVar = -5 + 6"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(1.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
assert_eq!(1.0, mem_get_json(exec_state.memory(), "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
@ -2872,7 +3103,7 @@ let shape = layer() |> patternTransform(10, transform, %)
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(
std::f64::consts::TAU,
mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap()
mem_get_json(exec_state.memory(), "myVar").as_f64().unwrap()
);
}
@ -2880,7 +3111,7 @@ let shape = layer() |> patternTransform(10, transform, %)
async fn test_math_define_decimal_without_leading_zero() {
let ast = r#"let thing = .4 + 7"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(7.4, mem_get_json(&exec_state.memory, "thing").as_f64().unwrap());
assert_eq!(7.4, mem_get_json(exec_state.memory(), "thing").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
@ -2920,10 +3151,10 @@ fn check = (x) => {
check(false)
"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(false, mem_get_json(&exec_state.memory, "notTrue").as_bool().unwrap());
assert_eq!(true, mem_get_json(&exec_state.memory, "notFalse").as_bool().unwrap());
assert_eq!(true, mem_get_json(&exec_state.memory, "c").as_bool().unwrap());
assert_eq!(false, mem_get_json(&exec_state.memory, "d").as_bool().unwrap());
assert_eq!(false, mem_get_json(exec_state.memory(), "notTrue").as_bool().unwrap());
assert_eq!(true, mem_get_json(exec_state.memory(), "notFalse").as_bool().unwrap());
assert_eq!(true, mem_get_json(exec_state.memory(), "c").as_bool().unwrap());
assert_eq!(false, mem_get_json(exec_state.memory(), "d").as_bool().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
@ -3024,10 +3255,8 @@ let notTagDeclarator = !myTagDeclarator";
);
let code9 = "
sk = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([5, 0], %, $myTag)
notTagIdentifier = !myTag";
let myTagDeclarator = $myTag
let notTagIdentifier = !myTag";
let tag_identifier_err = parse_execute(code9).await.unwrap_err().downcast::<KclError>().unwrap();
// These are currently printed out as JSON objects, so we don't want to
// check the full error.
@ -3497,12 +3726,7 @@ shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
})
.await;
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program_new.ast);
assert!(result.clear_scene);
assert!(result.is_none());
}
// Changing the units with the exact same file should bust the cache.
@ -3611,4 +3835,65 @@ shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_ids_stable_between_executions() {
let code = r#"sketch001 = startSketchOn('XZ')
|> startProfileAt([61.74, 206.13], %)
|> xLine(305.11, %, $seg01)
|> yLine(-291.85, %)
|> xLine(-segLen(seg01), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> extrude(40.14, %)
|> shell({
faces: [seg01],
thickness: 3.14,
}, %)
"#;
let ctx = crate::test_server::new_context(UnitLength::Mm, true, None)
.await
.unwrap();
let old_program = crate::Program::parse_no_errs(code).unwrap();
// Execute the program.
let mut exec_state = Default::default();
let cache_info = crate::CacheInformation {
old: None,
new_ast: old_program.ast.clone(),
};
ctx.run(cache_info, &mut exec_state).await.unwrap();
// Get the id_generator from the first execution.
let id_generator = exec_state.global.id_generator.clone();
let code = r#"sketch001 = startSketchOn('XZ')
|> startProfileAt([62.74, 206.13], %)
|> xLine(305.11, %, $seg01)
|> yLine(-291.85, %)
|> xLine(-segLen(seg01), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> extrude(40.14, %)
|> shell({
faces: [seg01],
thickness: 3.14,
}, %)
"#;
// Execute a slightly different program again.
let program: Program = crate::Program::parse_no_errs(code).unwrap();
let cache_info = crate::CacheInformation {
old: Some(crate::OldAstState {
ast: old_program.ast.clone(),
exec_state: exec_state.clone(),
settings: ctx.settings.clone(),
}),
new_ast: program.ast.clone(),
};
// Execute the program.
ctx.run(cache_info, &mut exec_state).await.unwrap();
assert_eq!(id_generator, exec_state.global.id_generator);
}
}

View File

@ -81,7 +81,7 @@ mod wasm;
pub use coredump::CoreDump;
pub use engine::{EngineManager, ExecutionKind};
pub use errors::{CompilationError, ConnectionError, ExecError, KclError};
pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs};
pub use execution::{
cache::{CacheInformation, OldAstState},
ExecState, ExecutorContext, ExecutorSettings,

View File

@ -726,11 +726,11 @@ impl Backend {
drop(last_successful_ast_state);
self.memory_map
.insert(params.uri.to_string(), exec_state.memory.clone());
.insert(params.uri.to_string(), exec_state.memory().clone());
// Send the notification to the client that the memory was updated.
self.client
.send_notification::<custom_notifications::MemoryUpdated>(exec_state.memory)
.send_notification::<custom_notifications::MemoryUpdated>(exec_state.mod_local.memory)
.await;
Ok(())
@ -1216,7 +1216,7 @@ impl LanguageServer for Backend {
return Ok(None);
}
// Get the completion items forem the ast.
// Get the completion items for the ast.
let Ok(variables) = ast.completion_items() else {
return Ok(Some(CompletionResponse::Array(completions)));
};

View File

@ -822,6 +822,59 @@ async fn test_kcl_lsp_completions_const_raw() {
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_completions_import() {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: r#"import boo, baz as bux from 'bar.kcl'
//import 'bar.kcl'
x = b"#
.to_string(),
},
})
.await;
// Send completion request.
let completions = server
.completion(tower_lsp::lsp_types::CompletionParams {
text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
position: tower_lsp::lsp_types::Position { line: 2, character: 5 },
},
context: None,
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
})
.await
.unwrap()
.unwrap();
// Check the completions.
if let tower_lsp::lsp_types::CompletionResponse::Array(completions) = completions {
assert!(completions.len() > 10);
// Find the one with label "foo".
completions.iter().find(|completion| completion.label == "boo").unwrap();
// completions
// .iter()
// .find(|completion| completion.label == "bar")
// .unwrap();
completions.iter().find(|completion| completion.label == "bux").unwrap();
assert!(!completions.iter().any(|completion| completion.label == "baz"));
// Find the one with label "bar".
} else {
panic!("Expected array of completions");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_on_hover() {
let server = kcl_lsp_server(false).await.unwrap();
@ -2344,6 +2397,7 @@ async fn kcl_test_kcl_lsp_diagnostics_on_execution_error() {
.await;
// Get the diagnostics.
// TODO warnings being stomped by execution errors?
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
// Update the text.

View File

@ -3,11 +3,10 @@ use sha2::{Digest as DigestTrait, Sha256};
use super::types::{DefaultParamVal, ItemVisibility, LabelledExpression, VariableKind};
use crate::parsing::ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CallExpressionKw,
CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgType, FunctionExpression, Identifier, IfExpression,
ImportItem, ImportSelector, ImportStatement, KclNone, Literal, LiteralIdentifier, MemberExpression, MemberObject,
NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter, PipeExpression,
PipeSubstitution, Program, ReturnStatement, TagDeclarator, UnaryExpression, VariableDeclaration,
VariableDeclarator,
ElseIf, Expr, ExpressionStatement, FnArgType, FunctionExpression, Identifier, IfExpression, ImportItem,
ImportSelector, ImportStatement, KclNone, Literal, LiteralIdentifier, MemberExpression, MemberObject,
ObjectExpression, ObjectProperty, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement,
TagDeclarator, UnaryExpression, VariableDeclaration, VariableDeclarator,
};
/// Position-independent digest of the AST node.
@ -60,8 +59,8 @@ impl ImportStatement {
}
}
ImportSelector::Glob(_) => hasher.update(b"ImportSelector::Glob"),
ImportSelector::None(None) => hasher.update(b"ImportSelector::None"),
ImportSelector::None(Some(alias)) => {
ImportSelector::None { alias: None } => hasher.update(b"ImportSelector::None"),
ImportSelector::None { alias: Some(alias) } => {
hasher.update(b"ImportSelector::None");
hasher.update(alias.compute_digest());
}
@ -82,7 +81,6 @@ impl Program {
if let Some(shebang) = &slf.shebang {
hasher.update(&shebang.inner.content);
}
hasher.update(slf.non_code_meta.compute_digest());
});
}
@ -234,53 +232,6 @@ impl ReturnStatement {
});
}
impl CommentStyle {
fn digestable_id(&self) -> [u8; 2] {
match &self {
CommentStyle::Line => *b"//",
CommentStyle::Block => *b"/*",
}
}
}
impl NonCodeNode {
compute_digest!(|slf, hasher| {
match &slf.value {
NonCodeValue::InlineComment { value, style } => {
hasher.update(value);
hasher.update(style.digestable_id());
}
NonCodeValue::BlockComment { value, style } => {
hasher.update(value);
hasher.update(style.digestable_id());
}
NonCodeValue::NewLineBlockComment { value, style } => {
hasher.update(value);
hasher.update(style.digestable_id());
}
NonCodeValue::NewLine => {
hasher.update(b"\r\n");
}
}
});
}
impl NonCodeMeta {
compute_digest!(|slf, hasher| {
let mut keys = slf.non_code_nodes.keys().copied().collect::<Vec<_>>();
keys.sort();
for key in keys.into_iter() {
hasher.update(key.to_ne_bytes());
let nodes = slf.non_code_nodes.get_mut(&key).unwrap();
hasher.update(nodes.len().to_ne_bytes());
for node in nodes.iter_mut() {
hasher.update(node.compute_digest());
}
}
});
}
impl ExpressionStatement {
compute_digest!(|slf, hasher| {
hasher.update(slf.expression.compute_digest());
@ -416,7 +367,6 @@ impl PipeExpression {
for value in slf.body.iter_mut() {
hasher.update(value.compute_digest());
}
hasher.update(slf.non_code_meta.compute_digest());
});
}

View File

@ -1,9 +1,11 @@
//! Data types for the AST.
use std::{
cell::RefCell,
collections::HashMap,
fmt,
ops::{Deref, DerefMut, RangeInclusive},
rc::Rc,
sync::{Arc, Mutex},
};
@ -183,21 +185,24 @@ pub struct Program {
impl Node<Program> {
/// Walk the ast and get all the variables and tags as completion items.
pub fn completion_items<'a>(&'a self) -> Result<Vec<CompletionItem>> {
let completions = Arc::new(Mutex::new(vec![]));
let completions = Rc::new(RefCell::new(vec![]));
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
let mut findings = completions.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
let mut findings = completions.borrow_mut();
match node {
crate::walk::Node::TagDeclarator(tag) => {
findings.push(tag.into());
}
crate::walk::Node::VariableDeclaration(variable) => {
findings.extend::<Vec<CompletionItem>>(variable.into());
findings.extend::<Vec<CompletionItem>>((&variable.inner).into());
}
crate::walk::Node::ImportStatement(i) => {
findings.extend::<Vec<CompletionItem>>((&i.inner).into());
}
_ => {}
}
Ok::<bool, anyhow::Error>(true)
})?;
let x = completions.lock().unwrap();
let x = completions.take();
Ok(x.clone())
}
@ -995,52 +1000,22 @@ pub struct NonCodeNode {
pub digest: Option<Digest>,
}
impl Node<NonCodeNode> {
pub fn format(&self, indentation: &str) -> String {
match &self.value {
NonCodeValue::InlineComment {
value,
style: CommentStyle::Line,
} => format!(" // {}\n", value),
NonCodeValue::InlineComment {
value,
style: CommentStyle::Block,
} => format!(" /* {} */", value),
NonCodeValue::BlockComment { value, style } => match style {
CommentStyle::Block => format!("{}/* {} */", indentation, value),
CommentStyle::Line => {
if value.trim().is_empty() {
format!("{}//\n", indentation)
} else {
format!("{}// {}\n", indentation, value.trim())
}
}
},
NonCodeValue::NewLineBlockComment { value, style } => {
let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
match style {
CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
CommentStyle::Line => {
if value.trim().is_empty() {
format!("{}{}//\n", add_start_new_line, indentation)
} else {
format!("{}{}// {}\n", add_start_new_line, indentation, value.trim())
}
}
}
}
NonCodeValue::NewLine => "\n\n".to_string(),
}
}
}
impl NonCodeNode {
#[cfg(test)]
pub fn value(&self) -> String {
match &self.value {
NonCodeValue::InlineComment { value, style: _ } => value.clone(),
NonCodeValue::BlockComment { value, style: _ } => value.clone(),
NonCodeValue::NewLineBlockComment { value, style: _ } => value.clone(),
NonCodeValue::NewLine => "\n\n".to_string(),
NonCodeValue::Annotation { name, .. } => name.name.clone(),
}
}
pub fn annotation(&self, expected_name: &str) -> Option<&NonCodeValue> {
match &self.value {
a @ NonCodeValue::Annotation { name, .. } if name.name == expected_name => Some(a),
_ => None,
}
}
}
@ -1058,6 +1033,7 @@ pub enum CommentStyle {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[allow(clippy::large_enum_variant)]
pub enum NonCodeValue {
/// An inline comment.
/// Here are examples:
@ -1090,6 +1066,10 @@ pub enum NonCodeValue {
// A new line like `\n\n` NOT a new line like `\n`.
// This is also not a comment.
NewLine,
Annotation {
name: Node<Identifier>,
properties: Option<Vec<Node<ObjectProperty>>>,
},
}
#[derive(Debug, Default, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -1225,7 +1205,7 @@ pub enum ImportSelector {
Glob(Node<()>),
/// Import the module itself (the param is an optional alias).
/// E.g., `import "foo.kcl" as bar`
None(Option<Node<Identifier>>),
None { alias: Option<Node<Identifier>> },
}
impl ImportSelector {
@ -1244,8 +1224,8 @@ impl ImportSelector {
None
}
ImportSelector::Glob(_) => None,
ImportSelector::None(None) => None,
ImportSelector::None(Some(alias)) => {
ImportSelector::None { alias: None } => None,
ImportSelector::None { alias: Some(alias) } => {
let alias_source_range = SourceRange::from(&*alias);
if !alias_source_range.contains(pos) {
return None;
@ -1264,8 +1244,8 @@ impl ImportSelector {
}
}
ImportSelector::Glob(_) => {}
ImportSelector::None(None) => {}
ImportSelector::None(Some(alias)) => alias.rename(old_name, new_name),
ImportSelector::None { alias: None } => {}
ImportSelector::None { alias: Some(alias) } => alias.rename(old_name, new_name),
}
}
}
@ -1296,30 +1276,10 @@ impl Node<ImportStatement> {
false
}
ImportSelector::Glob(_) => false,
ImportSelector::None(_) => name == self.module_name().unwrap(),
ImportSelector::None { .. } => name == self.module_name().unwrap(),
}
}
/// Get the name of the module object for this import.
/// Validated during parsing and guaranteed to return `Some` if the statement imports
/// the module itself (i.e., self.selector is ImportSelector::None).
pub fn module_name(&self) -> Option<String> {
if let ImportSelector::None(Some(alias)) = &self.selector {
return Some(alias.name.clone());
}
let mut parts = self.path.split('.');
let name = parts.next()?;
let ext = parts.next()?;
let rest = parts.next();
if rest.is_some() || ext != "kcl" {
return None;
}
Some(name.to_owned())
}
pub fn get_constraint_level(&self) -> ConstraintLevel {
ConstraintLevel::Full {
source_ranges: vec![self.into()],
@ -1335,6 +1295,59 @@ impl ImportStatement {
pub fn rename_identifiers(&mut self, old_name: &str, new_name: &str) {
self.selector.rename_identifiers(old_name, new_name);
}
/// Get the name of the module object for this import.
/// Validated during parsing and guaranteed to return `Some` if the statement imports
/// the module itself (i.e., self.selector is ImportSelector::None).
pub fn module_name(&self) -> Option<String> {
if let ImportSelector::None { alias: Some(alias) } = &self.selector {
return Some(alias.name.clone());
}
let mut parts = self.path.split('.');
let name = parts.next()?;
let ext = parts.next()?;
let rest = parts.next();
if rest.is_some() || ext != "kcl" {
return None;
}
Some(name.to_owned())
}
}
impl From<&ImportStatement> for Vec<CompletionItem> {
fn from(import: &ImportStatement) -> Self {
match &import.selector {
ImportSelector::List { items } => {
items
.iter()
.map(|i| {
let as_str = match &i.alias {
Some(s) => format!(" as {}", s.name),
None => String::new(),
};
CompletionItem {
label: i.identifier().to_owned(),
// TODO we can only find this after opening the module
kind: None,
detail: Some(format!("{}{as_str} from '{}'", i.name.name, import.path)),
..CompletionItem::default()
}
})
.collect()
}
// TODO can't do completion for glob imports without static name resolution
ImportSelector::Glob(_) => vec![],
ImportSelector::None { .. } => vec![CompletionItem {
label: import.module_name().unwrap(),
kind: Some(CompletionItemKind::MODULE),
detail: Some(format!("from '{}'", import.path)),
..CompletionItem::default()
}],
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -1605,30 +1618,16 @@ pub struct VariableDeclaration {
pub digest: Option<Digest>,
}
impl From<&Node<VariableDeclaration>> for Vec<CompletionItem> {
fn from(declaration: &Node<VariableDeclaration>) -> Self {
impl From<&VariableDeclaration> for Vec<CompletionItem> {
fn from(declaration: &VariableDeclaration) -> Self {
vec![CompletionItem {
label: declaration.declaration.id.name.to_string(),
label_details: None,
kind: Some(match declaration.inner.kind {
kind: Some(match declaration.kind {
VariableKind::Const => CompletionItemKind::CONSTANT,
VariableKind::Fn => CompletionItemKind::FUNCTION,
}),
detail: Some(declaration.inner.kind.to_string()),
documentation: None,
deprecated: None,
preselect: None,
sort_text: None,
filter_text: None,
insert_text: None,
insert_text_format: None,
insert_text_mode: None,
text_edit: None,
additional_text_edits: None,
command: None,
commit_characters: None,
data: None,
tags: None,
detail: Some(declaration.kind.to_string()),
..CompletionItem::default()
}]
}
}
@ -1928,6 +1927,10 @@ impl Identifier {
})
}
pub fn is_nameable(&self) -> bool {
!self.name.starts_with('_')
}
/// Rename all identifiers that have the old name to the new given name.
fn rename(&mut self, old_name: &str, new_name: &str) {
if self.name == old_name {
@ -2568,6 +2571,14 @@ pub enum BinaryOperator {
#[serde(rename = "<=")]
#[display("<=")]
Lte,
/// Are both left and right true?
#[serde(rename = "&")]
#[display("&")]
And,
/// Is either left or right true?
#[serde(rename = "|")]
#[display("|")]
Or,
}
/// Mathematical associativity.
@ -2602,6 +2613,8 @@ impl BinaryOperator {
BinaryOperator::Gte => *b"gte",
BinaryOperator::Lt => *b"ltr",
BinaryOperator::Lte => *b"lte",
BinaryOperator::And => *b"and",
BinaryOperator::Or => *b"lor",
}
}
@ -2614,6 +2627,8 @@ impl BinaryOperator {
BinaryOperator::Pow => 13,
Self::Gt | Self::Gte | Self::Lt | Self::Lte => 9,
Self::Eq | Self::Neq => 8,
Self::And => 7,
Self::Or => 6,
}
}
@ -2624,6 +2639,7 @@ impl BinaryOperator {
Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod => Associativity::Left,
Self::Pow => Associativity::Right,
Self::Gt | Self::Gte | Self::Lt | Self::Lte | Self::Eq | Self::Neq => Associativity::Left, // I don't know if this is correct
Self::And | Self::Or => Associativity::Left,
}
}
}

View File

@ -33,7 +33,7 @@ use crate::{
SourceRange,
};
use super::ast::types::LabelledExpression;
use super::{ast::types::LabelledExpression, token::NumericSuffix};
thread_local! {
/// The current `ParseContext`. `None` if parsing is not currently happening on this thread.
@ -96,10 +96,6 @@ impl ParseContext {
*e = err;
return;
}
if e.source_range.start() > err.source_range.end() {
break;
}
}
errors.push(err);
});
@ -287,38 +283,86 @@ fn non_code_node(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
alt((non_code_node_leading_whitespace, non_code_node_no_leading_whitespace)).parse_next(i)
}
fn annotation(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
let at = at_sign.parse_next(i)?;
let name = binding_name.parse_next(i)?;
let mut end = name.end;
let properties = if peek(open_paren).parse_next(i).is_ok() {
open_paren(i)?;
ignore_whitespace(i);
let properties: Vec<_> = separated(
0..,
separated_pair(
terminated(identifier, opt(whitespace)),
terminated(one_of((TokenType::Operator, "=")), opt(whitespace)),
expression,
)
.map(|(key, value)| Node {
start: key.start,
end: value.end(),
module_id: key.module_id,
inner: ObjectProperty {
key,
value,
digest: None,
},
}),
comma_sep,
)
.parse_next(i)?;
ignore_trailing_comma(i);
ignore_whitespace(i);
end = close_paren(i)?.end;
Some(properties)
} else {
None
};
let value = NonCodeValue::Annotation { name, properties };
Ok(Node::new(
NonCodeNode { value, digest: None },
at.start,
end,
at.module_id,
))
}
// Matches remaining three cases of NonCodeValue
fn non_code_node_no_leading_whitespace(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
any.verify_map(|token: Token| {
if token.is_code_token() {
None
} else {
let value = match token.token_type {
TokenType::Whitespace if token.value.contains("\n\n") => NonCodeValue::NewLine,
TokenType::LineComment => NonCodeValue::BlockComment {
value: token.value.trim_start_matches("//").trim().to_owned(),
style: CommentStyle::Line,
},
TokenType::BlockComment => NonCodeValue::BlockComment {
style: CommentStyle::Block,
value: token
.value
.trim_start_matches("/*")
.trim_end_matches("*/")
.trim()
.to_owned(),
},
_ => return None,
};
Some(Node::new(
NonCodeNode { value, digest: None },
token.start,
token.end,
token.module_id,
))
}
})
.context(expected("Non-code token (comments or whitespace)"))
alt((
annotation,
any.verify_map(|token: Token| {
if token.is_code_token() {
None
} else {
let value = match token.token_type {
TokenType::Whitespace if token.value.contains("\n\n") => NonCodeValue::NewLine,
TokenType::LineComment => NonCodeValue::BlockComment {
value: token.value.trim_start_matches("//").trim().to_owned(),
style: CommentStyle::Line,
},
TokenType::BlockComment => NonCodeValue::BlockComment {
style: CommentStyle::Block,
value: token
.value
.trim_start_matches("/*")
.trim_end_matches("*/")
.trim()
.to_owned(),
},
_ => return None,
};
Some(Node::new(
NonCodeNode { value, digest: None },
token.start,
token.end,
token.module_id,
))
}
})
.context(expected("Non-code token (comments or whitespace)")),
))
.parse_next(i)
}
@ -351,22 +395,6 @@ fn pipe_expression(i: &mut TokenSlice) -> PResult<Node<PipeExpression>> {
))
.parse_next(i)?;
// All child parsers have been run.
// First, ensure they all have a % in their args.
let calls_without_substitution = tail.iter().find_map(|(_nc, call_expr, _nc2)| {
if !call_expr.has_substitution_arg() {
Some(call_expr.into())
} else {
None
}
});
if let Some(source_range) = calls_without_substitution {
let err = CompilationError::fatal(
source_range,
"All expressions in a pipeline must use the % (substitution operator)",
);
return Err(ErrMode::Cut(err.into()));
}
// Time to structure the return value.
let mut code_count = 0;
let mut max_noncode_end = 0;
@ -457,10 +485,17 @@ pub(crate) fn unsigned_number_literal(i: &mut TokenSlice) -> PResult<Node<Litera
let (value, token) = any
.try_map(|token: Token| match token.token_type {
TokenType::Number => {
let x: f64 = token.value.parse().map_err(|_| {
let x: f64 = token.numeric_value().ok_or_else(|| {
CompilationError::fatal(token.as_source_range(), format!("Invalid float: {}", token.value))
})?;
if token.numeric_suffix().is_some() {
ParseContext::err(CompilationError::err(
(&token).into(),
"Unit of Measure suffixes are experimental and currently do nothing.",
));
}
Ok((LiteralValue::Number(x), token))
}
_ => Err(CompilationError::fatal(token.as_source_range(), "invalid literal")),
@ -501,6 +536,8 @@ fn binary_operator(i: &mut TokenSlice) -> PResult<BinaryOperator> {
">=" => BinaryOperator::Gte,
"<" => BinaryOperator::Lt,
"<=" => BinaryOperator::Lte,
"|" => BinaryOperator::Or,
"&" => BinaryOperator::And,
_ => {
return Err(CompilationError::fatal(
token.as_source_range(),
@ -754,7 +791,7 @@ fn array_end_start(i: &mut TokenSlice) -> PResult<Node<ArrayRangeExpression>> {
}
fn object_property_same_key_and_val(i: &mut TokenSlice) -> PResult<Node<ObjectProperty>> {
let key = identifier.context(expected("the property's key (the name or identifier of the property), e.g. in 'height: 4', 'height' is the property key")).parse_next(i)?;
let key = nameable_identifier.context(expected("the property's key (the name or identifier of the property), e.g. in 'height: 4', 'height' is the property key")).parse_next(i)?;
ignore_whitespace(i);
Ok(Node {
start: key.start,
@ -778,7 +815,7 @@ fn object_property(i: &mut TokenSlice) -> PResult<Node<ObjectProperty>> {
))
.parse_next(i)?;
ignore_whitespace(i);
let expr = expression
let expr = expression_but_not_ascription
.context(expected(
"the value which you're setting the property to, e.g. in 'height: 4', the value is 4",
))
@ -1086,7 +1123,7 @@ fn member_expression_dot(i: &mut TokenSlice) -> PResult<(LiteralIdentifier, usiz
period.parse_next(i)?;
let property = alt((
sketch_keyword.map(Box::new).map(LiteralIdentifier::Identifier),
identifier.map(Box::new).map(LiteralIdentifier::Identifier),
nameable_identifier.map(Box::new).map(LiteralIdentifier::Identifier),
))
.parse_next(i)?;
let end = property.end();
@ -1099,7 +1136,7 @@ fn member_expression_subscript(i: &mut TokenSlice) -> PResult<(LiteralIdentifier
let property = alt((
sketch_keyword.map(Box::new).map(LiteralIdentifier::Identifier),
literal.map(LiteralIdentifier::Literal),
identifier.map(Box::new).map(LiteralIdentifier::Identifier),
nameable_identifier.map(Box::new).map(LiteralIdentifier::Identifier),
))
.parse_next(i)?;
@ -1113,7 +1150,7 @@ fn member_expression_subscript(i: &mut TokenSlice) -> PResult<(LiteralIdentifier
fn member_expression(i: &mut TokenSlice) -> PResult<Node<MemberExpression>> {
// This is an identifier, followed by a sequence of members (aka properties)
// First, the identifier.
let id = identifier.context(expected("the identifier of the object whose property you're trying to access, e.g. in 'shape.size.width', 'shape' is the identifier")).parse_next(i)?;
let id = nameable_identifier.context(expected("the identifier of the object whose property you're trying to access, e.g. in 'shape.size.width', 'shape' is the identifier")).parse_next(i)?;
// Now a sequence of members.
let member = alt((member_expression_dot, member_expression_subscript)).context(expected("a member/property, e.g. size.x and size['height'] and size[0] are all different ways to access a member/property of 'size'"));
let mut members: Vec<_> = repeat(1.., member)
@ -1186,6 +1223,7 @@ fn noncode_just_after_code(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
x @ NonCodeValue::InlineComment { .. } => x,
x @ NonCodeValue::NewLineBlockComment { .. } => x,
x @ NonCodeValue::NewLine => x,
x @ NonCodeValue::Annotation { .. } => x,
};
Node::new(
NonCodeNode { value, ..nc.inner },
@ -1206,6 +1244,7 @@ fn noncode_just_after_code(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
x @ NonCodeValue::InlineComment { .. } => x,
x @ NonCodeValue::NewLineBlockComment { .. } => x,
x @ NonCodeValue::NewLine => x,
x @ NonCodeValue::Annotation { .. } => x,
};
Node::new(NonCodeNode { value, ..nc.inner }, nc.start, nc.end, nc.module_id)
}
@ -1245,7 +1284,7 @@ fn body_items_within_function(i: &mut TokenSlice) -> PResult<WithinFunction> {
(import_stmt.map(BodyItem::ImportStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
Token { ref value, .. } if value == "return" =>
(return_stmt.map(BodyItem::ReturnStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
token if !token.is_code_token() => {
token if !token.is_code_token() || token.token_type == TokenType::At => {
non_code_node.map(WithinFunction::NonCode)
},
_ =>
@ -1444,7 +1483,7 @@ fn import_stmt(i: &mut TokenSlice) -> PResult<BoxNode<ImportStatement>> {
require_whitespace(i)?;
let (mut selector, path) = alt((
string_literal.map(|s| (ImportSelector::None(None), Some(s))),
string_literal.map(|s| (ImportSelector::None { alias: None }, Some(s))),
glob.map(|t| {
let s = t.as_source_range();
(
@ -1507,7 +1546,7 @@ fn import_stmt(i: &mut TokenSlice) -> PResult<BoxNode<ImportStatement>> {
));
}
if let ImportSelector::None(ref mut a) = selector {
if let ImportSelector::None { alias: ref mut a } = selector {
if let Some(alias) = opt(preceded(
(whitespace, import_as_keyword, whitespace),
identifier.context(expected("an identifier to alias the import")),
@ -1553,7 +1592,9 @@ fn import_stmt(i: &mut TokenSlice) -> PResult<BoxNode<ImportStatement>> {
}
fn import_item(i: &mut TokenSlice) -> PResult<Node<ImportItem>> {
let name = identifier.context(expected("an identifier to import")).parse_next(i)?;
let name = nameable_identifier
.context(expected("an identifier to import"))
.parse_next(i)?;
let start = name.start;
let module_id = name.module_id;
let alias = opt(preceded(
@ -1622,6 +1663,24 @@ fn return_stmt(i: &mut TokenSlice) -> PResult<Node<ReturnStatement>> {
/// Parse a KCL expression.
fn expression(i: &mut TokenSlice) -> PResult<Expr> {
let expr = expression_but_not_ascription.parse_next(i)?;
let ty = opt((colon, opt(whitespace), argument_type)).parse_next(i)?;
// TODO this is probably not giving ascription the right precedence, but I have no idea how Winnow is handling that.
// Since we're not creating AST nodes for ascription, I don't think it matters right now.
if let Some((colon, _, _)) = ty {
ParseContext::err(CompilationError::err(
// Sadly there is no SourceRange for the type itself
colon.into(),
"Type ascription is experimental and currently does nothing.",
));
}
Ok(expr)
}
// TODO once we remove the old record instantiation syntax, we can accept types ascription anywhere.
fn expression_but_not_ascription(i: &mut TokenSlice) -> PResult<Expr> {
alt((
pipe_expression.map(Box::new).map(Expr::PipeExpression),
expression_but_not_pipe,
@ -1678,7 +1737,7 @@ fn expr_allowed_in_pipe_expr(i: &mut TokenSlice) -> PResult<Expr> {
literal.map(Expr::Literal),
fn_call.map(Box::new).map(Expr::CallExpression),
fn_call_kw.map(Box::new).map(Expr::CallExpressionKw),
identifier.map(Box::new).map(Expr::Identifier),
nameable_identifier.map(Box::new).map(Expr::Identifier),
array,
object.map(Box::new).map(Expr::ObjectExpression),
pipe_sub.map(Box::new).map(Expr::PipeSubstitution),
@ -1697,7 +1756,7 @@ fn possible_operands(i: &mut TokenSlice) -> PResult<Expr> {
member_expression.map(Box::new).map(Expr::MemberExpression),
literal.map(Expr::Literal),
fn_call.map(Box::new).map(Expr::CallExpression),
identifier.map(Box::new).map(Expr::Identifier),
nameable_identifier.map(Box::new).map(Expr::Identifier),
binary_expr_in_parens.map(Box::new).map(Expr::BinaryExpression),
unnecessarily_bracketed,
))
@ -1873,6 +1932,24 @@ fn identifier(i: &mut TokenSlice) -> PResult<Node<Identifier>> {
.parse_next(i)
}
fn nameable_identifier(i: &mut TokenSlice) -> PResult<Node<Identifier>> {
let result = identifier.parse_next(i)?;
if !result.is_nameable() {
let desc = if result.name == "_" {
"Underscores"
} else {
"Names with a leading underscore"
};
ParseContext::err(CompilationError::err(
SourceRange::new(result.start, result.end, result.module_id),
format!("{desc} cannot be referred to, only declared."),
));
}
Ok(result)
}
fn sketch_keyword(i: &mut TokenSlice) -> PResult<Node<Identifier>> {
any.try_map(|token: Token| {
if token.token_type == TokenType::Type && token.value == "sketch" {
@ -2224,9 +2301,8 @@ fn question_mark(i: &mut TokenSlice) -> PResult<()> {
Ok(())
}
fn at_sign(i: &mut TokenSlice) -> PResult<()> {
TokenType::At.parse_from(i)?;
Ok(())
fn at_sign(i: &mut TokenSlice) -> PResult<Token> {
TokenType::At.parse_from(i)
}
fn fun(i: &mut TokenSlice) -> PResult<Token> {
@ -2256,12 +2332,16 @@ fn arguments(i: &mut TokenSlice) -> PResult<Vec<Expr>> {
}
fn labeled_argument(i: &mut TokenSlice) -> PResult<LabeledArg> {
separated_pair(identifier, (one_of(TokenType::Colon), opt(whitespace)), expression)
.map(|(label, arg)| LabeledArg {
label: label.inner,
arg,
})
.parse_next(i)
separated_pair(
terminated(nameable_identifier, opt(whitespace)),
terminated(one_of((TokenType::Operator, "=")), opt(whitespace)),
expression,
)
.map(|(label, arg)| LabeledArg {
label: label.inner,
arg,
})
.parse_next(i)
}
/// Arguments are passed into a function,
@ -2289,17 +2369,31 @@ fn argument_type(i: &mut TokenSlice) -> PResult<FnArgType> {
.map_err(|err| CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", err)))
}),
// Primitive types
one_of(TokenType::Type).map(|token: Token| {
FnArgPrimitive::from_str(&token.value)
.map(FnArgType::Primitive)
.map_err(|err| CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", err)))
}),
(
one_of(TokenType::Type),
opt(delimited(open_paren, uom_for_type, close_paren)),
)
.map(|(token, suffix)| {
if suffix.is_some() {
ParseContext::err(CompilationError::err(
(&token).into(),
"Unit of Measure types are experimental and currently do nothing.",
));
}
FnArgPrimitive::from_str(&token.value)
.map(FnArgType::Primitive)
.map_err(|err| CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", err)))
}),
))
.parse_next(i)?
.map_err(|e: CompilationError| ErrMode::Backtrack(ContextError::from(e)))?;
Ok(type_)
}
fn uom_for_type(i: &mut TokenSlice) -> PResult<NumericSuffix> {
any.try_map(|t: Token| t.value.parse()).parse_next(i)
}
struct ParamDescription {
labeled: bool,
arg_name: Token,
@ -2486,7 +2580,7 @@ fn labelled_fn_call(i: &mut TokenSlice) -> PResult<Expr> {
}
fn fn_call(i: &mut TokenSlice) -> PResult<Node<CallExpression>> {
let fn_name = identifier(i)?;
let fn_name = nameable_identifier(i)?;
opt(whitespace).parse_next(i)?;
let _ = terminated(open_paren, opt(whitespace)).parse_next(i)?;
let args = arguments(i)?;
@ -2527,7 +2621,7 @@ fn fn_call(i: &mut TokenSlice) -> PResult<Node<CallExpression>> {
}
fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
let fn_name = identifier(i)?;
let fn_name = nameable_identifier(i)?;
opt(whitespace).parse_next(i)?;
let _ = open_paren.parse_next(i)?;
ignore_whitespace(i);
@ -2535,6 +2629,7 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
let initial_unlabeled_arg = opt((expression, comma, opt(whitespace)).map(|(arg, _, _)| arg)).parse_next(i)?;
let args = labeled_arguments(i)?;
ignore_whitespace(i);
opt(comma_sep).parse_next(i)?;
let end = close_paren.parse_next(i)?.end;
Ok(Node {
@ -3459,6 +3554,18 @@ mySk1 = startSketchAt([0, 0])"#;
(result.0.unwrap(), result.1)
}
#[track_caller]
fn assert_no_fatal(p: &str) -> (Node<Program>, Vec<CompilationError>) {
let result = crate::parsing::top_level_parse(p);
let result = result.0.unwrap();
assert!(
result.1.iter().all(|e| e.severity != Severity::Fatal),
"found: {:#?}",
result.1
);
(result.0.unwrap(), result.1)
}
#[track_caller]
fn assert_err(p: &str, msg: &str, src_expected: [usize; 2]) {
let result = crate::parsing::top_level_parse(p);
@ -3552,6 +3659,22 @@ height = [obj["a"] -1, 0]"#;
crate::parsing::top_level_parse("foo(42, fn(x) { return x + 1 })").unwrap();
}
#[test]
fn test_annotation_fn() {
crate::parsing::top_level_parse(
r#"fn foo() {
@annotated
return 1
}"#,
)
.unwrap();
}
#[test]
fn test_annotation_settings() {
crate::parsing::top_level_parse("@settings(units = mm)").unwrap();
}
#[test]
fn test_anon_fn_no_fn() {
assert_err_contains("foo(42, (x) { return x + 1 })", "Anonymous function requires `fn`");
@ -3856,6 +3979,25 @@ e
assert_eq!(errs.len(), 1);
}
#[test]
fn fn_decl_uom_ty() {
let some_program_string = r#"fn foo(x: number(mm)): number(_) { return 1 }"#;
let (_, errs) = assert_no_fatal(some_program_string);
assert_eq!(errs.len(), 2);
}
#[test]
fn error_underscore() {
let (_, errs) = assert_no_fatal("_foo(_blah, _)");
assert_eq!(errs.len(), 3, "found: {:#?}", errs);
}
#[test]
fn error_type_ascription() {
let (_, errs) = assert_no_fatal("a + b: number");
assert_eq!(errs.len(), 1, "found: {:#?}", errs);
}
#[test]
fn zero_param_function() {
let code = r#"
@ -4034,19 +4176,6 @@ let myBox = box([0,0], -3, -16, -10)
crate::parsing::top_level_parse(some_program_string).unwrap();
}
#[test]
fn must_use_percent_in_pipeline_fn() {
let some_program_string = r#"
foo()
|> bar(2)
"#;
assert_err(
some_program_string,
"All expressions in a pipeline must use the % (substitution operator)",
[30, 36],
);
}
#[test]
fn arg_labels() {
let input = r#"length: 3"#;
@ -4057,7 +4186,7 @@ let myBox = box([0,0], -3, -16, -10)
#[test]
fn kw_fn() {
for input in ["val = foo(x, y: z)", "val = foo(y: z)"] {
for input in ["val = foo(x, y = z)", "val = foo(y = z)"] {
let module_id = ModuleId::default();
let tokens = crate::parsing::token::lex(input, module_id).unwrap();
super::program.parse(tokens.as_slice()).unwrap();
@ -4239,6 +4368,20 @@ var baz = 2
"#
);
}
#[test]
fn test_unary_not_on_keyword_bool() {
let some_program_string = r#"!true"#;
let module_id = ModuleId::default();
let tokens = crate::parsing::token::lex(some_program_string, module_id).unwrap(); // Updated import path
let actual = match unary_expression.parse(tokens.as_slice()) {
// Use tokens.as_slice() for parsing
Ok(x) => x,
Err(e) => panic!("{e:?}"),
};
assert_eq!(actual.operator, UnaryOperator::Not);
crate::parsing::top_level_parse(some_program_string).unwrap(); // Updated import path
}
}
#[cfg(test)]
@ -4497,8 +4640,13 @@ my14 = 4 ^ 2 - 3 ^ 2 * 2
r#"x = 3
obj = { x, y: 4}"#
);
snapshot_test!(kw_function_unnamed_first, r#"val = foo(x, y: z)"#);
snapshot_test!(kw_function_all_named, r#"val = foo(x: a, y: b)"#);
snapshot_test!(bj, "true");
snapshot_test!(bk, "truee");
snapshot_test!(bl, "x = !true");
snapshot_test!(bm, "x = true & false");
snapshot_test!(bn, "x = true | false");
snapshot_test!(kw_function_unnamed_first, r#"val = foo(x, y = z)"#);
snapshot_test!(kw_function_all_named, r#"val = foo(x = a, y = b)"#);
snapshot_test!(kw_function_decl_all_labeled, r#"fn foo(x, y) { return 1 }"#);
snapshot_test!(kw_function_decl_first_unlabeled, r#"fn foo(@x, y) { return 1 }"#);
snapshot_test!(kw_function_decl_with_default_no_type, r#"fn foo(x? = 2) { return 1 }"#);

View File

@ -0,0 +1,26 @@
---
source: kcl/src/parsing/parser.rs
assertion_line: 4521
expression: actual
snapshot_kind: text
---
{
"body": [
{
"end": 4,
"expression": {
"end": 4,
"raw": "true",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": true
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
}
],
"end": 4,
"start": 0
}

View File

@ -0,0 +1,25 @@
---
source: kcl/src/parsing/parser.rs
assertion_line: 4522
expression: actual
snapshot_kind: text
---
{
"body": [
{
"end": 5,
"expression": {
"end": 5,
"name": "truee",
"start": 0,
"type": "Identifier",
"type": "Identifier"
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
}
],
"end": 5,
"start": 0
}

View File

@ -0,0 +1,45 @@
---
source: kcl/src/parsing/parser.rs
assertion_line: 4523
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 9,
"id": {
"end": 1,
"name": "x",
"start": 0,
"type": "Identifier"
},
"init": {
"argument": {
"end": 9,
"raw": "true",
"start": 5,
"type": "Literal",
"type": "Literal",
"value": true
},
"end": 9,
"operator": "!",
"start": 4,
"type": "UnaryExpression",
"type": "UnaryExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 9,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 9,
"start": 0
}

View File

@ -0,0 +1,53 @@
---
source: kcl/src/parsing/parser.rs
assertion_line: 4524
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 16,
"id": {
"end": 1,
"name": "x",
"start": 0,
"type": "Identifier"
},
"init": {
"end": 16,
"left": {
"end": 8,
"raw": "true",
"start": 4,
"type": "Literal",
"type": "Literal",
"value": true
},
"operator": "&",
"right": {
"end": 16,
"raw": "false",
"start": 11,
"type": "Literal",
"type": "Literal",
"value": false
},
"start": 4,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 16,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 16,
"start": 0
}

View File

@ -0,0 +1,53 @@
---
source: kcl/src/parsing/parser.rs
assertion_line: 4525
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 16,
"id": {
"end": 1,
"name": "x",
"start": 0,
"type": "Identifier"
},
"init": {
"end": 16,
"left": {
"end": 8,
"raw": "true",
"start": 4,
"type": "Literal",
"type": "Literal",
"value": true
},
"operator": "|",
"right": {
"end": 16,
"raw": "false",
"start": 11,
"type": "Literal",
"type": "Literal",
"value": false
},
"start": 4,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 16,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 16,
"start": 0
}

View File

@ -1,12 +1,13 @@
---
source: kcl/src/parsing/parser.rs
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 21,
"end": 23,
"id": {
"end": 3,
"name": "val",
@ -22,9 +23,9 @@ expression: actual
"name": "x"
},
"arg": {
"end": 14,
"end": 15,
"name": "a",
"start": 13,
"start": 14,
"type": "Identifier",
"type": "Identifier"
}
@ -36,9 +37,9 @@ expression: actual
"name": "y"
},
"arg": {
"end": 20,
"end": 22,
"name": "b",
"start": 19,
"start": 21,
"type": "Identifier",
"type": "Identifier"
}
@ -50,7 +51,7 @@ expression: actual
"start": 6,
"type": "Identifier"
},
"end": 21,
"end": 23,
"start": 6,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
@ -59,13 +60,13 @@ expression: actual
"start": 0,
"type": "VariableDeclarator"
},
"end": 21,
"end": 23,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 21,
"end": 23,
"start": 0
}

View File

@ -1,12 +1,13 @@
---
source: kcl/src/parsing/parser.rs
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 18,
"end": 19,
"id": {
"end": 3,
"name": "val",
@ -22,9 +23,9 @@ expression: actual
"name": "y"
},
"arg": {
"end": 17,
"end": 18,
"name": "z",
"start": 16,
"start": 17,
"type": "Identifier",
"type": "Identifier"
}
@ -36,7 +37,7 @@ expression: actual
"start": 6,
"type": "Identifier"
},
"end": 18,
"end": 19,
"start": 6,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
@ -51,13 +52,13 @@ expression: actual
"start": 0,
"type": "VariableDeclarator"
},
"end": 18,
"end": 19,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 18,
"end": 19,
"start": 0
}

View File

@ -1,7 +1,7 @@
// Clippy does not agree with rustc here for some reason.
#![allow(clippy::needless_lifetimes)]
use std::{fmt, iter::Enumerate, num::NonZeroUsize};
use std::{fmt, iter::Enumerate, num::NonZeroUsize, str::FromStr};
use anyhow::Result;
use parse_display::Display;
@ -17,6 +17,7 @@ use crate::{
errors::KclError,
parsing::ast::types::{ItemVisibility, VariableKind},
source_range::{ModuleId, SourceRange},
CompilationError,
};
mod tokeniser;
@ -24,6 +25,53 @@ mod tokeniser;
#[cfg(test)]
pub(crate) use tokeniser::RESERVED_WORDS;
// Note the ordering, it's important that `m` comes after `mm` and `cm`.
pub const NUM_SUFFIXES: [&str; 9] = ["mm", "cm", "m", "inch", "in", "ft", "yd", "deg", "rad"];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NumericSuffix {
None,
Count,
Mm,
Cm,
M,
Inch,
Ft,
Yd,
Deg,
Rad,
}
impl NumericSuffix {
#[allow(dead_code)]
pub fn is_none(self) -> bool {
self == Self::None
}
pub fn is_some(self) -> bool {
self != Self::None
}
}
impl FromStr for NumericSuffix {
type Err = CompilationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"_" => Ok(NumericSuffix::Count),
"mm" => Ok(NumericSuffix::Mm),
"cm" => Ok(NumericSuffix::Cm),
"m" => Ok(NumericSuffix::M),
"inch" | "in" => Ok(NumericSuffix::Inch),
"ft" => Ok(NumericSuffix::Ft),
"yd" => Ok(NumericSuffix::Yd),
"deg" => Ok(NumericSuffix::Deg),
"rad" => Ok(NumericSuffix::Rad),
_ => Err(CompilationError::err(SourceRange::default(), "invalid unit of measure")),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct TokenStream {
tokens: Vec<Token>,
@ -369,6 +417,36 @@ impl Token {
}
}
pub fn numeric_value(&self) -> Option<f64> {
if self.token_type != TokenType::Number {
return None;
}
let value = &self.value;
let value = value
.split_once(|c: char| c == '_' || c.is_ascii_alphabetic())
.map(|(s, _)| s)
.unwrap_or(value);
value.parse().ok()
}
pub fn numeric_suffix(&self) -> NumericSuffix {
if self.token_type != TokenType::Number {
return NumericSuffix::None;
}
if self.value.ends_with('_') {
return NumericSuffix::Count;
}
for suffix in NUM_SUFFIXES {
if self.value.ends_with(suffix) {
return suffix.parse().unwrap();
}
}
NumericSuffix::None
}
/// Is this token the beginning of a variable/function declaration?
/// If so, what kind?
/// If not, returns None.

View File

@ -50,7 +50,6 @@ lazy_static! {
set.insert("record", TokenType::Keyword);
set.insert("struct", TokenType::Keyword);
set.insert("object", TokenType::Keyword);
set.insert("_", TokenType::Keyword);
set.insert("string", TokenType::Type);
set.insert("number", TokenType::Type);
@ -147,9 +146,9 @@ fn line_comment(i: &mut Input<'_>) -> PResult<Token> {
fn number(i: &mut Input<'_>) -> PResult<Token> {
let number_parser = alt((
// Digits before the decimal point.
(digit1, opt(('.', digit1))).map(|_| ()),
(digit1, opt(('.', digit1)), opt('_'), opt(alt(super::NUM_SUFFIXES))).map(|_| ()),
// No digits before the decimal point.
('.', digit1).map(|_| ()),
('.', digit1, opt('_'), opt(alt(super::NUM_SUFFIXES))).map(|_| ()),
));
let (value, range) = number_parser.take().with_span().parse_next(i)?;
Ok(Token::from_range(
@ -188,7 +187,7 @@ fn word(i: &mut Input<'_>) -> PResult<Token> {
fn operator(i: &mut Input<'_>) -> PResult<Token> {
let (value, range) = alt((
">=", "<=", "==", "=>", "!=", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "|", "^",
">=", "<=", "==", "=>", "!=", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "^", "|", "&",
))
.with_span()
.parse_next(i)?;
@ -366,6 +365,7 @@ mod tests {
use super::*;
use crate::parsing::token::TokenSlice;
fn assert_parse_err<'i, P, O, E>(mut p: P, s: &'i str)
where
O: std::fmt::Debug,
@ -379,7 +379,8 @@ mod tests {
assert!(p.parse_next(&mut input).is_err(), "parsed {s} but should have failed");
}
fn assert_parse_ok<'i, P, O, E>(mut p: P, s: &'i str)
// Returns the token and whether any more input is remaining to tokenize.
fn assert_parse_ok<'i, P, O, E>(mut p: P, s: &'i str) -> (O, bool)
where
E: std::fmt::Debug,
O: std::fmt::Debug,
@ -392,14 +393,27 @@ mod tests {
};
let res = p.parse_next(&mut input);
assert!(res.is_ok(), "failed to parse {s}, got {}", res.unwrap_err());
(res.unwrap(), !input.is_empty())
}
#[test]
fn test_number() {
for valid in [
"1", "1 abc", "1.1", "1.1 abv", "1.1 abv", "1", ".1", "5?", "5 + 6", "5 + a", "5.5", "1abc",
for (valid, expected) in [
("1", false),
("1 abc", true),
("1.1", false),
("1.1 abv", true),
("1.1 abv", true),
("1", false),
(".1", false),
("5?", true),
("5 + 6", true),
("5 + a", true),
("5.5", false),
("1abc", true),
] {
assert_parse_ok(number, valid);
let (_, remaining) = assert_parse_ok(number, valid);
assert_eq!(expected, remaining, "`{valid}` expected another token to be {expected}");
}
for invalid in ["a", "?", "?5"] {
@ -415,6 +429,27 @@ mod tests {
assert_eq!(number.parse(input).unwrap().value, "0.0000000000");
}
#[test]
fn test_number_suffix() {
for (valid, expected_val, expected_next) in [
("1_", 1.0, false),
("1_mm", 1.0, false),
("1_yd", 1.0, false),
("1m", 1.0, false),
("1inch", 1.0, false),
("1toot", 1.0, true),
("1.4inch t", 1.4, true),
] {
let (t, remaining) = assert_parse_ok(number, valid);
assert_eq!(expected_next, remaining);
assert_eq!(
Some(expected_val),
t.numeric_value(),
"{valid} has incorrect numeric value, expected {expected_val} {t:?}"
);
}
}
#[test]
fn test_word() {
for valid in ["a", "a ", "a5", "a5a"] {
@ -429,7 +464,7 @@ mod tests {
#[test]
fn test_operator() {
for valid in [
"+", "+ ", "-", "<=", "<= ", ">=", ">= ", "> ", "< ", "| ", "|> ", "^ ", "% ", "+* ",
"+", "+ ", "-", "<=", "<= ", ">=", ">= ", "> ", "< ", "|> ", "^ ", "% ", "+* ", "| ", "& ",
] {
assert_parse_ok(operator, valid);
}
@ -715,4 +750,30 @@ const things = "things"
}
}
}
#[test]
fn test_boolean_literal() {
let module_id = ModuleId::default();
let actual = lex("true", module_id).unwrap();
let expected = Token {
token_type: TokenType::Keyword,
value: "true".to_owned(),
start: 0,
end: 4,
module_id,
};
assert_eq!(actual.tokens[0], expected);
}
#[test]
fn test_word_starting_with_keyword() {
let module_id = ModuleId::default();
let actual = lex("truee", module_id).unwrap();
let expected = Token {
token_type: TokenType::Word,
value: "truee".to_owned(),
start: 0,
end: 5,
module_id,
};
assert_eq!(actual.tokens[0], expected);
}
}

View File

@ -477,6 +477,10 @@ pub struct CommandBarSettings {
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum OnboardingStatus {
/// The unset state.
#[serde(rename = "")]
#[display("")]
Unset,
/// The user has completed onboarding.
Completed,
/// The user has not completed onboarding.

View File

@ -91,7 +91,7 @@ async fn execute(test_name: &str, render_to_png: bool) {
)
.await;
match exec_res {
Ok((program_memory, png)) => {
Ok((program_memory, ops, png)) => {
if render_to_png {
twenty_twenty::assert_image(format!("tests/{test_name}/rendered_model.png"), &png, 0.99);
}
@ -104,9 +104,12 @@ async fn execute(test_name: &str, render_to_png: bool) {
".environments[].**[].z[]" => rounded_redaction(4),
});
});
assert_snapshot(test_name, "Operations executed", || {
insta::assert_json_snapshot!("ops", ops);
});
}
Err(e) => {
match e {
match e.error {
crate::errors::ExecError::Kcl(error) => {
// Snapshot the KCL error with a fancy graphical report.
// This looks like a Cargo compile error, with arrows pointing
@ -122,6 +125,10 @@ async fn execute(test_name: &str, render_to_png: bool) {
assert_snapshot(test_name, "Error from executing", || {
insta::assert_snapshot!("execution_error", report);
});
assert_snapshot(test_name, "Operations executed", || {
insta::assert_json_snapshot!("ops", e.exec_state.mod_local.operations);
});
}
e => {
// These kinds of errors aren't expected to occur. We don't
@ -704,6 +711,27 @@ mod import_glob {
super::execute(TEST_NAME, false).await
}
}
mod import_whole {
const TEST_NAME: &str = "import_whole";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod import_side_effect {
const TEST_NAME: &str = "import_side_effect";
@ -1502,48 +1530,6 @@ mod kw_fn {
super::execute(TEST_NAME, true).await
}
}
mod tag_can_be_proxied_through_parameter {
const TEST_NAME: &str = "tag_can_be_proxied_through_parameter";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod tag_proxied_through_function_does_not_define_var {
const TEST_NAME: &str = "tag_proxied_through_function_does_not_define_var";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod kw_fn_too_few_args {
const TEST_NAME: &str = "kw_fn_too_few_args";
@ -1607,3 +1593,66 @@ mod kw_fn_with_defaults {
super::execute(TEST_NAME, false).await
}
}
mod boolean_logical_and {
const TEST_NAME: &str = "boolean_logical_and";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod boolean_logical_or {
const TEST_NAME: &str = "boolean_logical_or";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod boolean_logical_multiple {
const TEST_NAME: &str = "boolean_logical_multiple";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}

View File

@ -61,28 +61,34 @@ impl KwArgs {
pub struct Args {
/// Positional args.
pub args: Vec<Arg>,
/// Keyword arguments
pub kw_args: KwArgs,
pub source_range: SourceRange,
pub ctx: ExecutorContext,
/// If this call happens inside a pipe (|>) expression, this holds the LHS of that |>.
/// Otherwise it's None.
pipe_value: Option<Arg>,
}
impl Args {
pub fn new(args: Vec<Arg>, source_range: SourceRange, ctx: ExecutorContext) -> Self {
pub fn new(args: Vec<Arg>, source_range: SourceRange, ctx: ExecutorContext, pipe_value: Option<Arg>) -> Self {
Self {
args,
kw_args: Default::default(),
source_range,
ctx,
pipe_value,
}
}
/// Collect the given keyword arguments.
pub fn new_kw(kw_args: KwArgs, source_range: SourceRange, ctx: ExecutorContext) -> Self {
pub fn new_kw(kw_args: KwArgs, source_range: SourceRange, ctx: ExecutorContext, pipe_value: Option<Arg>) -> Self {
Self {
args: Default::default(),
kw_args,
source_range,
ctx,
pipe_value,
}
}
@ -101,6 +107,7 @@ impl Args {
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
},
pipe_value: None,
})
}
@ -133,12 +140,17 @@ impl Args {
where
T: FromKclValue<'a>,
{
let Some(ref arg) = self.kw_args.unlabeled else {
return Err(KclError::Semantic(KclErrorDetails {
let arg = self
.kw_args
.unlabeled
.as_ref()
.or(self.args.first())
.or(self.pipe_value.as_ref())
.ok_or(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.source_range],
message: format!("This function requires a value for the special unlabeled first parameter, '{label}'"),
}));
};
}))?;
T::from_kcl_val(&arg.value).ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
source_ranges: arg.source_ranges(),
@ -181,7 +193,7 @@ impl Args {
exec_state: &'e mut ExecState,
tag: &'a TagIdentifier,
) -> Result<&'e crate::execution::TagEngineInfo, KclError> {
if let KclValue::TagIdentifier(t) = exec_state.memory.get(&tag.value, self.source_range)? {
if let KclValue::TagIdentifier(t) = exec_state.memory().get(&tag.value, self.source_range)? {
Ok(t.info.as_ref().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Tag `{}` does not have engine info", tag.value),
@ -247,12 +259,12 @@ impl Args {
// Find all the solids on the same shared sketch.
ids.extend(
exec_state
.memory
.memory()
.find_solids_on_sketch(solid.sketch.id)
.iter()
.flat_map(|eg| eg.get_all_edge_cut_ids()),
);
ids.extend(exec_state.dynamic_state.edge_cut_ids_on_sketch(sketch_id));
ids.extend(exec_state.mod_local.dynamic_state.edge_cut_ids_on_sketch(sketch_id));
traversed_sketches.push(sketch_id);
}
@ -411,13 +423,6 @@ impl Args {
FromArgs::from_args(self, 0)
}
pub(crate) fn get_sketches_and_data<'a, T>(&'a self) -> Result<(Vec<Sketch>, Option<T>), KclError>
where
T: FromArgs<'a> + serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
{
FromArgs::from_args(self, 0)
}
pub(crate) fn get_data_and_optional_tag<'a, T>(&'a self) -> Result<(T, Option<FaceTag>), KclError>
where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
@ -865,22 +870,6 @@ impl<'a> FromKclValue<'a> for crate::std::polar::PolarCoordsData {
}
}
impl<'a> FromKclValue<'a> for crate::std::loft::LoftData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;
let_field_of!(obj, v_degree?);
let_field_of!(obj, bez_approximate_rational?);
let_field_of!(obj, base_curve_index?);
let_field_of!(obj, tolerance?);
Some(Self {
v_degree,
bez_approximate_rational,
base_curve_index,
tolerance,
})
}
}
impl<'a> FromKclValue<'a> for crate::std::planes::StandardPlane {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let s = arg.as_str()?;

View File

@ -30,7 +30,7 @@ pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
/// Given a list like `[a, b, c]`, and a function like `f`, returns
/// `[f(a), f(b), f(c)]`
/// ```no_run
/// const r = 10 // radius
/// r = 10 // radius
/// fn drawCircle(id) {
/// return startSketchOn("XY")
/// |> circle({ center: [id * 2 * r, 0], radius: r}, %)
@ -39,15 +39,15 @@ pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
/// // Call `drawCircle`, passing in each element of the array.
/// // The outputs from each `drawCircle` form a new array,
/// // which is the return value from `map`.
/// const circles = map(
/// circles = map(
/// [1..3],
/// drawCircle
/// )
/// ```
/// ```no_run
/// const r = 10 // radius
/// r = 10 // radius
/// // Call `map`, using an anonymous function instead of a named one.
/// const circles = map(
/// circles = map(
/// [1..3],
/// fn(id) {
/// return startSketchOn("XY")
@ -106,17 +106,17 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// using the previous value and the element.
/// ```no_run
/// // This function adds two numbers.
/// fn add = (a, b) => { return a + b }
/// fn add(a, b) { return a + b }
///
/// // This function adds an array of numbers.
/// // It uses the `reduce` function, to call the `add` function on every
/// // element of the `arr` parameter. The starting value is 0.
/// fn sum = (arr) => { return reduce(arr, 0, add) }
/// fn sum(arr) { return reduce(arr, 0, add) }
///
/// /*
/// The above is basically like this pseudo-code:
/// fn sum(arr):
/// let sumSoFar = 0
/// sumSoFar = 0
/// for i in arr:
/// sumSoFar = add(sumSoFar, i)
/// return sumSoFar
@ -139,7 +139,7 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// ```
/// ```no_run
/// // Declare a function that sketches a decagon.
/// fn decagon = (radius) => {
/// fn decagon(radius) {
/// // Each side of the decagon is turned this many degrees from the previous angle.
/// stepAngle = (1/10) * tau()
///
@ -151,8 +151,8 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// // which takes a partially-sketched decagon and adds one more edge to it.
/// fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {
/// // Draw one edge of the decagon.
/// let x = cos(stepAngle * i) * radius
/// let y = sin(stepAngle * i) * radius
/// x = cos(stepAngle * i) * radius
/// y = sin(stepAngle * i) * radius
/// return lineTo([x, y], partialDecagon)
/// })
///
@ -163,14 +163,14 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// /*
/// The `decagon` above is basically like this pseudo-code:
/// fn decagon(radius):
/// let stepAngle = (1/10) * tau()
/// let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
/// stepAngle = (1/10) * tau()
/// startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
///
/// // Here's the reduce part.
/// let partialDecagon = startOfDecagonSketch
/// partialDecagon = startOfDecagonSketch
/// for i in [1..10]:
/// let x = cos(stepAngle * i) * radius
/// let y = sin(stepAngle * i) * radius
/// x = cos(stepAngle * i) * radius
/// y = sin(stepAngle * i) * radius
/// partialDecagon = lineTo([x, y], partialDecagon)
/// fullDecagon = partialDecagon // it's now full
/// return fullDecagon
@ -224,8 +224,8 @@ async fn call_reduce_closure<'a>(
/// Returns a new array with the element appended.
///
/// ```no_run
/// let arr = [1, 2, 3]
/// let new_arr = push(arr, 4)
/// arr = [1, 2, 3]
/// new_arr = push(arr, 4)
/// assertEqual(new_arr[3], 4, 0.00001, "4 was added to the end of the array")
/// ```
#[stdlib {

View File

@ -31,7 +31,7 @@ pub async fn assert(_exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// is false.
///
/// ```no_run
/// const myVar = true
/// myVar = true
/// assert(myVar, "should always be true")
/// ```
#[stdlib {
@ -70,8 +70,8 @@ pub async fn assert_gt(_exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// otherwise raise an error.
///
/// ```no_run
/// let n = 1.0285
/// let o = 1.0286
/// n = 1.0285
/// o = 1.0286
/// assertEqual(n, o, 0.01, "n is within the given tolerance for o")
/// ```
#[stdlib {

View File

@ -43,19 +43,19 @@ pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
///
/// ```no_run
/// // Chamfer a mounting plate.
/// const width = 20
/// const length = 10
/// const thickness = 1
/// const chamferLength = 2
/// width = 20
/// length = 10
/// thickness = 1
/// chamferLength = 2
///
/// const mountingPlateSketch = startSketchOn("XY")
/// mountingPlateSketch = startSketchOn("XY")
/// |> startProfileAt([-width/2, -length/2], %)
/// |> lineTo([width/2, -length/2], %, $edge1)
/// |> lineTo([width/2, length/2], %, $edge2)
/// |> lineTo([-width/2, length/2], %, $edge3)
/// |> close(%, $edge4)
///
/// const mountingPlate = extrude(thickness, mountingPlateSketch)
/// mountingPlate = extrude(thickness, mountingPlateSketch)
/// |> chamfer({
/// length = chamferLength,
/// tags = [
@ -69,8 +69,8 @@ pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
///
/// ```no_run
/// // Sketch on the face of a chamfer.
/// fn cube = (pos, scale) => {
/// const sg = startSketchOn('XY')
/// fn cube(pos, scale) {
/// sg = startSketchOn('XY')
/// |> startProfileAt(pos, %)
/// |> line([0, scale], %)
/// |> line([scale, 0], %)
@ -79,7 +79,7 @@ pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// return sg
/// }
///
/// const part001 = cube([0,0], 20)
/// part001 = cube([0,0], 20)
/// |> close(%, $line1)
/// |> extrude(20, %)
/// |> chamfer({
@ -87,7 +87,7 @@ pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// tags = [getOppositeEdge(line1)]
/// }, %, $chamfer1) // We tag the chamfer to reference it later.
///
/// const sketch001 = startSketchOn(part001, chamfer1)
/// sketch001 = startSketchOn(part001, chamfer1)
/// |> startProfileAt([10, 10], %)
/// |> line([2, 0], %)
/// |> line([0, 2], %)
@ -98,6 +98,7 @@ pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// ```
#[stdlib {
name = "chamfer",
feature_tree_operation = true,
}]
async fn inner_chamfer(
data: ChamferData,
@ -133,7 +134,7 @@ async fn inner_chamfer(
EdgeReference::Tag(edge_tag) => args.get_tag_engine_info(exec_state, &edge_tag)?.id,
};
let id = exec_state.id_generator.next_uuid();
let id = exec_state.global.id_generator.next_uuid();
args.batch_end_cmd(
id,
ModelingCmd::from(mcmd::Solid3dFilletEdge {

View File

@ -21,7 +21,7 @@ pub async fn int(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
/// DEPRECATED use floor(), ceil(), or round().
///
/// ```no_run
/// let n = int(ceil(5/2))
/// n = int(ceil(5/2))
/// assertEqual(n, 3, 0.0001, "5/2 = 2.5, rounded up makes 3")
/// // Draw n cylinders.
/// startSketchOn('XZ')

View File

@ -33,7 +33,7 @@ pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// cut into an existing solid.
///
/// ```no_run
/// const example = startSketchOn('XZ')
/// example = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line([10, 0], %)
/// |> arc({
@ -54,7 +54,7 @@ pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// ```
///
/// ```no_run
/// const exampleSketch = startSketchOn('XZ')
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([-10, 0], %)
/// |> arc({
/// angleStart = 120,
@ -72,10 +72,11 @@ pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// |> line([-5, -2], %)
/// |> close(%)
///
/// const example = extrude(10, exampleSketch)
/// example = extrude(10, exampleSketch)
/// ```
#[stdlib {
name = "extrude"
name = "extrude",
feature_tree_operation = true,
}]
async fn inner_extrude(
length: f64,
@ -83,7 +84,7 @@ async fn inner_extrude(
exec_state: &mut ExecState,
args: Args,
) -> Result<SolidSet, KclError> {
let id = exec_state.id_generator.next_uuid();
let id = exec_state.next_uuid();
// Extrude the element(s).
let sketches: Vec<Sketch> = sketch_set.into();
@ -92,7 +93,7 @@ async fn inner_extrude(
// Before we extrude, we need to enable the sketch mode.
// We do this here in case extrude is called out of order.
args.batch_modeling_cmd(
exec_state.id_generator.next_uuid(),
exec_state.next_uuid(),
ModelingCmd::from(mcmd::EnableSketchMode {
animated: false,
ortho: false,
@ -113,13 +114,14 @@ async fn inner_extrude(
ModelingCmd::from(mcmd::Extrude {
target: sketch.id.into(),
distance: LengthUnit(length),
faces: Default::default(),
}),
)
.await?;
// Disable the sketch mode.
args.batch_modeling_cmd(
exec_state.id_generator.next_uuid(),
exec_state.next_uuid(),
ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable {}),
)
.await?;
@ -138,7 +140,7 @@ pub(crate) async fn do_post_extrude(
// Bring the object to the front of the scene.
// See: https://github.com/KittyCAD/modeling-app/issues/806
args.batch_modeling_cmd(
exec_state.id_generator.next_uuid(),
exec_state.next_uuid(),
ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
)
.await?;
@ -161,7 +163,7 @@ pub(crate) async fn do_post_extrude(
let solid3d_info = args
.send_modeling_cmd(
exec_state.id_generator.next_uuid(),
exec_state.next_uuid(),
ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
edge_id: any_edge_id,
object_id: sketch.id,
@ -194,7 +196,7 @@ pub(crate) async fn do_post_extrude(
// Instead, the Typescript codebases (which handles WebSocket sends when compiled via Wasm)
// uses this to build the artifact graph, which the UI needs.
args.batch_modeling_cmd(
exec_state.id_generator.next_uuid(),
exec_state.next_uuid(),
ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
edge_id: curve_id,
object_id: sketch.id,
@ -204,7 +206,7 @@ pub(crate) async fn do_post_extrude(
.await?;
args.batch_modeling_cmd(
exec_state.id_generator.next_uuid(),
exec_state.next_uuid(),
ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
edge_id: curve_id,
object_id: sketch.id,
@ -257,7 +259,7 @@ pub(crate) async fn do_post_extrude(
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
// pushing this values with a fake face_id to make extrudes mock-execute safe
face_id: exec_state.id_generator.next_uuid(),
face_id: exec_state.next_uuid(),
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
@ -303,8 +305,8 @@ fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<Extrus
};
if args.ctx.is_mock() {
// Create fake IDs for start and end caps, to make extrudes mock-execute safe
faces.start_cap_id = Some(exec_state.id_generator.next_uuid());
faces.end_cap_id = Some(exec_state.id_generator.next_uuid());
faces.start_cap_id = Some(exec_state.next_uuid());
faces.end_cap_id = Some(exec_state.next_uuid());
}
for face_info in face_infos {
match face_info.cap {

View File

@ -68,19 +68,19 @@ pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// will smoothly blend the transition.
///
/// ```no_run
/// const width = 20
/// const length = 10
/// const thickness = 1
/// const filletRadius = 2
/// width = 20
/// length = 10
/// thickness = 1
/// filletRadius = 2
///
/// const mountingPlateSketch = startSketchOn("XY")
/// mountingPlateSketch = startSketchOn("XY")
/// |> startProfileAt([-width/2, -length/2], %)
/// |> lineTo([width/2, -length/2], %, $edge1)
/// |> lineTo([width/2, length/2], %, $edge2)
/// |> lineTo([-width/2, length/2], %, $edge3)
/// |> close(%, $edge4)
///
/// const mountingPlate = extrude(thickness, mountingPlateSketch)
/// mountingPlate = extrude(thickness, mountingPlateSketch)
/// |> fillet({
/// radius = filletRadius,
/// tags = [
@ -93,19 +93,19 @@ pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// ```
///
/// ```no_run
/// const width = 20
/// const length = 10
/// const thickness = 1
/// const filletRadius = 1
/// width = 20
/// length = 10
/// thickness = 1
/// filletRadius = 1
///
/// const mountingPlateSketch = startSketchOn("XY")
/// mountingPlateSketch = startSketchOn("XY")
/// |> startProfileAt([-width/2, -length/2], %)
/// |> lineTo([width/2, -length/2], %, $edge1)
/// |> lineTo([width/2, length/2], %, $edge2)
/// |> lineTo([-width/2, length/2], %, $edge3)
/// |> close(%, $edge4)
///
/// const mountingPlate = extrude(thickness, mountingPlateSketch)
/// mountingPlate = extrude(thickness, mountingPlateSketch)
/// |> fillet({
/// radius = filletRadius,
/// tolerance = 0.000001,
@ -119,6 +119,7 @@ pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// ```
#[stdlib {
name = "fillet",
feature_tree_operation = true,
}]
async fn inner_fillet(
data: FilletData,
@ -142,7 +143,7 @@ async fn inner_fillet(
for edge_tag in data.tags {
let edge_id = edge_tag.get_engine_id(exec_state, &args)?;
let id = exec_state.id_generator.next_uuid();
let id = exec_state.next_uuid();
args.batch_end_cmd(
id,
ModelingCmd::from(mcmd::Solid3dFilletEdge {
@ -195,7 +196,7 @@ pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result
/// Get the opposite edge to the edge given.
///
/// ```no_run
/// const exampleSketch = startSketchOn('XZ')
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line([10, 0], %)
/// |> angledLine({
@ -213,7 +214,7 @@ pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result
/// }, %, $referenceEdge)
/// |> close(%)
///
/// const example = extrude(5, exampleSketch)
/// example = extrude(5, exampleSketch)
/// |> fillet({
/// radius = 3,
/// tags = [getOppositeEdge(referenceEdge)],
@ -224,11 +225,11 @@ pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result
}]
async fn inner_get_opposite_edge(tag: TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<Uuid, KclError> {
if args.ctx.is_mock() {
return Ok(exec_state.id_generator.next_uuid());
return Ok(exec_state.next_uuid());
}
let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
let id = exec_state.id_generator.next_uuid();
let id = exec_state.next_uuid();
let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
let resp = args
@ -268,7 +269,7 @@ pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> R
/// Get the next adjacent edge to the edge given.
///
/// ```no_run
/// const exampleSketch = startSketchOn('XZ')
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line([10, 0], %)
/// |> angledLine({
@ -286,7 +287,7 @@ pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> R
/// }, %, $referenceEdge)
/// |> close(%)
///
/// const example = extrude(5, exampleSketch)
/// example = extrude(5, exampleSketch)
/// |> fillet({
/// radius = 3,
/// tags = [getNextAdjacentEdge(referenceEdge)],
@ -301,11 +302,11 @@ async fn inner_get_next_adjacent_edge(
args: Args,
) -> Result<Uuid, KclError> {
if args.ctx.is_mock() {
return Ok(exec_state.id_generator.next_uuid());
return Ok(exec_state.next_uuid());
}
let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
let id = exec_state.id_generator.next_uuid();
let id = exec_state.next_uuid();
let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
let resp = args
@ -353,7 +354,7 @@ pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args)
/// Get the previous adjacent edge to the edge given.
///
/// ```no_run
/// const exampleSketch = startSketchOn('XZ')
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line([10, 0], %)
/// |> angledLine({
@ -371,7 +372,7 @@ pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args)
/// }, %, $referenceEdge)
/// |> close(%)
///
/// const example = extrude(5, exampleSketch)
/// example = extrude(5, exampleSketch)
/// |> fillet({
/// radius = 3,
/// tags = [getPreviousAdjacentEdge(referenceEdge)],
@ -386,11 +387,11 @@ async fn inner_get_previous_adjacent_edge(
args: Args,
) -> Result<Uuid, KclError> {
if args.ctx.is_mock() {
return Ok(exec_state.id_generator.next_uuid());
return Ok(exec_state.next_uuid());
}
let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
let id = exec_state.id_generator.next_uuid();
let id = exec_state.next_uuid();
let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
let resp = args

View File

@ -42,7 +42,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// Create a helix on a cylinder.
///
/// ```no_run
/// const part001 = startSketchOn('XY')
/// part001 = startSketchOn('XY')
/// |> circle({ center: [5, 5], radius: 10 }, %)
/// |> extrude(10, %)
/// |> helix({
@ -53,6 +53,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// ```
#[stdlib {
name = "helix",
feature_tree_operation = true,
}]
async fn inner_helix(
data: HelixData,
@ -60,7 +61,7 @@ async fn inner_helix(
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
let id = exec_state.id_generator.next_uuid();
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::EntityMakeHelix {

View File

@ -148,23 +148,23 @@ pub async fn import(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// [KCL modules](/docs/kcl/modules).
///
/// ```no_run
/// const model = import("tests/inputs/cube.obj")
/// model = import("tests/inputs/cube.obj")
/// ```
///
/// ```no_run
/// const model = import("tests/inputs/cube.obj", {format: "obj", units: "m"})
/// model = import("tests/inputs/cube.obj", {format: "obj", units: "m"})
/// ```
///
/// ```no_run
/// const model = import("tests/inputs/cube.gltf")
/// model = import("tests/inputs/cube.gltf")
/// ```
///
/// ```no_run
/// const model = import("tests/inputs/cube.sldprt")
/// model = import("tests/inputs/cube.sldprt")
/// ```
///
/// ```no_run
/// const model = import("tests/inputs/cube.step")
/// model = import("tests/inputs/cube.step")
/// ```
///
/// ```no_run
@ -177,6 +177,7 @@ pub async fn import(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// ```
#[stdlib {
name = "import",
feature_tree_operation = true,
tags = [],
}]
async fn inner_import(
@ -299,13 +300,13 @@ async fn inner_import(
if args.ctx.is_mock() {
return Ok(ImportedGeometry {
id: exec_state.id_generator.next_uuid(),
id: exec_state.next_uuid(),
value: import_files.iter().map(|f| f.path.to_string()).collect(),
meta: vec![args.source_range.into()],
});
}
let id = exec_state.id_generator.next_uuid();
let id = exec_state.next_uuid();
let resp = args
.send_modeling_cmd(
id,

View File

@ -1,11 +1,11 @@
//! Standard library lofts.
use std::num::NonZeroU32;
use anyhow::Result;
use derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
@ -15,45 +15,31 @@ use crate::{
const DEFAULT_V_DEGREE: u32 = 2;
/// Data for a loft.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct LoftData {
/// Degree of the interpolation. Must be greater than zero.
/// For example, use 2 for quadratic, or 3 for cubic interpolation in the V direction.
/// This defaults to 2, if not specified.
pub v_degree: Option<std::num::NonZeroU32>,
/// Attempt to approximate rational curves (such as arcs) using a bezier.
/// This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios
/// Over time, this field won't be necessary.
#[serde(default)]
pub bez_approximate_rational: Option<bool>,
/// This can be set to override the automatically determined topological base curve, which is usually the first section encountered.
#[serde(default)]
pub base_curve_index: Option<u32>,
/// Tolerance for the loft operation.
#[serde(default)]
pub tolerance: Option<f64>,
}
impl Default for LoftData {
fn default() -> Self {
Self {
// This unwrap is safe because the default value is always greater than zero.
v_degree: Some(std::num::NonZeroU32::new(DEFAULT_V_DEGREE).unwrap()),
bez_approximate_rational: None,
base_curve_index: None,
tolerance: None,
}
}
}
/// Create a 3D surface or solid by interpolating between two or more sketches.
pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (sketches, data): (Vec<Sketch>, Option<LoftData>) = args.get_sketches_and_data()?;
let sketches = args.get_unlabeled_kw_arg("sketches")?;
let v_degree: NonZeroU32 = args
.get_kw_arg_opt("vDegree")
.unwrap_or(NonZeroU32::new(DEFAULT_V_DEGREE).unwrap());
// Attempt to approximate rational curves (such as arcs) using a bezier.
// This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios
// Over time, this field won't be necessary.
let bez_approximate_rational = args.get_kw_arg_opt("bezApproximateRational").unwrap_or(false);
// This can be set to override the automatically determined topological base curve, which is usually the first section encountered.
let base_curve_index: Option<u32> = args.get_kw_arg_opt("baseCurveIndex");
// Tolerance for the loft operation.
let tolerance: Option<f64> = args.get_kw_arg_opt("tolerance");
let solid = inner_loft(sketches, data, exec_state, args).await?;
let solid = inner_loft(
sketches,
v_degree,
bez_approximate_rational,
base_curve_index,
tolerance,
exec_state,
args,
)
.await?;
Ok(KclValue::Solid(solid))
}
@ -63,7 +49,7 @@ pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
///
/// ```no_run
/// // Loft a square and a triangle.
/// const squareSketch = startSketchOn('XY')
/// squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
@ -71,7 +57,7 @@ pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const triangleSketch = startSketchOn(offsetPlane('XY', 75))
/// triangleSketch = startSketchOn(offsetPlane('XY', 75))
/// |> startProfileAt([0, 125], %)
/// |> line([-15, -30], %)
/// |> line([30, 0], %)
@ -83,7 +69,7 @@ pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
///
/// ```no_run
/// // Loft a square, a circle, and another circle.
/// const squareSketch = startSketchOn('XY')
/// squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
@ -91,10 +77,10 @@ pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch0 = startSketchOn(offsetPlane('XY', 75))
/// circleSketch0 = startSketchOn(offsetPlane('XY', 75))
/// |> circle({ center = [0, 100], radius = 50 }, %)
///
/// const circleSketch1 = startSketchOn(offsetPlane('XY', 150))
/// circleSketch1 = startSketchOn(offsetPlane('XY', 150))
/// |> circle({ center = [0, 100], radius = 20 }, %)
///
/// loft([squareSketch, circleSketch0, circleSketch1])
@ -102,7 +88,7 @@ pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
///
/// ```no_run
/// // Loft a square, a circle, and another circle with options.
/// const squareSketch = startSketchOn('XY')
/// squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line([200, 0], %)
/// |> line([0, -200], %)
@ -110,34 +96,38 @@ pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const circleSketch0 = startSketchOn(offsetPlane('XY', 75))
/// circleSketch0 = startSketchOn(offsetPlane('XY', 75))
/// |> circle({ center = [0, 100], radius = 50 }, %)
///
/// const circleSketch1 = startSketchOn(offsetPlane('XY', 150))
/// circleSketch1 = startSketchOn(offsetPlane('XY', 150))
/// |> circle({ center = [0, 100], radius = 20 }, %)
///
/// loft([squareSketch, circleSketch0, circleSketch1], {
/// // This can be set to override the automatically determined
/// // topological base curve, which is usually the first section encountered.
/// loft([squareSketch, circleSketch0, circleSketch1],
/// baseCurveIndex = 0,
/// // Attempt to approximate rational curves (such as arcs) using a bezier.
/// // This will remove banding around interpolations between arcs and non-arcs.
/// // It may produce errors in other scenarios Over time, this field won't be necessary.
/// bezApproximateRational = false,
/// // Tolerance for the loft operation.
/// tolerance = 0.000001,
/// // Degree of the interpolation. Must be greater than zero.
/// // For example, use 2 for quadratic, or 3 for cubic interpolation in
/// // the V direction. This defaults to 2, if not specified.
/// vDegree = 2,
/// })
/// )
/// ```
#[stdlib {
name = "loft",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
arg_docs = {
sketches = "Which sketches to loft. Must include at least 2 sketches.",
v_degree = "Degree of the interpolation. Must be greater than zero. For example, use 2 for quadratic, or 3 for cubic interpolation in the V direction. This defaults to 2, if not specified.",
bez_approximate_rational = "Attempt to approximate rational curves (such as arcs) using a bezier. This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios Over time, this field won't be necessary.",
base_curve_index = "This can be set to override the automatically determined topological base curve, which is usually the first section encountered.",
tolerance = "Tolerance for the loft operation.",
}
}]
async fn inner_loft(
sketches: Vec<Sketch>,
data: Option<LoftData>,
v_degree: NonZeroU32,
bez_approximate_rational: bool,
base_curve_index: Option<u32>,
tolerance: Option<f64>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
@ -152,20 +142,15 @@ async fn inner_loft(
}));
}
// Get the loft data.
let data = data.unwrap_or_default();
let id = exec_state.id_generator.next_uuid();
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::Loft {
section_ids: sketches.iter().map(|group| group.id).collect(),
base_curve_index: data.base_curve_index,
bez_approximate_rational: data.bez_approximate_rational.unwrap_or(false),
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
v_degree: data
.v_degree
.unwrap_or_else(|| std::num::NonZeroU32::new(DEFAULT_V_DEGREE).unwrap()),
base_curve_index,
bez_approximate_rational,
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
v_degree,
}),
)
.await?;

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