Compare commits
22 Commits
import-fix
...
kcl-0.2.22
Author | SHA1 | Date | |
---|---|---|---|
0255fde5fe | |||
ebade29ed0 | |||
582d37e51b | |||
4ef9429842 | |||
0577b6a984 | |||
7d44de0c12 | |||
f7d5313588 | |||
bd4783e885 | |||
8794696b26 | |||
1c2e415c70 | |||
248ef8ebb3 | |||
fbac9935fe | |||
b4c171a347 | |||
0811d9fa4e | |||
1efc2b9762 | |||
d361bda180 | |||
1d3ade114f | |||
3382b66075 | |||
5e8b5c254d | |||
b99b2d9a96 | |||
81041661c7 | |||
9d99b5be7f |
13
.github/workflows/static-analysis.yml
vendored
@ -37,10 +37,6 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
- run: yarn install
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- run: yarn build:wasm
|
||||
|
||||
yarn-tsc:
|
||||
@ -70,10 +66,6 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
- run: yarn install
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- run: yarn lint
|
||||
|
||||
python-codespell:
|
||||
@ -101,11 +93,6 @@ jobs:
|
||||
cache: 'yarn'
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- run: yarn build:wasm
|
||||
|
||||
- run: yarn simpleserver:bg
|
||||
|
@ -36,7 +36,7 @@ exampleSketch = startSketchOn('XZ')
|
||||
|> close(%)
|
||||
|> patternCircular2d({
|
||||
center: [0, 0],
|
||||
repetitions: 12,
|
||||
instances: 13,
|
||||
arcDegrees: 360,
|
||||
rotateDuplicates: true
|
||||
}, %)
|
||||
|
@ -35,7 +35,7 @@ example = extrude(-5, exampleSketch)
|
||||
|> patternCircular3d({
|
||||
axis: [1, -1, 0],
|
||||
center: [10, -20, 0],
|
||||
repetitions: 10,
|
||||
instances: 11,
|
||||
arcDegrees: 360,
|
||||
rotateDuplicates: true
|
||||
}, %)
|
||||
|
@ -32,7 +32,7 @@ exampleSketch = startSketchOn('XZ')
|
||||
|> circle({ center: [0, 0], radius: 1 }, %)
|
||||
|> patternLinear2d({
|
||||
axis: [1, 0],
|
||||
repetitions: 6,
|
||||
instances: 7,
|
||||
distance: 4
|
||||
}, %)
|
||||
|
||||
|
@ -38,7 +38,7 @@ exampleSketch = startSketchOn('XZ')
|
||||
example = extrude(1, exampleSketch)
|
||||
|> patternLinear3d({
|
||||
axis: [1, 0, 1],
|
||||
repetitions: 6,
|
||||
instances: 7,
|
||||
distance: 6
|
||||
}, %)
|
||||
```
|
||||
|
@ -32,7 +32,7 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
|
||||
fn decagon = (radius) => {
|
||||
step = 1 / 10 * tau()
|
||||
sketch001 = startSketchAt([cos(0) * radius, sin(0) * radius])
|
||||
return reduce([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], sketch001, (i, sg) => {
|
||||
return reduce([1..10], sketch001, (i, sg) => {
|
||||
x = cos(step * i) * radius
|
||||
y = sin(step * i) * radius
|
||||
return lineTo([x, y], sg)
|
||||
|
1452
docs/kcl/std.json
@ -82,6 +82,78 @@ Raise a number to a power.
|
||||
|
||||
|
||||
|
||||
----
|
||||
Are two numbers equal?
|
||||
|
||||
**enum:** `==`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Are two numbers not equal?
|
||||
|
||||
**enum:** `!=`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Is left greater than right
|
||||
|
||||
**enum:** `>`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Is left greater than or equal to right
|
||||
|
||||
**enum:** `>=`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Is left less than right
|
||||
|
||||
**enum:** `<`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Is left less than or equal to right
|
||||
|
||||
**enum:** `<=`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
@ -18,6 +18,27 @@ layout: manual
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `ImportStatement`| | No |
|
||||
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
||||
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
||||
| `items` |`[` [`ImportItem`](/docs/kcl/types/ImportItem) `]`| | No |
|
||||
| `path` |`string`| | No |
|
||||
| `raw_path` |`string`| | No |
|
||||
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
@ -45,6 +66,7 @@ layout: manual
|
||||
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
||||
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
||||
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
|
||||
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
|
||||
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | No |
|
||||
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
|
||||
|
||||
|
@ -16,7 +16,7 @@ Data for a circular pattern on a 2D sketch.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
|
||||
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
|
||||
| `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | No |
|
||||
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
|
||||
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |
|
||||
|
@ -16,7 +16,7 @@ Data for a circular pattern on a 3D model.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
|
||||
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
|
||||
| `axis` |`[number, number, number]`| The axis around which to make the pattern. This is a 3D vector. | No |
|
||||
| `center` |`[number, number, number]`| The center about which to make the pattern. This is a 3D vector. | No |
|
||||
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
|
||||
|
@ -197,6 +197,27 @@ An expression can be evaluated to yield a single KCL value.
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `ArrayRangeExpression`| | No |
|
||||
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
||||
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
||||
| `startElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
|
||||
| `endElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
|
||||
| `endInclusive` |`boolean`| Is the `end_element` included in the range? | No |
|
||||
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|
24
docs/kcl/types/ImportItem.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
title: "ImportItem"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `name` |[`Identifier`](/docs/kcl/types/Identifier)| Name of the item to import. | No |
|
||||
| `alias` |[`Identifier`](/docs/kcl/types/Identifier)| Rename the item using an identifier after "as". | No |
|
||||
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
||||
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
||||
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
|
||||
|
||||
|
16
docs/kcl/types/ItemVisibility.md
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
title: "ItemVisibility"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
**enum:** `default`, `export`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -16,7 +16,7 @@ Data for a linear pattern on a 2D sketch.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
|
||||
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
|
||||
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No |
|
||||
| `axis` |`[number, number]`| The axis of the pattern. This is a 2D vector. | No |
|
||||
|
||||
|
@ -16,7 +16,7 @@ Data for a linear pattern on a 3D model.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
|
||||
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
|
||||
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No |
|
||||
| `axis` |`[number, number, number]`| The axis of the pattern. | No |
|
||||
|
||||
|
@ -669,6 +669,7 @@ test.describe(
|
||||
// screen shot should show the sketch
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
})
|
||||
|
||||
// exit sketch
|
||||
@ -686,6 +687,7 @@ test.describe(
|
||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
2
interface.d.ts
vendored
@ -73,7 +73,7 @@ export interface IElectronAPI {
|
||||
callback: (value: { version: string }) => void
|
||||
) => Electron.IpcRenderer
|
||||
onUpdateDownloaded: (
|
||||
callback: (value: string) => void
|
||||
callback: (value: { version: string; releaseNotes: string }) => void
|
||||
) => Electron.IpcRenderer
|
||||
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
|
||||
appRestart: () => void
|
||||
|
@ -425,6 +425,34 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/metrics": {
|
||||
"get": {
|
||||
"operationId": "get_metrics",
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "String",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "successful operation"
|
||||
},
|
||||
"4XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"5XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
}
|
||||
},
|
||||
"summary": "List available machines and their statuses",
|
||||
"tags": [
|
||||
"hidden"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ping": {
|
||||
"get": {
|
||||
"operationId": "ping",
|
||||
@ -492,6 +520,13 @@
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"description": "Hidden API endpoints that should not show up in the docs.",
|
||||
"externalDocs": {
|
||||
"url": "https://docs.zoo.dev/api/machines"
|
||||
},
|
||||
"name": "hidden"
|
||||
},
|
||||
{
|
||||
"description": "Utilities for making parts and discovering machines.",
|
||||
"externalDocs": {
|
||||
|
153
src/components/ToastUpdate.test.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { vi } from 'vitest'
|
||||
import { ToastUpdate } from './ToastUpdate'
|
||||
|
||||
describe('ToastUpdate tests', () => {
|
||||
const testData = {
|
||||
version: '0.255.255',
|
||||
files: [
|
||||
{
|
||||
url: 'Zoo Modeling App-0.255.255-x64-mac.zip',
|
||||
sha512:
|
||||
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
|
||||
size: 141277345,
|
||||
},
|
||||
{
|
||||
url: 'Zoo Modeling App-0.255.255-arm64-mac.zip',
|
||||
sha512:
|
||||
'b+ugdg7A4LhYYJaFkPRxh1RvmGGMlPJJj7inkLg9PwRtCnR9ePMlktj2VRciXF1iLh59XW4bLc4dK1dFQHMULA==',
|
||||
size: 135278259,
|
||||
},
|
||||
{
|
||||
url: 'Zoo Modeling App-0.255.255-x64-mac.dmg',
|
||||
sha512:
|
||||
'gCUqww05yj8OYwPiTq6bo5GbkpngSbXGtenmDD7+kUm0UyVK8WD3dMAfQJtGNG5HY23aHCHe9myE2W4mbZGmiQ==',
|
||||
size: 146004232,
|
||||
},
|
||||
{
|
||||
url: 'Zoo Modeling App-0.255.255-arm64-mac.dmg',
|
||||
sha512:
|
||||
'ND871ayf81F1ZT+iWVLYTc2jdf/Py6KThuxX2QFWz14ebmIbJPL07lNtxQOexOFiuk0MwRhlCy1RzOSG1b9bmw==',
|
||||
size: 140021522,
|
||||
},
|
||||
],
|
||||
path: 'Zoo Modeling App-0.255.255-x64-mac.zip',
|
||||
sha512:
|
||||
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
|
||||
releaseNotes:
|
||||
'## Some markdown release notes\n\n- This is a list item\n- This is another list item\n\n```javascript\nconsole.log("Hello, world!")\n```\n',
|
||||
releaseDate: '2024-10-09T11:57:59.133Z',
|
||||
} as const
|
||||
|
||||
test('Happy path: renders the toast with good data', () => {
|
||||
const onRestart = vi.fn()
|
||||
const onDismiss = vi.fn()
|
||||
|
||||
render(
|
||||
<ToastUpdate
|
||||
onRestart={onRestart}
|
||||
onDismiss={onDismiss}
|
||||
version={testData.version}
|
||||
releaseNotes={testData.releaseNotes}
|
||||
/>
|
||||
)
|
||||
|
||||
// Locators and other constants
|
||||
const versionText = screen.getByTestId('update-version')
|
||||
const restartButton = screen.getByRole('button', { name: /restart/i })
|
||||
const dismissButton = screen.getByRole('button', { name: /got it/i })
|
||||
const releaseNotes = screen.getByTestId('release-notes')
|
||||
|
||||
expect(versionText).toBeVisible()
|
||||
expect(versionText).toHaveTextContent(testData.version)
|
||||
|
||||
expect(restartButton).toBeEnabled()
|
||||
fireEvent.click(restartButton)
|
||||
expect(onRestart.mock.calls).toHaveLength(1)
|
||||
|
||||
expect(dismissButton).toBeEnabled()
|
||||
fireEvent.click(dismissButton)
|
||||
expect(onDismiss.mock.calls).toHaveLength(1)
|
||||
|
||||
// I cannot for the life of me seem to get @testing-library/react
|
||||
// to properly handle click events or visibility checks on the details element.
|
||||
// So I'm only checking that the content is in the document.
|
||||
expect(releaseNotes).toBeInTheDocument()
|
||||
expect(releaseNotes).toHaveTextContent('Release notes')
|
||||
const releaseNotesListItems = screen.getAllByRole('listitem')
|
||||
expect(releaseNotesListItems.map((el) => el.textContent)).toEqual([
|
||||
'This is a list item',
|
||||
'This is another list item',
|
||||
])
|
||||
})
|
||||
|
||||
test('Happy path: renders the breaking changes notice', () => {
|
||||
const releaseNotesWithBreakingChanges = `
|
||||
## Some markdown release notes
|
||||
- This is a list item
|
||||
- This is another list item with a breaking change
|
||||
- This is a list item
|
||||
`
|
||||
const onRestart = vi.fn()
|
||||
const onDismiss = vi.fn()
|
||||
|
||||
render(
|
||||
<ToastUpdate
|
||||
onRestart={onRestart}
|
||||
onDismiss={onDismiss}
|
||||
version={testData.version}
|
||||
releaseNotes={releaseNotesWithBreakingChanges}
|
||||
/>
|
||||
)
|
||||
|
||||
// Locators and other constants
|
||||
const releaseNotes = screen.getByText('Release notes', {
|
||||
selector: 'summary',
|
||||
})
|
||||
const listItemContents = screen
|
||||
.getAllByRole('listitem')
|
||||
.map((el) => el.textContent)
|
||||
|
||||
// I cannot for the life of me seem to get @testing-library/react
|
||||
// to properly handle click events or visibility checks on the details element.
|
||||
// So I'm only checking that the content is in the document.
|
||||
expect(releaseNotes).toBeInTheDocument()
|
||||
expect(listItemContents).toEqual([
|
||||
'This is a list item',
|
||||
'This is another list item with a breaking change',
|
||||
'This is a list item',
|
||||
])
|
||||
})
|
||||
|
||||
test('Missing release notes: renders the toast without release notes', () => {
|
||||
const onRestart = vi.fn()
|
||||
const onDismiss = vi.fn()
|
||||
|
||||
render(
|
||||
<ToastUpdate
|
||||
onRestart={onRestart}
|
||||
onDismiss={onDismiss}
|
||||
version={testData.version}
|
||||
releaseNotes={''}
|
||||
/>
|
||||
)
|
||||
|
||||
// Locators and other constants
|
||||
const versionText = screen.getByTestId('update-version')
|
||||
const restartButton = screen.getByRole('button', { name: /restart/i })
|
||||
const dismissButton = screen.getByRole('button', { name: /got it/i })
|
||||
const releaseNotes = screen.queryByText(/release notes/i, {
|
||||
selector: 'details > summary',
|
||||
})
|
||||
const releaseNotesListItem = screen.queryByRole('listitem', {
|
||||
name: /this is a list item/i,
|
||||
})
|
||||
|
||||
expect(versionText).toBeVisible()
|
||||
expect(versionText).toHaveTextContent(testData.version)
|
||||
expect(releaseNotes).not.toBeInTheDocument()
|
||||
expect(releaseNotesListItem).not.toBeInTheDocument()
|
||||
expect(restartButton).toBeEnabled()
|
||||
expect(dismissButton).toBeEnabled()
|
||||
})
|
||||
})
|
@ -1,14 +1,23 @@
|
||||
import toast from 'react-hot-toast'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { Marked } from '@ts-stack/markdown'
|
||||
|
||||
export function ToastUpdate({
|
||||
version,
|
||||
releaseNotes,
|
||||
onRestart,
|
||||
onDismiss,
|
||||
}: {
|
||||
version: string
|
||||
releaseNotes?: string
|
||||
onRestart: () => void
|
||||
onDismiss: () => void
|
||||
}) {
|
||||
const containsBreakingChanges = releaseNotes
|
||||
?.toLocaleLowerCase()
|
||||
.includes('breaking')
|
||||
|
||||
return (
|
||||
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
|
||||
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||
@ -19,7 +28,7 @@ export function ToastUpdate({
|
||||
>
|
||||
v{version}
|
||||
</span>
|
||||
<span className="ml-4 text-md text-bold">
|
||||
<p className="ml-4 text-md text-bold">
|
||||
A new update has downloaded and will be available next time you
|
||||
start the app. You can view the release notes{' '}
|
||||
<a
|
||||
@ -32,15 +41,39 @@ export function ToastUpdate({
|
||||
>
|
||||
here on GitHub.
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{releaseNotes && (
|
||||
<details
|
||||
className="my-4 border border-chalkboard-30 dark:border-chalkboard-60 rounded"
|
||||
open={containsBreakingChanges}
|
||||
data-testid="release-notes"
|
||||
>
|
||||
<summary className="p-2 select-none cursor-pointer">
|
||||
Release notes
|
||||
{containsBreakingChanges && (
|
||||
<strong className="text-destroy-50"> (Breaking changes)</strong>
|
||||
)}
|
||||
</summary>
|
||||
<div
|
||||
className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Marked.parse(releaseNotes, {
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
sanitize: true,
|
||||
}),
|
||||
}}
|
||||
></div>
|
||||
</details>
|
||||
)}
|
||||
<div className="flex justify-between gap-8">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{
|
||||
icon: 'arrowRotateRight',
|
||||
}}
|
||||
name="Restart app now"
|
||||
name="restart"
|
||||
onClick={onRestart}
|
||||
>
|
||||
Restart app now
|
||||
@ -50,9 +83,10 @@ export function ToastUpdate({
|
||||
iconStart={{
|
||||
icon: 'checkmark',
|
||||
}}
|
||||
name="Got it"
|
||||
name="dismiss"
|
||||
onClick={() => {
|
||||
toast.dismiss()
|
||||
onDismiss()
|
||||
}}
|
||||
>
|
||||
Got it
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight'
|
||||
|
||||
export const kclHighlight = styleTags({
|
||||
'import export': t.moduleKeyword,
|
||||
ImportItemAs: t.definitionKeyword,
|
||||
ImportFrom: t.moduleKeyword,
|
||||
'fn var let const': t.definitionKeyword,
|
||||
'if else': t.controlKeyword,
|
||||
return: t.controlKeyword,
|
||||
|
@ -15,8 +15,9 @@
|
||||
}
|
||||
|
||||
statement[@isGroup=Statement] {
|
||||
FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
|
||||
VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
|
||||
ImportStatement { kw<"import"> ImportItems ImportFrom String } |
|
||||
FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
|
||||
VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
|
||||
ReturnStatement { kw<"return"> expression } |
|
||||
ExpressionStatement { expression }
|
||||
}
|
||||
@ -25,6 +26,9 @@ ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")"
|
||||
|
||||
Body { "{" statement* "}" }
|
||||
|
||||
ImportItems { commaSep1NoTrailingComma<ImportItem> }
|
||||
ImportItem { identifier (ImportItemAs identifier)? }
|
||||
|
||||
expression[@isGroup=Expression] {
|
||||
String |
|
||||
Number |
|
||||
@ -74,6 +78,8 @@ kw<term> { @specialize[@name={term}]<identifier, term> }
|
||||
|
||||
commaSep<term> { (term ("," term)*)? ","? }
|
||||
|
||||
commaSep1NoTrailingComma<term> { term ("," term)* }
|
||||
|
||||
@tokens {
|
||||
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
|
||||
|
||||
@ -106,6 +112,9 @@ commaSep<term> { (term ("," term)*)? ","? }
|
||||
|
||||
Shebang { "#!" ![\n]* }
|
||||
|
||||
ImportItemAs { "as" }
|
||||
ImportFrom { "from" }
|
||||
|
||||
"(" ")"
|
||||
"{" "}"
|
||||
"[" "]"
|
||||
|
@ -293,6 +293,24 @@ code {
|
||||
which lets you use them with @apply in your CSS, and get
|
||||
autocomplete in classNames in your JSX.
|
||||
*/
|
||||
.parsed-markdown ul,
|
||||
.parsed-markdown ol {
|
||||
@apply list-outside pl-4 lg:pl-8 my-2;
|
||||
}
|
||||
|
||||
.parsed-markdown ul li {
|
||||
@apply list-disc;
|
||||
}
|
||||
|
||||
.parsed-markdown li p {
|
||||
@apply inline;
|
||||
}
|
||||
|
||||
.parsed-markdown code {
|
||||
@apply px-1 py-0.5 rounded-sm;
|
||||
@apply bg-chalkboard-20 text-chalkboard-80;
|
||||
@apply dark:bg-chalkboard-80 dark:text-chalkboard-30;
|
||||
}
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-scroller,
|
||||
|
@ -70,15 +70,17 @@ if (isDesktop()) {
|
||||
id: AUTO_UPDATER_TOAST_ID,
|
||||
})
|
||||
})
|
||||
window.electron.onUpdateDownloaded((version: string) => {
|
||||
window.electron.onUpdateDownloaded(({ version, releaseNotes }) => {
|
||||
const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
|
||||
console.log(message)
|
||||
toast.custom(
|
||||
ToastUpdate({
|
||||
version,
|
||||
releaseNotes,
|
||||
onRestart: () => {
|
||||
window.electron.appRestart()
|
||||
},
|
||||
onDismiss: () => {},
|
||||
}),
|
||||
{ duration: 30000, id: AUTO_UPDATER_TOAST_ID }
|
||||
)
|
||||
|
@ -501,6 +501,7 @@ export function sketchOnExtrudedFace(
|
||||
createIdentifier(extrudeName ? extrudeName : oldSketchName),
|
||||
_tag,
|
||||
]),
|
||||
undefined,
|
||||
'const'
|
||||
)
|
||||
|
||||
@ -682,6 +683,7 @@ export function createPipeExpression(
|
||||
export function createVariableDeclaration(
|
||||
varName: string,
|
||||
init: VariableDeclarator['init'],
|
||||
visibility: VariableDeclaration['visibility'] = 'default',
|
||||
kind: VariableDeclaration['kind'] = 'const'
|
||||
): VariableDeclaration {
|
||||
return {
|
||||
@ -699,6 +701,7 @@ export function createVariableDeclaration(
|
||||
init,
|
||||
},
|
||||
],
|
||||
visibility,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
@ -620,7 +620,7 @@ describe('Testing button states', () => {
|
||||
it('should return true when body exists and segment is selected', async () => {
|
||||
await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true)
|
||||
})
|
||||
it('hould return false when body exists and not a segment is selected', async () => {
|
||||
it('should return false when body exists and not a segment is selected', async () => {
|
||||
await runButtonStateTest(codeWithBody, `close(%)`, false)
|
||||
})
|
||||
})
|
||||
|
@ -1,5 +1,7 @@
|
||||
import {
|
||||
CallExpression,
|
||||
Expr,
|
||||
Identifier,
|
||||
ObjectExpression,
|
||||
PathToNode,
|
||||
Program,
|
||||
@ -27,7 +29,7 @@ import {
|
||||
sketchLineHelperMap,
|
||||
} from '../std/sketch'
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { Selections, canFilletSelection } from 'lib/selections'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { KclCommandValue } from 'lib/commandTypes'
|
||||
import {
|
||||
ArtifactGraph,
|
||||
@ -66,7 +68,10 @@ export function modifyAstCloneWithFilletAndTag(
|
||||
const artifactGraph = engineCommandManager.artifactGraph
|
||||
|
||||
// Step 1: modify ast with tags and group them by extrude nodes (bodies)
|
||||
const extrudeToTagsMap: Map<PathToNode, string[]> = new Map()
|
||||
const extrudeToTagsMap: Map<
|
||||
PathToNode,
|
||||
Array<{ tag: string; selectionType: string }>
|
||||
> = new Map()
|
||||
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
||||
|
||||
for (const selectionRange of selection.codeBasedSelections) {
|
||||
@ -74,6 +79,7 @@ export function modifyAstCloneWithFilletAndTag(
|
||||
codeBasedSelections: [selectionRange],
|
||||
otherSelections: [],
|
||||
}
|
||||
const selectionType = singleSelection.codeBasedSelections[0].type
|
||||
|
||||
const result = getPathToExtrudeForSegmentSelection(
|
||||
clonedAstForGetExtrude,
|
||||
@ -89,6 +95,7 @@ export function modifyAstCloneWithFilletAndTag(
|
||||
)
|
||||
if (err(tagResult)) return tagResult
|
||||
const { tag } = tagResult
|
||||
const tagInfo = { tag, selectionType }
|
||||
|
||||
// Group tags by their corresponding extrude node
|
||||
const extrudeKey = JSON.stringify(pathToExtrudeNode)
|
||||
@ -96,23 +103,29 @@ export function modifyAstCloneWithFilletAndTag(
|
||||
if (lookupMap.has(extrudeKey)) {
|
||||
const existingPath = lookupMap.get(extrudeKey)
|
||||
if (!existingPath) return new Error('Path to extrude node not found.')
|
||||
extrudeToTagsMap.get(existingPath)?.push(tag)
|
||||
extrudeToTagsMap.get(existingPath)?.push(tagInfo)
|
||||
} else {
|
||||
lookupMap.set(extrudeKey, pathToExtrudeNode)
|
||||
extrudeToTagsMap.set(pathToExtrudeNode, [tag])
|
||||
extrudeToTagsMap.set(pathToExtrudeNode, [tagInfo])
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Apply fillet(s) for each extrude node (body)
|
||||
let pathToFilletNodes: Array<PathToNode> = []
|
||||
for (const [pathToExtrudeNode, tags] of extrudeToTagsMap.entries()) {
|
||||
for (const [pathToExtrudeNode, tagInfos] of extrudeToTagsMap.entries()) {
|
||||
// Create a fillet expression with multiple tags
|
||||
const radiusValue =
|
||||
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
|
||||
|
||||
const tagCalls = tagInfos.map(({ tag, selectionType }) => {
|
||||
return getEdgeTagCall(tag, selectionType)
|
||||
})
|
||||
const firstTag = tagCalls[0] // can be Identifier or CallExpression (for opposite and adjacent edges)
|
||||
|
||||
const filletCall = createCallExpressionStdLib('fillet', [
|
||||
createObjectExpression({
|
||||
radius: radiusValue,
|
||||
tags: createArrayExpression(tags.map((tag) => createIdentifier(tag))),
|
||||
tags: createArrayExpression(tagCalls),
|
||||
}),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
@ -144,7 +157,7 @@ export function modifyAstCloneWithFilletAndTag(
|
||||
pathToFilletNode = getPathToNodeOfFilletLiteral(
|
||||
pathToExtrudeNode,
|
||||
extrudeDeclarator,
|
||||
tags[0]
|
||||
firstTag
|
||||
)
|
||||
pathToFilletNodes.push(pathToFilletNode)
|
||||
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
|
||||
@ -165,7 +178,7 @@ export function modifyAstCloneWithFilletAndTag(
|
||||
pathToFilletNode = getPathToNodeOfFilletLiteral(
|
||||
pathToExtrudeNode,
|
||||
extrudeDeclarator,
|
||||
tags[0]
|
||||
firstTag
|
||||
)
|
||||
pathToFilletNodes.push(pathToFilletNode)
|
||||
} else {
|
||||
@ -276,6 +289,21 @@ function mutateAstWithTagForSketchSegment(
|
||||
return { modifiedAst: astClone, tag }
|
||||
}
|
||||
|
||||
function getEdgeTagCall(
|
||||
tag: string,
|
||||
selectionType: string
|
||||
): Identifier | CallExpression {
|
||||
let tagCall: Expr = createIdentifier(tag)
|
||||
|
||||
// Modify the tag based on selectionType
|
||||
if (selectionType === 'edge') {
|
||||
tagCall = createCallExpressionStdLib('getOppositeEdge', [tagCall])
|
||||
} else if (selectionType === 'adjacent-edge') {
|
||||
tagCall = createCallExpressionStdLib('getNextAdjacentEdge', [tagCall])
|
||||
}
|
||||
return tagCall
|
||||
}
|
||||
|
||||
function locateExtrudeDeclarator(
|
||||
node: Program,
|
||||
pathToExtrudeNode: PathToNode
|
||||
@ -311,7 +339,7 @@ function locateExtrudeDeclarator(
|
||||
function getPathToNodeOfFilletLiteral(
|
||||
pathToExtrudeNode: PathToNode,
|
||||
extrudeDeclarator: VariableDeclarator,
|
||||
tag: string
|
||||
tag: Identifier | CallExpression
|
||||
): PathToNode {
|
||||
let pathToFilletObj: PathToNode = []
|
||||
let inFillet = false
|
||||
@ -347,12 +375,30 @@ function getPathToNodeOfFilletLiteral(
|
||||
]
|
||||
}
|
||||
|
||||
function hasTag(node: ObjectExpression, tag: string): boolean {
|
||||
function hasTag(
|
||||
node: ObjectExpression,
|
||||
tag: Identifier | CallExpression
|
||||
): boolean {
|
||||
return node.properties.some((prop) => {
|
||||
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
|
||||
return prop.value.elements.some(
|
||||
(element) => element.type === 'Identifier' && element.name === tag
|
||||
)
|
||||
// if selection is a base edge:
|
||||
if (tag.type === 'Identifier') {
|
||||
return prop.value.elements.some(
|
||||
(element) =>
|
||||
element.type === 'Identifier' && element.name === tag.name
|
||||
)
|
||||
}
|
||||
// if selection is an adjacent or opposite edge:
|
||||
if (tag.type === 'CallExpression') {
|
||||
return prop.value.elements.some(
|
||||
(element) =>
|
||||
element.type === 'CallExpression' &&
|
||||
element.callee.name === tag.callee.name && // edge location
|
||||
element.arguments[0].type === 'Identifier' &&
|
||||
tag.arguments[0].type === 'Identifier' &&
|
||||
element.arguments[0].name === tag.arguments[0].name // tag name
|
||||
)
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
@ -383,7 +429,7 @@ export const hasValidFilletSelection = ({
|
||||
ast: Program
|
||||
code: string
|
||||
}) => {
|
||||
// case 0: check if there is anything filletable in the scene
|
||||
// check if there is anything filletable in the scene
|
||||
let extrudeExists = false
|
||||
traverse(ast, {
|
||||
enter(node) {
|
||||
@ -394,65 +440,88 @@ export const hasValidFilletSelection = ({
|
||||
})
|
||||
if (!extrudeExists) return false
|
||||
|
||||
// case 1: nothing selected, test whether the extrusion exists
|
||||
if (selectionRanges) {
|
||||
if (selectionRanges.codeBasedSelections.length === 0) {
|
||||
return true
|
||||
}
|
||||
const range0 = selectionRanges.codeBasedSelections[0].range[0]
|
||||
const codeLength = code.length
|
||||
if (range0 === codeLength) {
|
||||
return true
|
||||
}
|
||||
// check if nothing is selected
|
||||
if (selectionRanges.codeBasedSelections.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
// case 2: sketch segment selected, test whether it is extruded
|
||||
// TODO: add loft / sweep check
|
||||
if (selectionRanges.codeBasedSelections.length > 0) {
|
||||
const isExtruded = hasSketchPipeBeenExtruded(
|
||||
selectionRanges.codeBasedSelections[0],
|
||||
ast
|
||||
// check if selection is last string in code
|
||||
if (selectionRanges.codeBasedSelections[0].range[0] === code.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
// selection exists:
|
||||
for (const selection of selectionRanges.codeBasedSelections) {
|
||||
// check if all selections are in sketchLineHelperMap
|
||||
const path = getNodePathFromSourceRange(ast, selection.range)
|
||||
const segmentNode = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
path,
|
||||
'CallExpression'
|
||||
)
|
||||
if (isExtruded) {
|
||||
const pathToSelectedNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const segmentNode = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
pathToSelectedNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(segmentNode)) return false
|
||||
if (segmentNode.node.type === 'CallExpression') {
|
||||
const segmentName = segmentNode.node.callee.name
|
||||
if (segmentName in sketchLineHelperMap) {
|
||||
// Add check whether the tag exists at all:
|
||||
if (!(segmentNode.node.arguments.length === 3)) return true
|
||||
// If the tag exists, check if it is already filleted
|
||||
const edges = isTagUsedInFillet({
|
||||
ast,
|
||||
callExp: segmentNode.node,
|
||||
})
|
||||
// edge has already been filleted
|
||||
if (
|
||||
['edge', 'default'].includes(
|
||||
selectionRanges.codeBasedSelections[0].type
|
||||
) &&
|
||||
edges.includes('baseEdge')
|
||||
)
|
||||
return false
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (err(segmentNode)) return false
|
||||
if (segmentNode.node.type !== 'CallExpression') {
|
||||
return false
|
||||
}
|
||||
if (!(segmentNode.node.callee.name in sketchLineHelperMap)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return canFilletSelection(selectionRanges)
|
||||
// check if selection is extruded
|
||||
// TODO: option 1 : extrude is in the sketch pipe
|
||||
|
||||
// option 2: extrude is outside the sketch pipe
|
||||
const extrudeExists = hasSketchPipeBeenExtruded(selection, ast)
|
||||
if (err(extrudeExists)) {
|
||||
return false
|
||||
}
|
||||
if (!extrudeExists) {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if tag exists for the selection
|
||||
let tagExists = false
|
||||
let tag = ''
|
||||
traverse(segmentNode.node, {
|
||||
enter(node) {
|
||||
if (node.type === 'TagDeclarator') {
|
||||
tagExists = true
|
||||
tag = node.value
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// check if tag is used in fillet
|
||||
if (tagExists) {
|
||||
// create tag call
|
||||
let tagCall: Expr = getEdgeTagCall(tag, selection.type)
|
||||
|
||||
// check if tag is used in fillet
|
||||
let inFillet = false
|
||||
let tagUsedInFillet = false
|
||||
traverse(ast, {
|
||||
enter(node) {
|
||||
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
|
||||
inFillet = true
|
||||
}
|
||||
if (inFillet && node.type === 'ObjectExpression') {
|
||||
if (hasTag(node, tagCall)) {
|
||||
tagUsedInFillet = true
|
||||
}
|
||||
}
|
||||
},
|
||||
leave(node) {
|
||||
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
|
||||
inFillet = false
|
||||
}
|
||||
},
|
||||
})
|
||||
if (tagUsedInFillet) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type EdgeTypes =
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
getConstraintType,
|
||||
} from './std/sketchcombos'
|
||||
import { err } from 'lib/trap'
|
||||
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
|
||||
|
||||
/**
|
||||
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
|
||||
@ -120,7 +121,12 @@ export function getNodeFromPathCurry(
|
||||
}
|
||||
|
||||
function moreNodePathFromSourceRange(
|
||||
node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement,
|
||||
node:
|
||||
| Expr
|
||||
| ImportStatement
|
||||
| ExpressionStatement
|
||||
| VariableDeclaration
|
||||
| ReturnStatement,
|
||||
sourceRange: Selection['range'],
|
||||
previousPath: PathToNode = [['body', '']]
|
||||
): PathToNode {
|
||||
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 378 KiB |
Before Width: | Height: | Size: 577 KiB After Width: | Height: | Size: 613 KiB |
@ -426,6 +426,7 @@ export const _executor = async (
|
||||
baseUnit,
|
||||
engineCommandManager,
|
||||
fileSystemManager,
|
||||
undefined,
|
||||
isMock
|
||||
)
|
||||
return execStateFromRaw(execState)
|
||||
|
39
src/lib/machine-api.d.ts
vendored
@ -55,6 +55,23 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/metrics': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
/** List available machines and their statuses */
|
||||
get: operations['get_metrics']
|
||||
put?: never
|
||||
post?: never
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/ping': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@ -278,6 +295,28 @@ export interface operations {
|
||||
'5XX': components['responses']['Error']
|
||||
}
|
||||
}
|
||||
get_metrics: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody?: never
|
||||
responses: {
|
||||
/** @description successful operation */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': string
|
||||
}
|
||||
}
|
||||
'4XX': components['responses']['Error']
|
||||
'5XX': components['responses']['Error']
|
||||
}
|
||||
}
|
||||
ping: {
|
||||
parameters: {
|
||||
query?: never
|
||||
|
@ -287,7 +287,10 @@ app.on('ready', () => {
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
console.log('update-downloaded', info)
|
||||
mainWindow?.webContents.send('update-downloaded', info.version)
|
||||
mainWindow?.webContents.send('update-downloaded', {
|
||||
version: info.version,
|
||||
releaseNotes: info.releaseNotes,
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('app.restart', () => {
|
||||
|
@ -16,11 +16,12 @@ const startDeviceFlow = (host: string): Promise<string> =>
|
||||
ipcRenderer.invoke('startDeviceFlow', host)
|
||||
const loginWithDeviceFlow = (): Promise<string> =>
|
||||
ipcRenderer.invoke('loginWithDeviceFlow')
|
||||
const onUpdateDownloaded = (
|
||||
callback: (value: { version: string; releaseNotes: string }) => void
|
||||
) => ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
|
||||
const onUpdateDownloadStart = (
|
||||
callback: (value: { version: string }) => void
|
||||
) => ipcRenderer.on('update-download-start', (_event, value) => callback(value))
|
||||
const onUpdateDownloaded = (callback: (value: string) => void) =>
|
||||
ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
|
||||
const onUpdateError = (callback: (value: Error) => void) =>
|
||||
ipcRenderer.on('update-error', (_event, value) => callback(value))
|
||||
const appRestart = () => ipcRenderer.invoke('app.restart')
|
||||
|
64
src/wasm-lib/Cargo.lock
generated
@ -1394,9 +1394,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.2"
|
||||
version = "0.25.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
|
||||
checksum = "d97eb9a8e0cd5b76afea91d7eecd5cf8338cd44ced04256cf1f800474b227c52"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
@ -1533,16 +1533,16 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.71"
|
||||
version = "0.3.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cb94a0ffd3f3ee755c20f7d8752f45cac88605a4dcf808abcff72873296ec7b"
|
||||
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.2.20"
|
||||
version = "0.2.22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx 0.5.1",
|
||||
@ -1617,7 +1617,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hyper 0.14.30",
|
||||
@ -2337,18 +2337,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
version = "1.0.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.22.3"
|
||||
version = "0.22.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15ee168e30649f7f234c3d49ef5a7a6cbf5134289bc46c29ff3155fa3221c225"
|
||||
checksum = "3d922163ba1f79c04bc49073ba7b32fd5a8d3b76a87c955921234b8e77333c51"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"indoc",
|
||||
@ -2364,9 +2364,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.22.3"
|
||||
version = "0.22.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e61cef80755fe9e46bb8a0b8f20752ca7676dcc07a5277d8b7768c6172e529b3"
|
||||
checksum = "bc38c5feeb496c8321091edf3d63e9a6829eab4b863b4a6a65f26f3e9cc6b179"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"target-lexicon",
|
||||
@ -2374,9 +2374,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.22.3"
|
||||
version = "0.22.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67ce096073ec5405f5ee2b8b31f03a68e02aa10d5d4f565eca04acc41931fa1c"
|
||||
checksum = "94845622d88ae274d2729fcefc850e63d7a3ddff5e3ce11bd88486db9f1d357d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
@ -2384,9 +2384,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.22.3"
|
||||
version = "0.22.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2440c6d12bc8f3ae39f1e775266fa5122fd0c8891ce7520fa6048e683ad3de28"
|
||||
checksum = "e655aad15e09b94ffdb3ce3d217acf652e26bbc37697ef012f5e5e348c716e5e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
@ -2396,9 +2396,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.22.3"
|
||||
version = "0.22.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1be962f0e06da8f8465729ea2cb71a416d2257dff56cbe40a70d3e62a93ae5d1"
|
||||
checksum = "ae1e3f09eecd94618f60a455a23def79f79eba4dc561a97324bf9ac8c6df30ce"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@ -3829,9 +3829,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
@ -3907,9 +3907,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.94"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef073ced962d62984fb38a36e5fdc1a2b23c9e0e1fa0689bb97afa4202ef6887"
|
||||
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@ -3918,9 +3918,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.94"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4bfab14ef75323f4eb75fa52ee0a3fb59611977fd3240da19b2cf36ff85030e"
|
||||
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
@ -3946,9 +3946,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.94"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7bec9830f60924d9ceb3ef99d55c155be8afa76954edffbb5936ff4509474e7"
|
||||
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@ -3956,9 +3956,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.94"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c74f6e152a76a2ad448e223b0fc0b6b5747649c3d769cc6bf45737bf97d0ed6"
|
||||
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3969,9 +3969,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.94"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a42f6c679374623f295a8623adfe63d9284091245c3504bde47c17a3ce2777d9"
|
||||
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-lib"
|
||||
@ -4032,9 +4032,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.70"
|
||||
version = "0.3.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
|
||||
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
@ -18,31 +18,31 @@ kittycad.workspace = true
|
||||
serde_json = "1.0.128"
|
||||
tokio = { version = "1.40.0", features = ["sync"] }
|
||||
toml = "0.8.19"
|
||||
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
|
||||
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
|
||||
wasm-bindgen = "0.2.91"
|
||||
wasm-bindgen-futures = "0.4.44"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1"
|
||||
image = { version = "0.25.1", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25.3", default-features = false, features = ["png"] }
|
||||
kittycad = { workspace = true, default-features = true }
|
||||
kittycad-modeling-cmds = { workspace = true }
|
||||
pretty_assertions = "1.4.1"
|
||||
reqwest = { version = "0.12", default-features = false }
|
||||
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||
twenty-twenty = "0.8"
|
||||
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
|
||||
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.31"
|
||||
js-sys = "0.3.71"
|
||||
js-sys = "0.3.72"
|
||||
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
|
||||
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.69"
|
||||
version = "0.3.72"
|
||||
features = [
|
||||
"console",
|
||||
"HtmlTextAreaElement",
|
||||
|
@ -762,7 +762,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_someFn {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_someFn {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_show {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
@ -51,7 +51,7 @@ mod test_examples_show {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_show {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -18,7 +18,7 @@ mod test_examples_my_func {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
@ -52,7 +52,7 @@ mod test_examples_my_func {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -18,7 +18,7 @@ mod test_examples_line_to {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
@ -52,7 +52,7 @@ mod test_examples_line_to {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_min {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
@ -51,7 +51,7 @@ mod test_examples_min {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_show {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_import {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_import {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_import {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_show {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -17,7 +17,7 @@ mod test_examples_some_function {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -1,7 +1,7 @@
|
||||
extern crate alloc;
|
||||
use kcl_lib::ast::types::{
|
||||
BodyItem, Expr, Identifier, Literal, LiteralValue, NonCodeMeta, Program, VariableDeclaration, VariableDeclarator,
|
||||
VariableKind,
|
||||
BodyItem, Expr, Identifier, ItemVisibility, Literal, LiteralValue, NonCodeMeta, Program, VariableDeclaration,
|
||||
VariableDeclarator, VariableKind,
|
||||
};
|
||||
use kcl_macros::parse;
|
||||
use pretty_assertions::assert_eq;
|
||||
@ -33,6 +33,7 @@ fn basic() {
|
||||
})),
|
||||
digest: None,
|
||||
}],
|
||||
visibility: ItemVisibility::Default,
|
||||
kind: VariableKind::Const,
|
||||
digest: None,
|
||||
})],
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-test-server"
|
||||
description = "A test server for KCL"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
|
@ -178,7 +178,7 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
|
||||
// Let users know if the test is taking a long time.
|
||||
let (done_tx, done_rx) = oneshot::channel::<()>();
|
||||
let timer = time_until(done_rx);
|
||||
let snapshot = match state.execute_and_prepare_snapshot(&program, id_generator).await {
|
||||
let snapshot = match state.execute_and_prepare_snapshot(&program, id_generator, None).await {
|
||||
Ok(sn) => sn,
|
||||
Err(e) => return kcl_err(e),
|
||||
};
|
||||
|
@ -20,4 +20,4 @@ kcl-lib = { path = "../kcl" }
|
||||
kittycad = { workspace = true, features = ["clap"] }
|
||||
kittycad-modeling-cmds = { workspace = true }
|
||||
tokio = { version = "1.38", features = ["full", "time", "rt", "tracing"] }
|
||||
uuid = { version = "1.9.1", features = ["v4", "js", "serde"] }
|
||||
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
|
||||
|
@ -1,6 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use indexmap::IndexMap;
|
||||
use kcl_lib::{
|
||||
engine::ExecutionKind,
|
||||
errors::KclError,
|
||||
executor::{DefaultPlanes, IdGenerator},
|
||||
};
|
||||
@ -26,6 +27,7 @@ pub struct EngineConnection {
|
||||
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::executor::SourceRange)>>>,
|
||||
core_test: Arc<Mutex<String>>,
|
||||
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
|
||||
execution_kind: Arc<Mutex<ExecutionKind>>,
|
||||
}
|
||||
|
||||
impl EngineConnection {
|
||||
@ -39,6 +41,7 @@ impl EngineConnection {
|
||||
batch_end: Arc::new(Mutex::new(IndexMap::new())),
|
||||
core_test: result,
|
||||
default_planes: Default::default(),
|
||||
execution_kind: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -360,6 +363,18 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
|
||||
self.batch_end.clone()
|
||||
}
|
||||
|
||||
fn execution_kind(&self) -> ExecutionKind {
|
||||
let guard = self.execution_kind.lock().unwrap();
|
||||
*guard
|
||||
}
|
||||
|
||||
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
|
||||
let mut guard = self.execution_kind.lock().unwrap();
|
||||
let original = *guard;
|
||||
*guard = execution_kind;
|
||||
original
|
||||
}
|
||||
|
||||
async fn default_planes(
|
||||
&self,
|
||||
id_generator: &mut IdGenerator,
|
||||
|
@ -23,7 +23,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
|
||||
settings: Default::default(),
|
||||
context_type: kcl_lib::executor::ContextType::MockCustomForwarded,
|
||||
};
|
||||
let _memory = ctx.run(&program, None, IdGenerator::default()).await?;
|
||||
let _memory = ctx.run(&program, None, IdGenerator::default(), None).await?;
|
||||
|
||||
let result = result.lock().expect("mutex lock").clone();
|
||||
Ok(result)
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.2.20"
|
||||
version = "0.2.22"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
@ -26,7 +26,7 @@ futures = { version = "0.3.31" }
|
||||
git_rev = "0.1.0"
|
||||
gltf-json = "1.4.1"
|
||||
http = { workspace = true }
|
||||
image = { version = "0.25.1", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25.3", default-features = false, features = ["png"] }
|
||||
indexmap = { version = "2.6.0", features = ["serde"] }
|
||||
kittycad = { workspace = true }
|
||||
kittycad-modeling-cmds = { workspace = true }
|
||||
@ -34,7 +34,7 @@ lazy_static = "1.5.0"
|
||||
measurements = "0.11.0"
|
||||
mime_guess = "2.0.5"
|
||||
parse-display = "0.9.1"
|
||||
pyo3 = { version = "0.22.3", optional = true }
|
||||
pyo3 = { version = "0.22.5", optional = true }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls"] }
|
||||
ropey = "1.6.1"
|
||||
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1", "preserve_order"] }
|
||||
@ -47,18 +47,18 @@ toml = "0.8.19"
|
||||
ts-rs = { version = "10.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
|
||||
url = { version = "2.5.2", features = ["serde"] }
|
||||
urlencoding = "2.1.3"
|
||||
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
|
||||
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
|
||||
validator = { version = "0.18.1", features = ["derive"] }
|
||||
winnow = "0.6.18"
|
||||
zip = { version = "2.0.0", default-features = false }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = { version = "0.3.71" }
|
||||
js-sys = { version = "0.3.72" }
|
||||
tokio = { version = "1.40.0", 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.69", features = ["console"] }
|
||||
web-sys = { version = "0.3.72", features = ["console"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
approx = "0.5"
|
||||
@ -93,7 +93,7 @@ criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
expectorate = "1.1.0"
|
||||
handlebars = "6.1.0"
|
||||
iai = "0.1"
|
||||
image = { version = "0.25.1", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25.3", default-features = false, features = ["png"] }
|
||||
insta = { version = "1.40.0", features = ["json"] }
|
||||
itertools = "0.13.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
|
@ -48,7 +48,10 @@ pub async fn modify_ast_for_sketch(
|
||||
|
||||
// Get the information about the sketch.
|
||||
if let Some(ast_sketch) = program.get_variable(sketch_name) {
|
||||
let constraint_level = ast_sketch.get_constraint_level();
|
||||
let constraint_level = match ast_sketch {
|
||||
super::types::Definition::Variable(var) => var.get_constraint_level(),
|
||||
super::types::Definition::Import(import) => import.get_constraint_level(),
|
||||
};
|
||||
match &constraint_level {
|
||||
ConstraintLevel::None { source_ranges: _ } => {}
|
||||
ConstraintLevel::Ignore { source_ranges: _ } => {}
|
||||
|
740
src/wasm-lib/kcl/src/ast/types/execute.rs
Normal file
@ -0,0 +1,740 @@
|
||||
use super::{
|
||||
human_friendly_type, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart,
|
||||
CallExpression, Expr, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, ObjectExpression,
|
||||
TagDeclarator, UnaryExpression, UnaryOperator,
|
||||
};
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{
|
||||
BodyType, ExecState, ExecutorContext, KclValue, Metadata, Sketch, SourceRange, StatementKind, TagEngineInfo,
|
||||
TagIdentifier, UserVal,
|
||||
},
|
||||
std::FunctionKind,
|
||||
};
|
||||
use async_recursion::async_recursion;
|
||||
use serde_json::Value as JValue;
|
||||
|
||||
impl BinaryPart {
|
||||
#[async_recursion]
|
||||
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||
match self {
|
||||
BinaryPart::Literal(literal) => Ok(literal.into()),
|
||||
BinaryPart::Identifier(identifier) => {
|
||||
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,
|
||||
BinaryPart::CallExpression(call_expression) => call_expression.execute(exec_state, ctx).await,
|
||||
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, ctx).await,
|
||||
BinaryPart::MemberExpression(member_expression) => member_expression.get_result(exec_state),
|
||||
BinaryPart::IfExpression(e) => e.get_result(exec_state, ctx).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemberExpression {
|
||||
pub fn get_result_array(&self, exec_state: &mut ExecState, index: usize) -> Result<KclValue, KclError> {
|
||||
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())?;
|
||||
value.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let array_json = array.get_json_value()?;
|
||||
|
||||
if let serde_json::Value::Array(array) = array_json {
|
||||
if let Some(value) = array.get(index) {
|
||||
Ok(KclValue::UserVal(UserVal {
|
||||
value: value.clone(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
}))
|
||||
} else {
|
||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("index {} not found in array", index),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("MemberExpression array is not an array: {:?}", array),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
}))
|
||||
}
|
||||
}
|
||||
pub fn get_result(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
|
||||
#[derive(Debug)]
|
||||
enum Property {
|
||||
Number(usize),
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl Property {
|
||||
fn type_name(&self) -> &'static str {
|
||||
match self {
|
||||
Property::Number(_) => "number",
|
||||
Property::String(_) => "string",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let property_src: SourceRange = self.property.clone().into();
|
||||
let property_sr = vec![property_src];
|
||||
|
||||
let property: Property = match self.property.clone() {
|
||||
LiteralIdentifier::Identifier(identifier) => {
|
||||
let name = identifier.name;
|
||||
if !self.computed {
|
||||
// Treat the property as a literal
|
||||
Property::String(name.to_string())
|
||||
} else {
|
||||
// Actually evaluate memory to compute the property.
|
||||
let prop = exec_state.memory.get(&name, property_src)?;
|
||||
let KclValue::UserVal(prop) = prop else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: property_sr,
|
||||
message: format!(
|
||||
"{name} is not a valid property/index, you can only use a string or int (>= 0) here",
|
||||
),
|
||||
}));
|
||||
};
|
||||
match prop.value {
|
||||
JValue::Number(ref num) => {
|
||||
num
|
||||
.as_u64()
|
||||
.and_then(|x| usize::try_from(x).ok())
|
||||
.map(Property::Number)
|
||||
.ok_or_else(|| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: property_sr,
|
||||
message: format!(
|
||||
"{name}'s value is not a valid property/index, you can only use a string or int (>= 0) here",
|
||||
),
|
||||
})
|
||||
})?
|
||||
}
|
||||
JValue::String(ref x) => Property::String(x.to_owned()),
|
||||
_ => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: property_sr,
|
||||
message: format!(
|
||||
"{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array",
|
||||
),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LiteralIdentifier::Literal(literal) => {
|
||||
let value = literal.value.clone();
|
||||
match value {
|
||||
LiteralValue::IInteger(x) => {
|
||||
if let Ok(x) = u64::try_from(x) {
|
||||
Property::Number(x.try_into().unwrap())
|
||||
} else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: property_sr,
|
||||
message: format!("{x} is not a valid index, indices must be whole numbers >= 0"),
|
||||
}));
|
||||
}
|
||||
}
|
||||
LiteralValue::String(s) => Property::String(s),
|
||||
_ => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: vec![self.into()],
|
||||
message: "Only strings or ints (>= 0) can be properties/indexes".to_owned(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let object = match &self.object {
|
||||
// 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())?;
|
||||
value.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let object_json = object.get_json_value()?;
|
||||
|
||||
// Check the property and object match -- e.g. ints for arrays, strs for objects.
|
||||
match (object_json, property) {
|
||||
(JValue::Object(map), Property::String(property)) => {
|
||||
if let Some(value) = map.get(&property) {
|
||||
Ok(KclValue::UserVal(UserVal {
|
||||
value: value.clone(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
}))
|
||||
} else {
|
||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("Property '{property}' not found in object"),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
}))
|
||||
}
|
||||
}
|
||||
(JValue::Object(_), p) => Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"Only strings can be used as the property of an object, but you're using a {}",
|
||||
p.type_name()
|
||||
),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
})),
|
||||
(JValue::Array(arr), Property::Number(index)) => {
|
||||
let value_of_arr: Option<&JValue> = arr.get(index);
|
||||
if let Some(value) = value_of_arr {
|
||||
Ok(KclValue::UserVal(UserVal {
|
||||
value: value.clone(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
}))
|
||||
} else {
|
||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("The array doesn't have any item at index {index}"),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
}))
|
||||
}
|
||||
}
|
||||
(JValue::Array(_), p) => Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"Only integers >= 0 can be used as the index of an array, but you're using a {}",
|
||||
p.type_name()
|
||||
),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
})),
|
||||
(being_indexed, _) => {
|
||||
let t = human_friendly_type(&being_indexed);
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Only arrays and objects can be indexed, but you're trying to index a {t}"),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BinaryExpression {
|
||||
#[async_recursion]
|
||||
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||
let left_json_value = self.left.get_result(exec_state, ctx).await?.get_json_value()?;
|
||||
let right_json_value = self.right.get_result(exec_state, ctx).await?.get_json_value()?;
|
||||
|
||||
// First check if we are doing string concatenation.
|
||||
if self.operator == BinaryOperator::Add {
|
||||
if let (Some(left), Some(right)) = (
|
||||
parse_json_value_as_string(&left_json_value),
|
||||
parse_json_value_as_string(&right_json_value),
|
||||
) {
|
||||
let value = serde_json::Value::String(format!("{}{}", left, right));
|
||||
return Ok(KclValue::UserVal(UserVal {
|
||||
value,
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let left = parse_json_number_as_f64(&left_json_value, self.left.clone().into())?;
|
||||
let right = parse_json_number_as_f64(&right_json_value, self.right.clone().into())?;
|
||||
|
||||
let value: serde_json::Value = match self.operator {
|
||||
BinaryOperator::Add => (left + right).into(),
|
||||
BinaryOperator::Sub => (left - right).into(),
|
||||
BinaryOperator::Mul => (left * right).into(),
|
||||
BinaryOperator::Div => (left / right).into(),
|
||||
BinaryOperator::Mod => (left % right).into(),
|
||||
BinaryOperator::Pow => (left.powf(right)).into(),
|
||||
BinaryOperator::Eq => (left == right).into(),
|
||||
BinaryOperator::Neq => (left != right).into(),
|
||||
BinaryOperator::Gt => (left > right).into(),
|
||||
BinaryOperator::Gte => (left >= right).into(),
|
||||
BinaryOperator::Lt => (left < right).into(),
|
||||
BinaryOperator::Lte => (left <= right).into(),
|
||||
};
|
||||
|
||||
Ok(KclValue::UserVal(UserVal {
|
||||
value,
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl UnaryExpression {
|
||||
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||
if self.operator == UnaryOperator::Not {
|
||||
let value = self.argument.get_result(exec_state, ctx).await?.get_json_value()?;
|
||||
let Some(bool_value) = json_as_bool(&value) else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Cannot apply unary operator ! to non-boolean value: {}", value),
|
||||
source_ranges: vec![self.into()],
|
||||
}));
|
||||
};
|
||||
let negated = !bool_value;
|
||||
return Ok(KclValue::UserVal(UserVal {
|
||||
value: serde_json::Value::Bool(negated),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
}));
|
||||
}
|
||||
|
||||
let num = parse_json_number_as_f64(
|
||||
&self.argument.get_result(exec_state, ctx).await?.get_json_value()?,
|
||||
self.into(),
|
||||
)?;
|
||||
Ok(KclValue::UserVal(UserVal {
|
||||
value: (-(num)).into(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn execute_pipe_body(
|
||||
exec_state: &mut ExecState,
|
||||
body: &[Expr],
|
||||
source_range: SourceRange,
|
||||
ctx: &ExecutorContext,
|
||||
) -> Result<KclValue, KclError> {
|
||||
let Some((first, body)) = body.split_first() else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Pipe expressions cannot be empty".to_owned(),
|
||||
source_ranges: vec![source_range],
|
||||
}));
|
||||
};
|
||||
// Evaluate the first element in the pipeline.
|
||||
// They use the pipe_value from some AST node above this, so that if pipe expression is nested in a larger pipe expression,
|
||||
// they use the % from the parent. After all, this pipe expression hasn't been executed yet, so it doesn't have any % value
|
||||
// of its own.
|
||||
let meta = Metadata {
|
||||
source_range: SourceRange([first.start(), first.end()]),
|
||||
};
|
||||
let output = ctx
|
||||
.execute_expr(first, exec_state, &meta, StatementKind::Expression)
|
||||
.await?;
|
||||
|
||||
// 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));
|
||||
// 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;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Execute the tail of a pipe expression. exec_state.pipe_value must be set by
|
||||
/// the caller.
|
||||
#[async_recursion]
|
||||
async fn inner_execute_pipe_body(
|
||||
exec_state: &mut ExecState,
|
||||
body: &[Expr],
|
||||
ctx: &ExecutorContext,
|
||||
) -> Result<KclValue, KclError> {
|
||||
for expression in body {
|
||||
match expression {
|
||||
Expr::TagDeclarator(_) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("This cannot be in a PipeExpression: {:?}", expression),
|
||||
source_ranges: vec![expression.into()],
|
||||
}));
|
||||
}
|
||||
Expr::Literal(_)
|
||||
| Expr::Identifier(_)
|
||||
| Expr::BinaryExpression(_)
|
||||
| Expr::FunctionExpression(_)
|
||||
| Expr::CallExpression(_)
|
||||
| Expr::PipeExpression(_)
|
||||
| Expr::PipeSubstitution(_)
|
||||
| Expr::ArrayExpression(_)
|
||||
| Expr::ArrayRangeExpression(_)
|
||||
| Expr::ObjectExpression(_)
|
||||
| Expr::MemberExpression(_)
|
||||
| Expr::UnaryExpression(_)
|
||||
| Expr::IfExpression(_)
|
||||
| Expr::None(_) => {}
|
||||
};
|
||||
let metadata = Metadata {
|
||||
source_range: SourceRange([expression.start(), expression.end()]),
|
||||
};
|
||||
let output = ctx
|
||||
.execute_expr(expression, exec_state, &metadata, StatementKind::Expression)
|
||||
.await?;
|
||||
exec_state.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();
|
||||
Ok(final_output)
|
||||
}
|
||||
|
||||
impl CallExpression {
|
||||
#[async_recursion]
|
||||
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||
let fn_name = &self.callee.name;
|
||||
|
||||
let mut fn_args: Vec<KclValue> = Vec::with_capacity(self.arguments.len());
|
||||
|
||||
for arg in &self.arguments {
|
||||
let metadata = Metadata {
|
||||
source_range: SourceRange::from(arg),
|
||||
};
|
||||
let result = ctx
|
||||
.execute_expr(arg, exec_state, &metadata, StatementKind::Expression)
|
||||
.await?;
|
||||
fn_args.push(result);
|
||||
}
|
||||
|
||||
match ctx.stdlib.get_either(&self.callee.name) {
|
||||
FunctionKind::Core(func) => {
|
||||
// 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?;
|
||||
|
||||
// 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
|
||||
// and it works.
|
||||
match result {
|
||||
KclValue::UserVal(ref mut uval) => {
|
||||
uval.mutate(|sketch: &mut Sketch| {
|
||||
for (_, tag) in sketch.tags.iter() {
|
||||
exec_state.memory.update_tag(&tag.value, tag.clone())?;
|
||||
}
|
||||
Ok::<_, KclError>(())
|
||||
})?;
|
||||
}
|
||||
KclValue::Solid(ref mut solid) => {
|
||||
for value in &solid.value {
|
||||
if let Some(tag) = value.get_tag() {
|
||||
// Get the past tag and update it.
|
||||
let mut t = if let Some(t) = solid.sketch.tags.get(&tag.name) {
|
||||
t.clone()
|
||||
} else {
|
||||
// It's probably a fillet or a chamfer.
|
||||
// Initialize it.
|
||||
TagIdentifier {
|
||||
value: tag.name.clone(),
|
||||
info: Some(TagEngineInfo {
|
||||
id: value.get_id(),
|
||||
surface: Some(value.clone()),
|
||||
path: None,
|
||||
sketch: solid.id,
|
||||
}),
|
||||
meta: vec![Metadata {
|
||||
source_range: tag.clone().into(),
|
||||
}],
|
||||
}
|
||||
};
|
||||
|
||||
let Some(ref info) = t.info else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Tag {} does not have path info", tag.name),
|
||||
source_ranges: vec![tag.into()],
|
||||
}));
|
||||
};
|
||||
|
||||
let mut info = info.clone();
|
||||
info.surface = Some(value.clone());
|
||||
info.sketch = solid.id;
|
||||
t.info = Some(info);
|
||||
|
||||
exec_state.memory.update_tag(&tag.name, t.clone())?;
|
||||
|
||||
// update the sketch tags.
|
||||
solid.sketch.tags.insert(tag.name.clone(), t);
|
||||
}
|
||||
}
|
||||
|
||||
// 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())
|
||||
{
|
||||
current_env.update_sketch_tags(&solid.sketch);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
FunctionKind::Std(func) => {
|
||||
let function_expression = func.function();
|
||||
let (required_params, optional_params) =
|
||||
function_expression.required_and_optional_params().map_err(|e| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Error getting parts of function: {}", e),
|
||||
source_ranges: vec![self.into()],
|
||||
})
|
||||
})?;
|
||||
if fn_args.len() < required_params.len() || fn_args.len() > function_expression.params.len() {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"this function expected {} arguments, got {}",
|
||||
required_params.len(),
|
||||
fn_args.len(),
|
||||
),
|
||||
source_ranges: vec![self.into()],
|
||||
}));
|
||||
}
|
||||
|
||||
// Add the arguments to the memory.
|
||||
let mut fn_memory = exec_state.memory.clone();
|
||||
for (index, param) in required_params.iter().enumerate() {
|
||||
fn_memory.add(
|
||||
¶m.identifier.name,
|
||||
fn_args.get(index).unwrap().clone(),
|
||||
param.identifier.clone().into(),
|
||||
)?;
|
||||
}
|
||||
// Add the optional arguments to the memory.
|
||||
for (index, param) in optional_params.iter().enumerate() {
|
||||
if let Some(arg) = fn_args.get(index + required_params.len()) {
|
||||
fn_memory.add(¶m.identifier.name, arg.clone(), param.identifier.clone().into())?;
|
||||
} else {
|
||||
fn_memory.add(
|
||||
¶m.identifier.name,
|
||||
KclValue::UserVal(UserVal {
|
||||
value: serde_json::value::Value::Null,
|
||||
meta: Default::default(),
|
||||
}),
|
||||
param.identifier.clone().into(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
let fn_dynamic_state = exec_state.dynamic_state.clone();
|
||||
// TODO: Shouldn't we merge program memory into fn_dynamic_state
|
||||
// here?
|
||||
|
||||
// Call the stdlib function
|
||||
let p = &func.function().body;
|
||||
|
||||
let (exec_result, fn_memory) = {
|
||||
let previous_memory = std::mem::replace(&mut exec_state.memory, fn_memory);
|
||||
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
|
||||
let result = ctx.inner_execute(p, exec_state, BodyType::Block).await;
|
||||
exec_state.dynamic_state = previous_dynamic_state;
|
||||
let fn_memory = std::mem::replace(&mut exec_state.memory, previous_memory);
|
||||
(result, fn_memory)
|
||||
};
|
||||
|
||||
match exec_result {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
// We need to override the source ranges so we don't get the embedded kcl
|
||||
// function from the stdlib.
|
||||
return Err(err.override_source_ranges(vec![self.into()]));
|
||||
}
|
||||
};
|
||||
let out = fn_memory.return_;
|
||||
let result = out.ok_or_else(|| {
|
||||
KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("Result of stdlib function {} is undefined", fn_name),
|
||||
source_ranges: vec![self.into()],
|
||||
})
|
||||
})?;
|
||||
Ok(result)
|
||||
}
|
||||
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 return_value = {
|
||||
let previous_dynamic_state = std::mem::replace(&mut exec_state.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.
|
||||
e.add_source_ranges(vec![source_range])
|
||||
});
|
||||
exec_state.dynamic_state = previous_dynamic_state;
|
||||
result?
|
||||
};
|
||||
|
||||
let result = return_value.ok_or_else(move || {
|
||||
let mut source_ranges: Vec<SourceRange> = vec![source_range];
|
||||
// We want to send the source range of the original function.
|
||||
if let KclValue::Function { meta, .. } = func {
|
||||
source_ranges = meta.iter().map(|m| m.source_range).collect();
|
||||
};
|
||||
KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("Result of user-defined function {} is undefined", fn_name),
|
||||
source_ranges,
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl 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.memory.add(&self.name, memory_item.clone(), self.into())?;
|
||||
|
||||
Ok(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ArrayExpression {
|
||||
#[async_recursion]
|
||||
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||
let mut results = Vec::with_capacity(self.elements.len());
|
||||
|
||||
for element in &self.elements {
|
||||
let metadata = Metadata::from(element);
|
||||
// TODO: Carry statement kind here so that we know if we're
|
||||
// inside a variable declaration.
|
||||
let value = ctx
|
||||
.execute_expr(element, exec_state, &metadata, StatementKind::Expression)
|
||||
.await?;
|
||||
|
||||
results.push(value.get_json_value()?);
|
||||
}
|
||||
|
||||
Ok(KclValue::UserVal(UserVal {
|
||||
value: results.into(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl ArrayRangeExpression {
|
||||
#[async_recursion]
|
||||
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||
let metadata = Metadata::from(&*self.start_element);
|
||||
let start = ctx
|
||||
.execute_expr(&self.start_element, exec_state, &metadata, StatementKind::Expression)
|
||||
.await?
|
||||
.get_json_value()?;
|
||||
let start = parse_json_number_as_u64(&start, (&*self.start_element).into())?;
|
||||
let metadata = Metadata::from(&*self.end_element);
|
||||
let end = ctx
|
||||
.execute_expr(&self.end_element, exec_state, &metadata, StatementKind::Expression)
|
||||
.await?
|
||||
.get_json_value()?;
|
||||
let end = parse_json_number_as_u64(&end, (&*self.end_element).into())?;
|
||||
|
||||
if end < start {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: vec![self.into()],
|
||||
message: format!("Range start is greater than range end: {start} .. {end}"),
|
||||
}));
|
||||
}
|
||||
|
||||
let range: Vec<_> = if self.end_inclusive {
|
||||
(start..=end).map(JValue::from).collect()
|
||||
} else {
|
||||
(start..end).map(JValue::from).collect()
|
||||
};
|
||||
|
||||
Ok(KclValue::UserVal(UserVal {
|
||||
value: range.into(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectExpression {
|
||||
#[async_recursion]
|
||||
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||
let mut object = serde_json::Map::new();
|
||||
for property in &self.properties {
|
||||
let metadata = Metadata::from(&property.value);
|
||||
let result = ctx
|
||||
.execute_expr(&property.value, exec_state, &metadata, StatementKind::Expression)
|
||||
.await?;
|
||||
|
||||
object.insert(property.key.name.clone(), result.get_json_value()?);
|
||||
}
|
||||
|
||||
Ok(KclValue::UserVal(UserVal {
|
||||
value: object.into(),
|
||||
meta: vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_json_number_as_u64(j: &serde_json::Value, source_range: SourceRange) -> Result<u64, KclError> {
|
||||
if let serde_json::Value::Number(n) = &j {
|
||||
n.as_u64().ok_or_else(|| {
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![source_range],
|
||||
message: format!("Invalid integer: {}", j),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![source_range],
|
||||
message: format!("Invalid integer: {}", j),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange) -> Result<f64, KclError> {
|
||||
if let serde_json::Value::Number(n) = &j {
|
||||
n.as_f64().ok_or_else(|| {
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![source_range],
|
||||
message: format!("Invalid number: {}", j),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![source_range],
|
||||
message: format!("Invalid number: {}", j),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
|
||||
if let serde_json::Value::String(n) = &j {
|
||||
Some(n.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON value as bool. If it isn't a bool, returns None.
|
||||
pub fn json_as_bool(j: &serde_json::Value) -> Option<bool> {
|
||||
match j {
|
||||
JValue::Null => None,
|
||||
JValue::Bool(b) => Some(*b),
|
||||
JValue::Number(_) => None,
|
||||
JValue::String(_) => None,
|
||||
JValue::Array(_) => None,
|
||||
JValue::Object(_) => None,
|
||||
}
|
||||
}
|
@ -784,6 +784,9 @@ fn test_generate_stdlib_markdown_docs() {
|
||||
|
||||
#[test]
|
||||
fn test_generate_stdlib_json_schema() {
|
||||
// If this test fails and you've modified the AST or something else which affects the json repr
|
||||
// of stdlib functions, you should rerun the test with `EXPECTORATE=overwrite` to create new
|
||||
// test data, then check `/docs/kcl/std.json` to ensure the changes are expected.
|
||||
let stdlib = StdLib::new();
|
||||
let combined = stdlib.combined();
|
||||
|
||||
|
@ -859,7 +859,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
snippet,
|
||||
r#"patternCircular3d({
|
||||
repetitions: ${0:10},
|
||||
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},
|
||||
@ -921,7 +921,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
snippet,
|
||||
r#"patternLinear2d({
|
||||
repetitions: ${0:10},
|
||||
instances: ${0:10},
|
||||
distance: ${1:3.14},
|
||||
axis: [${2:3.14}, ${3:3.14}],
|
||||
}, ${4:%})${}"#
|
||||
|
@ -24,6 +24,8 @@ use crate::{
|
||||
executor::{DefaultPlanes, IdGenerator},
|
||||
};
|
||||
|
||||
use super::ExecutionKind;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum SocketHealth {
|
||||
Active,
|
||||
@ -46,6 +48,8 @@ pub struct EngineConnection {
|
||||
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
|
||||
/// If the server sends session data, it'll be copied to here.
|
||||
session_data: Arc<Mutex<Option<ModelingSessionData>>>,
|
||||
|
||||
execution_kind: Arc<Mutex<ExecutionKind>>,
|
||||
}
|
||||
|
||||
pub struct TcpRead {
|
||||
@ -300,6 +304,7 @@ impl EngineConnection {
|
||||
batch_end: Arc::new(Mutex::new(IndexMap::new())),
|
||||
default_planes: Default::default(),
|
||||
session_data,
|
||||
execution_kind: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -314,6 +319,18 @@ impl EngineManager for EngineConnection {
|
||||
self.batch_end.clone()
|
||||
}
|
||||
|
||||
fn execution_kind(&self) -> ExecutionKind {
|
||||
let guard = self.execution_kind.lock().unwrap();
|
||||
*guard
|
||||
}
|
||||
|
||||
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
|
||||
let mut guard = self.execution_kind.lock().unwrap();
|
||||
let original = *guard;
|
||||
*guard = execution_kind;
|
||||
original
|
||||
}
|
||||
|
||||
async fn default_planes(
|
||||
&self,
|
||||
id_generator: &mut IdGenerator,
|
||||
|
@ -22,10 +22,13 @@ use crate::{
|
||||
executor::{DefaultPlanes, IdGenerator},
|
||||
};
|
||||
|
||||
use super::ExecutionKind;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EngineConnection {
|
||||
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
|
||||
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>,
|
||||
execution_kind: Arc<Mutex<ExecutionKind>>,
|
||||
}
|
||||
|
||||
impl EngineConnection {
|
||||
@ -33,6 +36,7 @@ impl EngineConnection {
|
||||
Ok(EngineConnection {
|
||||
batch: Arc::new(Mutex::new(Vec::new())),
|
||||
batch_end: Arc::new(Mutex::new(IndexMap::new())),
|
||||
execution_kind: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -47,6 +51,18 @@ impl crate::engine::EngineManager for EngineConnection {
|
||||
self.batch_end.clone()
|
||||
}
|
||||
|
||||
fn execution_kind(&self) -> ExecutionKind {
|
||||
let guard = self.execution_kind.lock().unwrap();
|
||||
*guard
|
||||
}
|
||||
|
||||
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
|
||||
let mut guard = self.execution_kind.lock().unwrap();
|
||||
let original = *guard;
|
||||
*guard = execution_kind;
|
||||
original
|
||||
}
|
||||
|
||||
async fn default_planes(
|
||||
&self,
|
||||
_id_generator: &mut IdGenerator,
|
||||
|
@ -9,6 +9,7 @@ use kittycad_modeling_cmds as kcmc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
engine::ExecutionKind,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{DefaultPlanes, IdGenerator},
|
||||
};
|
||||
@ -42,6 +43,7 @@ pub struct EngineConnection {
|
||||
manager: Arc<EngineCommandManager>,
|
||||
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
|
||||
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>,
|
||||
execution_kind: Arc<Mutex<ExecutionKind>>,
|
||||
}
|
||||
|
||||
// Safety: WebAssembly will only ever run in a single-threaded context.
|
||||
@ -54,6 +56,7 @@ impl EngineConnection {
|
||||
manager: Arc::new(manager),
|
||||
batch: Arc::new(Mutex::new(Vec::new())),
|
||||
batch_end: Arc::new(Mutex::new(IndexMap::new())),
|
||||
execution_kind: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -68,6 +71,18 @@ impl crate::engine::EngineManager for EngineConnection {
|
||||
self.batch_end.clone()
|
||||
}
|
||||
|
||||
fn execution_kind(&self) -> ExecutionKind {
|
||||
let guard = self.execution_kind.lock().unwrap();
|
||||
*guard
|
||||
}
|
||||
|
||||
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
|
||||
let mut guard = self.execution_kind.lock().unwrap();
|
||||
let original = *guard;
|
||||
*guard = execution_kind;
|
||||
original
|
||||
}
|
||||
|
||||
async fn default_planes(
|
||||
&self,
|
||||
_id_generator: &mut IdGenerator,
|
||||
|
@ -41,6 +41,23 @@ lazy_static::lazy_static! {
|
||||
pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap();
|
||||
}
|
||||
|
||||
/// The mode of execution. When isolated, like during an import, attempting to
|
||||
/// send a command results in an error.
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ExecutionKind {
|
||||
#[default]
|
||||
Normal,
|
||||
Isolated,
|
||||
}
|
||||
|
||||
impl ExecutionKind {
|
||||
pub fn is_isolated(&self) -> bool {
|
||||
matches!(self, ExecutionKind::Isolated)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
/// Get the batch of commands to be sent to the engine.
|
||||
@ -49,6 +66,13 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
/// Get the batch of end commands to be sent to the engine.
|
||||
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>;
|
||||
|
||||
/// Get the current execution kind.
|
||||
fn execution_kind(&self) -> ExecutionKind;
|
||||
|
||||
/// Replace the current execution kind with a new value and return the
|
||||
/// existing value.
|
||||
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind;
|
||||
|
||||
/// Get the default planes.
|
||||
async fn default_planes(
|
||||
&self,
|
||||
@ -102,6 +126,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
source_range: crate::executor::SourceRange,
|
||||
cmd: &ModelingCmd,
|
||||
) -> Result<(), crate::errors::KclError> {
|
||||
let execution_kind = self.execution_kind();
|
||||
if execution_kind.is_isolated() {
|
||||
return Err(KclError::Semantic(KclErrorDetails { message: "Cannot send modeling commands while importing. Wrap your code in a function if you want to import the file.".to_owned(), source_ranges: vec![source_range] }));
|
||||
}
|
||||
let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
|
||||
cmd: cmd.clone(),
|
||||
cmd_id: id.into(),
|
||||
|
@ -14,6 +14,8 @@ pub enum KclError {
|
||||
Syntax(KclErrorDetails),
|
||||
#[error("semantic: {0:?}")]
|
||||
Semantic(KclErrorDetails),
|
||||
#[error("import cycle: {0:?}")]
|
||||
ImportCycle(KclErrorDetails),
|
||||
#[error("type: {0:?}")]
|
||||
Type(KclErrorDetails),
|
||||
#[error("unimplemented: {0:?}")]
|
||||
@ -52,6 +54,7 @@ impl KclError {
|
||||
KclError::Lexical(_) => "lexical",
|
||||
KclError::Syntax(_) => "syntax",
|
||||
KclError::Semantic(_) => "semantic",
|
||||
KclError::ImportCycle(_) => "import cycle",
|
||||
KclError::Type(_) => "type",
|
||||
KclError::Unimplemented(_) => "unimplemented",
|
||||
KclError::Unexpected(_) => "unexpected",
|
||||
@ -68,6 +71,7 @@ impl KclError {
|
||||
KclError::Lexical(e) => e.source_ranges.clone(),
|
||||
KclError::Syntax(e) => e.source_ranges.clone(),
|
||||
KclError::Semantic(e) => e.source_ranges.clone(),
|
||||
KclError::ImportCycle(e) => e.source_ranges.clone(),
|
||||
KclError::Type(e) => e.source_ranges.clone(),
|
||||
KclError::Unimplemented(e) => e.source_ranges.clone(),
|
||||
KclError::Unexpected(e) => e.source_ranges.clone(),
|
||||
@ -85,6 +89,7 @@ impl KclError {
|
||||
KclError::Lexical(e) => &e.message,
|
||||
KclError::Syntax(e) => &e.message,
|
||||
KclError::Semantic(e) => &e.message,
|
||||
KclError::ImportCycle(e) => &e.message,
|
||||
KclError::Type(e) => &e.message,
|
||||
KclError::Unimplemented(e) => &e.message,
|
||||
KclError::Unexpected(e) => &e.message,
|
||||
@ -102,6 +107,7 @@ impl KclError {
|
||||
KclError::Lexical(e) => e.source_ranges = source_ranges,
|
||||
KclError::Syntax(e) => e.source_ranges = source_ranges,
|
||||
KclError::Semantic(e) => e.source_ranges = source_ranges,
|
||||
KclError::ImportCycle(e) => e.source_ranges = source_ranges,
|
||||
KclError::Type(e) => e.source_ranges = source_ranges,
|
||||
KclError::Unimplemented(e) => e.source_ranges = source_ranges,
|
||||
KclError::Unexpected(e) => e.source_ranges = source_ranges,
|
||||
@ -121,6 +127,7 @@ impl KclError {
|
||||
KclError::Lexical(e) => e.source_ranges.extend(source_ranges),
|
||||
KclError::Syntax(e) => e.source_ranges.extend(source_ranges),
|
||||
KclError::Semantic(e) => e.source_ranges.extend(source_ranges),
|
||||
KclError::ImportCycle(e) => e.source_ranges.extend(source_ranges),
|
||||
KclError::Type(e) => e.source_ranges.extend(source_ranges),
|
||||
KclError::Unimplemented(e) => e.source_ranges.extend(source_ranges),
|
||||
KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),
|
||||
|
@ -1,6 +1,9 @@
|
||||
//! The executor for the AST.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_recursion::async_recursion;
|
||||
@ -23,12 +26,12 @@ type Point3D = kcmc::shared::Point3d<f64>;
|
||||
|
||||
use crate::{
|
||||
ast::types::{
|
||||
human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, KclNone, Program,
|
||||
ReturnStatement, TagDeclarator,
|
||||
human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, ImportStatement, ItemVisibility,
|
||||
KclNone, Program, ReturnStatement, TagDeclarator,
|
||||
},
|
||||
engine::EngineManager,
|
||||
engine::{EngineManager, ExecutionKind},
|
||||
errors::{KclError, KclErrorDetails},
|
||||
fs::FileManager,
|
||||
fs::{FileManager, FileSystem},
|
||||
settings::types::UnitLength,
|
||||
std::{FnAsArg, StdLib},
|
||||
};
|
||||
@ -47,6 +50,14 @@ pub struct ExecState {
|
||||
/// The current value of the pipe operator returned from the previous
|
||||
/// expression. If we're not currently in a pipeline, this will be None.
|
||||
pub pipe_value: Option<KclValue>,
|
||||
/// Identifiers that have been exported from the current module.
|
||||
pub module_exports: HashSet<String>,
|
||||
/// The stack of import statements for detecting circular module imports.
|
||||
/// If this is empty, we're not currently executing an import statement.
|
||||
pub import_stack: Vec<std::path::PathBuf>,
|
||||
/// The directory of the current project. This is used for resolving import
|
||||
/// paths. If None is given, the current working directory is used.
|
||||
pub project_directory: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
@ -391,6 +402,20 @@ impl KclValue {
|
||||
KclValue::Face(_) => "Face",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_function(&self) -> bool {
|
||||
match self {
|
||||
KclValue::UserVal(..)
|
||||
| KclValue::TagIdentifier(..)
|
||||
| KclValue::TagDeclarator(..)
|
||||
| KclValue::Plane(..)
|
||||
| KclValue::Face(..)
|
||||
| KclValue::Solid(..)
|
||||
| KclValue::Solids { .. }
|
||||
| KclValue::ImportedGeometry(..) => false,
|
||||
KclValue::Function { .. } => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SketchSet> for KclValue {
|
||||
@ -452,6 +477,15 @@ pub enum Geometries {
|
||||
Solids(Vec<Box<Solid>>),
|
||||
}
|
||||
|
||||
impl From<Geometry> for Geometries {
|
||||
fn from(value: Geometry) -> Self {
|
||||
match value {
|
||||
Geometry::Sketch(x) => Self::Sketches(vec![x]),
|
||||
Geometry::Solid(x) => Self::Solids(vec![x]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A sketch or a group of sketches.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
@ -1495,6 +1529,14 @@ impl From<SourceRange> for Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ImportStatement> for Metadata {
|
||||
fn from(stmt: &ImportStatement) -> Self {
|
||||
Self {
|
||||
source_range: SourceRange::new(stmt.start, stmt.end),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ExpressionStatement> for Metadata {
|
||||
fn from(exp_statement: &ExpressionStatement) -> Self {
|
||||
Self {
|
||||
@ -1958,8 +2000,9 @@ impl ExecutorContext {
|
||||
program: &crate::ast::types::Program,
|
||||
memory: Option<ProgramMemory>,
|
||||
id_generator: IdGenerator,
|
||||
project_directory: Option<String>,
|
||||
) -> Result<ExecState, KclError> {
|
||||
self.run_with_session_data(program, memory, id_generator)
|
||||
self.run_with_session_data(program, memory, id_generator, project_directory)
|
||||
.await
|
||||
.map(|x| x.0)
|
||||
}
|
||||
@ -1971,6 +2014,7 @@ impl ExecutorContext {
|
||||
program: &crate::ast::types::Program,
|
||||
memory: Option<ProgramMemory>,
|
||||
id_generator: IdGenerator,
|
||||
project_directory: Option<String>,
|
||||
) -> Result<(ExecState, Option<ModelingSessionData>), KclError> {
|
||||
let memory = if let Some(memory) = memory {
|
||||
memory.clone()
|
||||
@ -1980,6 +2024,7 @@ impl ExecutorContext {
|
||||
let mut exec_state = ExecState {
|
||||
memory,
|
||||
id_generator,
|
||||
project_directory,
|
||||
..Default::default()
|
||||
};
|
||||
// Before we even start executing the program, set the units.
|
||||
@ -2018,6 +2063,91 @@ impl ExecutorContext {
|
||||
// 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 path = import_stmt.path.clone();
|
||||
let resolved_path = if let Some(project_dir) = &exec_state.project_directory {
|
||||
std::path::PathBuf::from(project_dir).join(&path)
|
||||
} else {
|
||||
std::path::PathBuf::from(&path)
|
||||
};
|
||||
if exec_state.import_stack.contains(&resolved_path) {
|
||||
return Err(KclError::ImportCycle(KclErrorDetails {
|
||||
message: format!(
|
||||
"circular import of modules is not allowed: {} -> {}",
|
||||
exec_state
|
||||
.import_stack
|
||||
.iter()
|
||||
.map(|p| p.as_path().to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" -> "),
|
||||
resolved_path.to_string_lossy()
|
||||
),
|
||||
source_ranges: vec![import_stmt.into()],
|
||||
}));
|
||||
}
|
||||
let source = self.fs.read_to_string(&resolved_path, source_range).await?;
|
||||
let program = crate::parser::parse(&source)?;
|
||||
let (module_memory, module_exports) = {
|
||||
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);
|
||||
let result = self
|
||||
.inner_execute(&program, exec_state, crate::executor::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| {
|
||||
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}: {}",
|
||||
err.message()
|
||||
),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
(module_memory, module_exports)
|
||||
};
|
||||
for import_item in &import_stmt.items {
|
||||
// Extract the item from the module.
|
||||
let item = module_memory
|
||||
.get(&import_item.name.name, import_item.into())
|
||||
.map_err(|_err| {
|
||||
KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("{} is not defined in module", import_item.name.name),
|
||||
source_ranges: vec![SourceRange::from(&import_item.name)],
|
||||
})
|
||||
})?;
|
||||
// Check that the item is allowed to be imported.
|
||||
if !module_exports.contains(&import_item.name.name) {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
|
||||
import_item.name.name
|
||||
),
|
||||
source_ranges: vec![SourceRange::from(&import_item.name)],
|
||||
}));
|
||||
}
|
||||
|
||||
// Add the item to the current module.
|
||||
exec_state.memory.add(
|
||||
import_item.identifier(),
|
||||
item.clone(),
|
||||
SourceRange::from(&import_item.name),
|
||||
)?;
|
||||
}
|
||||
last_expr = None;
|
||||
}
|
||||
BodyItem::ExpressionStatement(expression_statement) => {
|
||||
let metadata = Metadata::from(expression_statement);
|
||||
last_expr = Some(
|
||||
@ -2044,7 +2174,21 @@ impl ExecutorContext {
|
||||
StatementKind::Declaration { name: &var_name },
|
||||
)
|
||||
.await?;
|
||||
let is_function = memory_item.is_function();
|
||||
exec_state.memory.add(&var_name, memory_item, source_range)?;
|
||||
// Track exports.
|
||||
match variable_declaration.visibility {
|
||||
ItemVisibility::Export => {
|
||||
if !is_function {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Only functions can be exported".to_owned(),
|
||||
source_ranges: vec![source_range],
|
||||
}));
|
||||
}
|
||||
exec_state.module_exports.insert(var_name);
|
||||
}
|
||||
ItemVisibility::Default => {}
|
||||
}
|
||||
}
|
||||
last_expr = None;
|
||||
}
|
||||
@ -2130,6 +2274,7 @@ impl ExecutorContext {
|
||||
},
|
||||
},
|
||||
Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?,
|
||||
Expr::ArrayRangeExpression(range_expression) => range_expression.execute(exec_state, self).await?,
|
||||
Expr::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?,
|
||||
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
|
||||
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
|
||||
@ -2148,8 +2293,9 @@ impl ExecutorContext {
|
||||
&self,
|
||||
program: &Program,
|
||||
id_generator: IdGenerator,
|
||||
project_directory: Option<String>,
|
||||
) -> Result<TakeSnapshot> {
|
||||
let _ = self.run(program, None, id_generator).await?;
|
||||
let _ = self.run(program, None, id_generator, project_directory).await?;
|
||||
|
||||
// Zoom to fit.
|
||||
self.engine
|
||||
@ -2294,7 +2440,7 @@ mod tests {
|
||||
settings: Default::default(),
|
||||
context_type: ContextType::Mock,
|
||||
};
|
||||
let exec_state = ctx.run(&program, None, IdGenerator::default()).await?;
|
||||
let exec_state = ctx.run(&program, None, IdGenerator::default(), None).await?;
|
||||
|
||||
Ok(exec_state.memory)
|
||||
}
|
||||
|
@ -37,6 +37,19 @@ impl FileSystem for FileManager {
|
||||
})
|
||||
}
|
||||
|
||||
async fn read_to_string<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
|
||||
&self,
|
||||
path: P,
|
||||
source_range: crate::executor::SourceRange,
|
||||
) -> Result<String, KclError> {
|
||||
tokio::fs::read_to_string(&path).await.map_err(|e| {
|
||||
KclError::Engine(KclErrorDetails {
|
||||
message: format!("Failed to read file `{}`: {}", path.as_ref().display(), e),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
|
||||
&self,
|
||||
path: P,
|
||||
|
@ -23,6 +23,13 @@ pub trait FileSystem: Clone {
|
||||
source_range: crate::executor::SourceRange,
|
||||
) -> Result<Vec<u8>, crate::errors::KclError>;
|
||||
|
||||
/// Read a file from the local file system.
|
||||
async fn read_to_string<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
|
||||
&self,
|
||||
path: P,
|
||||
source_range: crate::executor::SourceRange,
|
||||
) -> Result<String, crate::errors::KclError>;
|
||||
|
||||
/// Check if a file exists on the local file system.
|
||||
async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
|
||||
&self,
|
||||
|
@ -78,6 +78,22 @@ impl FileSystem for FileManager {
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
async fn read_to_string<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
|
||||
&self,
|
||||
path: P,
|
||||
source_range: crate::executor::SourceRange,
|
||||
) -> Result<String, KclError> {
|
||||
let bytes = self.read(path, source_range).await?;
|
||||
let string = String::from_utf8(bytes).map_err(|e| {
|
||||
KclError::Engine(KclErrorDetails {
|
||||
message: format!("Failed to convert bytes to string: {:?}", e),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(string)
|
||||
}
|
||||
|
||||
async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
|
||||
&self,
|
||||
path: P,
|
||||
|
@ -596,7 +596,7 @@ impl Backend {
|
||||
.clear_scene(&mut id_generator, SourceRange::default())
|
||||
.await?;
|
||||
|
||||
let exec_state = match executor_ctx.run(ast, None, id_generator).await {
|
||||
let exec_state = match executor_ctx.run(ast, None, id_generator, None).await {
|
||||
Ok(exec_state) => exec_state,
|
||||
Err(err) => {
|
||||
self.memory_map.remove(params.uri.as_str());
|
||||
@ -1123,7 +1123,7 @@ impl LanguageServer for Backend {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(value) = ast.get_value_for_position(pos) else {
|
||||
let Some(value) = ast.get_expr_for_position(pos) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
|
@ -12,6 +12,13 @@ pub(crate) mod parser_impl;
|
||||
pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%";
|
||||
pub const PIPE_OPERATOR: &str = "|>";
|
||||
|
||||
/// Parse the given KCL code into an AST.
|
||||
pub fn parse(code: &str) -> Result<Program, KclError> {
|
||||
let tokens = crate::token::lexer(code)?;
|
||||
let parser = Parser::new(tokens);
|
||||
parser.ast()
|
||||
}
|
||||
|
||||
pub struct Parser {
|
||||
pub tokens: Vec<Token>,
|
||||
pub unknown_tokens: Vec<Token>,
|
||||
|
@ -10,12 +10,12 @@ use winnow::{
|
||||
|
||||
use crate::{
|
||||
ast::types::{
|
||||
ArrayExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, CommentStyle, ElseIf,
|
||||
Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier, IfExpression, Literal,
|
||||
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue,
|
||||
ObjectExpression, ObjectProperty, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement,
|
||||
TagDeclarator, UnaryExpression, UnaryOperator, ValueMeta, VariableDeclaration, VariableDeclarator,
|
||||
VariableKind,
|
||||
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
|
||||
CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier,
|
||||
IfExpression, ImportItem, ImportStatement, ItemVisibility, Literal, LiteralIdentifier, LiteralValue,
|
||||
MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty,
|
||||
Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement, TagDeclarator, UnaryExpression,
|
||||
UnaryOperator, ValueMeta, VariableDeclaration, VariableDeclarator, VariableKind,
|
||||
},
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::SourceRange,
|
||||
@ -303,6 +303,12 @@ fn binary_operator(i: TokenSlice) -> PResult<BinaryOperator> {
|
||||
"*" => BinaryOperator::Mul,
|
||||
"%" => BinaryOperator::Mod,
|
||||
"^" => BinaryOperator::Pow,
|
||||
"==" => BinaryOperator::Eq,
|
||||
"!=" => BinaryOperator::Neq,
|
||||
">" => BinaryOperator::Gt,
|
||||
">=" => BinaryOperator::Gte,
|
||||
"<" => BinaryOperator::Lt,
|
||||
"<=" => BinaryOperator::Lte,
|
||||
_ => {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: token.as_source_ranges(),
|
||||
@ -330,6 +336,7 @@ fn operand(i: TokenSlice) -> PResult<BinaryPart> {
|
||||
| Expr::PipeExpression(_)
|
||||
| Expr::PipeSubstitution(_)
|
||||
| Expr::ArrayExpression(_)
|
||||
| Expr::ArrayRangeExpression(_)
|
||||
| Expr::ObjectExpression(_) => {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges,
|
||||
@ -460,8 +467,13 @@ pub enum NonCodeOr<T> {
|
||||
}
|
||||
|
||||
/// Parse a KCL array of elements.
|
||||
fn array(i: TokenSlice) -> PResult<ArrayExpression> {
|
||||
alt((array_empty, array_elem_by_elem, array_end_start)).parse_next(i)
|
||||
fn array(i: TokenSlice) -> PResult<Expr> {
|
||||
alt((
|
||||
array_empty.map(Box::new).map(Expr::ArrayExpression),
|
||||
array_elem_by_elem.map(Box::new).map(Expr::ArrayExpression),
|
||||
array_end_start.map(Box::new).map(Expr::ArrayRangeExpression),
|
||||
))
|
||||
.parse_next(i)
|
||||
}
|
||||
|
||||
/// Match an empty array.
|
||||
@ -533,44 +545,29 @@ pub(crate) fn array_elem_by_elem(i: TokenSlice) -> PResult<ArrayExpression> {
|
||||
})
|
||||
}
|
||||
|
||||
fn array_end_start(i: TokenSlice) -> PResult<ArrayExpression> {
|
||||
fn array_end_start(i: TokenSlice) -> PResult<ArrayRangeExpression> {
|
||||
let start = open_bracket(i)?.start;
|
||||
ignore_whitespace(i);
|
||||
let elements = integer_range
|
||||
.context(expected("array contents, a numeric range (like 0..10)"))
|
||||
.parse_next(i)?;
|
||||
let start_element = Box::new(expression.parse_next(i)?);
|
||||
ignore_whitespace(i);
|
||||
double_period.parse_next(i)?;
|
||||
ignore_whitespace(i);
|
||||
let end_element = Box::new(expression.parse_next(i)?);
|
||||
ignore_whitespace(i);
|
||||
let end = close_bracket(i)?.end;
|
||||
Ok(ArrayExpression {
|
||||
Ok(ArrayRangeExpression {
|
||||
start,
|
||||
end,
|
||||
elements,
|
||||
non_code_meta: Default::default(),
|
||||
start_element,
|
||||
end_element,
|
||||
end_inclusive: true,
|
||||
digest: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse n..m into a vec of numbers [n, n+1, ..., m-1]
|
||||
fn integer_range(i: TokenSlice) -> PResult<Vec<Expr>> {
|
||||
let (token0, floor) = integer.parse_next(i)?;
|
||||
double_period.parse_next(i)?;
|
||||
let (_token1, ceiling) = integer.parse_next(i)?;
|
||||
Ok((floor..=ceiling)
|
||||
.map(|num| {
|
||||
let num = num as i64;
|
||||
Expr::Literal(Box::new(Literal {
|
||||
start: token0.start,
|
||||
end: token0.end,
|
||||
value: num.into(),
|
||||
raw: num.to_string(),
|
||||
digest: None,
|
||||
}))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn object_property(i: TokenSlice) -> PResult<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)?;
|
||||
ignore_whitespace(i);
|
||||
colon
|
||||
.context(expected(
|
||||
"a colon, which separates the property's key from the value you're setting it to, e.g. 'height: 4'",
|
||||
@ -959,8 +956,10 @@ fn body_items_within_function(i: TokenSlice) -> PResult<WithinFunction> {
|
||||
// Any of the body item variants, each of which can optionally be followed by a comment.
|
||||
// If there is a comment, it may be preceded by whitespace.
|
||||
let item = dispatch! {peek(any);
|
||||
token if token.declaration_keyword().is_some() =>
|
||||
token if token.declaration_keyword().is_some() || token.visibility_keyword().is_some() =>
|
||||
(declaration.map(BodyItem::VariableDeclaration), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
|
||||
token if token.value == "import" && matches!(token.token_type, TokenType::Keyword) =>
|
||||
(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() => {
|
||||
@ -1125,6 +1124,111 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> {
|
||||
})
|
||||
}
|
||||
|
||||
fn import_stmt(i: TokenSlice) -> PResult<Box<ImportStatement>> {
|
||||
let import_token = any
|
||||
.try_map(|token: Token| {
|
||||
if matches!(token.token_type, TokenType::Keyword) && token.value == "import" {
|
||||
Ok(token)
|
||||
} else {
|
||||
Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: token.as_source_ranges(),
|
||||
message: format!("{} is not the 'import' keyword", token.value.as_str()),
|
||||
}))
|
||||
}
|
||||
})
|
||||
.context(expected("the 'import' keyword"))
|
||||
.parse_next(i)?;
|
||||
let start = import_token.start;
|
||||
|
||||
require_whitespace(i)?;
|
||||
|
||||
let items = separated(1.., import_item, comma_sep)
|
||||
.parse_next(i)
|
||||
.map_err(|e| e.cut())?;
|
||||
|
||||
require_whitespace(i)?;
|
||||
|
||||
any.try_map(|token: Token| {
|
||||
if matches!(token.token_type, TokenType::Keyword | TokenType::Word) && token.value == "from" {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: token.as_source_ranges(),
|
||||
message: format!("{} is not the 'from' keyword", token.value.as_str()),
|
||||
}))
|
||||
}
|
||||
})
|
||||
.context(expected("the 'from' keyword"))
|
||||
.parse_next(i)
|
||||
.map_err(|e| e.cut())?;
|
||||
|
||||
require_whitespace(i)?;
|
||||
|
||||
let path = string_literal(i)?;
|
||||
let end = path.end();
|
||||
let path_string = match path.value {
|
||||
LiteralValue::String(s) => s,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if path_string
|
||||
.chars()
|
||||
.any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != '.')
|
||||
{
|
||||
return Err(ErrMode::Cut(
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![SourceRange::new(path.start, path.end)],
|
||||
message: "import path may only contain alphanumeric characters, underscore, hyphen, and period. Files in other directories are not yet supported.".to_owned(),
|
||||
})
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
Ok(Box::new(ImportStatement {
|
||||
items,
|
||||
path: path_string,
|
||||
raw_path: path.raw,
|
||||
start,
|
||||
end,
|
||||
digest: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn import_item(i: TokenSlice) -> PResult<ImportItem> {
|
||||
let name = identifier.context(expected("an identifier to import")).parse_next(i)?;
|
||||
let start = name.start;
|
||||
let alias = opt(preceded(
|
||||
(whitespace, import_as_keyword, whitespace),
|
||||
identifier.context(expected("an identifier to alias the import")),
|
||||
))
|
||||
.parse_next(i)?;
|
||||
let end = if let Some(ref alias) = alias {
|
||||
alias.end()
|
||||
} else {
|
||||
name.end()
|
||||
};
|
||||
Ok(ImportItem {
|
||||
name,
|
||||
alias,
|
||||
start,
|
||||
end,
|
||||
digest: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn import_as_keyword(i: TokenSlice) -> PResult<Token> {
|
||||
any.try_map(|token: Token| {
|
||||
if matches!(token.token_type, TokenType::Keyword | TokenType::Word) && token.value == "as" {
|
||||
Ok(token)
|
||||
} else {
|
||||
Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: token.as_source_ranges(),
|
||||
message: format!("{} is not the 'as' keyword", token.value.as_str()),
|
||||
}))
|
||||
}
|
||||
})
|
||||
.context(expected("the 'as' keyword"))
|
||||
.parse_next(i)
|
||||
}
|
||||
|
||||
/// Parse a return statement of a user-defined function, e.g. `return x`.
|
||||
pub fn return_stmt(i: TokenSlice) -> PResult<ReturnStatement> {
|
||||
let start = any
|
||||
@ -1189,7 +1293,7 @@ fn expr_allowed_in_pipe_expr(i: TokenSlice) -> PResult<Expr> {
|
||||
literal.map(Box::new).map(Expr::Literal),
|
||||
fn_call.map(Box::new).map(Expr::CallExpression),
|
||||
identifier.map(Box::new).map(Expr::Identifier),
|
||||
array.map(Box::new).map(Expr::ArrayExpression),
|
||||
array,
|
||||
object.map(Box::new).map(Expr::ObjectExpression),
|
||||
pipe_sub.map(Box::new).map(Expr::PipeSubstitution),
|
||||
function_expression.map(Box::new).map(Expr::FunctionExpression),
|
||||
@ -1217,6 +1321,19 @@ fn possible_operands(i: TokenSlice) -> PResult<Expr> {
|
||||
.parse_next(i)
|
||||
}
|
||||
|
||||
/// Parse an item visibility specifier, e.g. export.
|
||||
fn item_visibility(i: TokenSlice) -> PResult<(ItemVisibility, Token)> {
|
||||
any.verify_map(|token: Token| {
|
||||
if token.token_type == TokenType::Keyword && token.value == "export" {
|
||||
Some((ItemVisibility::Export, token))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.context(expected("item visibility, e.g. 'export'"))
|
||||
.parse_next(i)
|
||||
}
|
||||
|
||||
fn declaration_keyword(i: TokenSlice) -> PResult<(VariableKind, Token)> {
|
||||
let res = any
|
||||
.verify_map(|token: Token| token.declaration_keyword().map(|kw| (kw, token)))
|
||||
@ -1226,6 +1343,9 @@ fn declaration_keyword(i: TokenSlice) -> PResult<(VariableKind, Token)> {
|
||||
|
||||
/// Parse a variable/constant declaration.
|
||||
fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
|
||||
let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace))
|
||||
.parse_next(i)?
|
||||
.map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1)));
|
||||
let decl_token = opt(declaration_keyword).parse_next(i)?;
|
||||
if decl_token.is_some() {
|
||||
// If there was a declaration keyword like `fn`, then it must be followed by some spaces.
|
||||
@ -1238,11 +1358,14 @@ fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
|
||||
"an identifier, which becomes name you're binding the value to",
|
||||
))
|
||||
.parse_next(i)?;
|
||||
let (kind, start, dec_end) = if let Some((kind, token)) = &decl_token {
|
||||
let (kind, mut start, dec_end) = if let Some((kind, token)) = &decl_token {
|
||||
(*kind, token.start, token.end)
|
||||
} else {
|
||||
(VariableKind::Const, id.start(), id.end())
|
||||
};
|
||||
if let Some(token) = visibility_token {
|
||||
start = token.start;
|
||||
}
|
||||
|
||||
ignore_whitespace(i);
|
||||
equals(i)?;
|
||||
@ -1291,6 +1414,7 @@ fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
|
||||
init: val,
|
||||
digest: None,
|
||||
}],
|
||||
visibility,
|
||||
kind,
|
||||
digest: None,
|
||||
})
|
||||
@ -1505,25 +1629,6 @@ fn expression_stmt(i: TokenSlice) -> PResult<ExpressionStatement> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a KCL integer, and the token that held it.
|
||||
fn integer(i: TokenSlice) -> PResult<(Token, u64)> {
|
||||
let num = one_of(TokenType::Number)
|
||||
.context(expected("a number token e.g. 3"))
|
||||
.try_map(|token: Token| {
|
||||
let source_ranges = token.as_source_ranges();
|
||||
let value = token.value.clone();
|
||||
token.value.parse().map(|num| (token, num)).map_err(|e| {
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges,
|
||||
message: format!("invalid integer {value}: {e}"),
|
||||
})
|
||||
})
|
||||
})
|
||||
.context(expected("an integer e.g. 3 (but not 3.1)"))
|
||||
.parse_next(i)?;
|
||||
Ok(num)
|
||||
}
|
||||
|
||||
/// Parse the given brace symbol.
|
||||
fn some_brace(symbol: &'static str, i: TokenSlice) -> PResult<Token> {
|
||||
one_of((TokenType::Brace, symbol))
|
||||
@ -3054,123 +3159,6 @@ e
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_expand_array() {
|
||||
let code = "const myArray = [0..10]";
|
||||
let parser = crate::parser::Parser::new(crate::token::lexer(code).unwrap());
|
||||
let result = parser.ast().unwrap();
|
||||
let expected_result = Program {
|
||||
start: 0,
|
||||
end: 23,
|
||||
body: vec![BodyItem::VariableDeclaration(VariableDeclaration {
|
||||
start: 0,
|
||||
end: 23,
|
||||
declarations: vec![VariableDeclarator {
|
||||
start: 6,
|
||||
end: 23,
|
||||
id: Identifier {
|
||||
start: 6,
|
||||
end: 13,
|
||||
name: "myArray".to_string(),
|
||||
digest: None,
|
||||
},
|
||||
init: Expr::ArrayExpression(Box::new(ArrayExpression {
|
||||
start: 16,
|
||||
end: 23,
|
||||
non_code_meta: Default::default(),
|
||||
elements: vec![
|
||||
Expr::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 0u32.into(),
|
||||
raw: "0".to_string(),
|
||||
digest: None,
|
||||
})),
|
||||
Expr::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 1u32.into(),
|
||||
raw: "1".to_string(),
|
||||
digest: None,
|
||||
})),
|
||||
Expr::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 2u32.into(),
|
||||
raw: "2".to_string(),
|
||||
digest: None,
|
||||
})),
|
||||
Expr::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 3u32.into(),
|
||||
raw: "3".to_string(),
|
||||
digest: None,
|
||||
})),
|
||||
Expr::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 4u32.into(),
|
||||
raw: "4".to_string(),
|
||||
digest: None,
|
||||
})),
|
||||
Expr::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 5u32.into(),
|
||||
raw: "5".to_string(),
|
||||
digest: None,
|
||||
})),
|
||||
Expr::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 6u32.into(),
|
||||
raw: "6".to_string(),
|
||||
digest: None,
|
||||
})),
|
||||
Expr::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 7u32.into(),
|
||||
raw: "7".to_string(),
|
||||
digest: None,
|
||||
})),
|
||||
Expr::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 8u32.into(),
|
||||
raw: "8".to_string(),
|
||||
digest: None,
|
||||
})),
|
||||
Expr::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 9u32.into(),
|
||||
raw: "9".to_string(),
|
||||
digest: None,
|
||||
})),
|
||||
Expr::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 10u32.into(),
|
||||
raw: "10".to_string(),
|
||||
digest: None,
|
||||
})),
|
||||
],
|
||||
digest: None,
|
||||
})),
|
||||
digest: None,
|
||||
}],
|
||||
kind: VariableKind::Const,
|
||||
digest: None,
|
||||
})],
|
||||
non_code_meta: NonCodeMeta::default(),
|
||||
digest: None,
|
||||
};
|
||||
|
||||
assert_eq!(result, expected_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_keyword_in_variable() {
|
||||
let some_program_string = r#"const let = "thing""#;
|
||||
@ -3705,7 +3693,10 @@ const my14 = 4 ^ 2 - 3 ^ 2 * 2
|
||||
5
|
||||
}"#
|
||||
);
|
||||
snapshot_test!(be, "let x = 3 == 3");
|
||||
snapshot_test!(bf, "let x = 3 != 3");
|
||||
snapshot_test!(bg, r#"x = 4"#);
|
||||
snapshot_test!(bh, "const obj = {center : [10, 10], radius: 5}");
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
|
@ -24,111 +24,29 @@ expression: actual
|
||||
"digest": null
|
||||
},
|
||||
"init": {
|
||||
"type": "ArrayExpression",
|
||||
"type": "ArrayExpression",
|
||||
"type": "ArrayRangeExpression",
|
||||
"type": "ArrayRangeExpression",
|
||||
"start": 16,
|
||||
"end": 23,
|
||||
"elements": [
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 17,
|
||||
"end": 18,
|
||||
"value": 0,
|
||||
"raw": "0",
|
||||
"digest": null
|
||||
},
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 17,
|
||||
"end": 18,
|
||||
"value": 1,
|
||||
"raw": "1",
|
||||
"digest": null
|
||||
},
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 17,
|
||||
"end": 18,
|
||||
"value": 2,
|
||||
"raw": "2",
|
||||
"digest": null
|
||||
},
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 17,
|
||||
"end": 18,
|
||||
"value": 3,
|
||||
"raw": "3",
|
||||
"digest": null
|
||||
},
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 17,
|
||||
"end": 18,
|
||||
"value": 4,
|
||||
"raw": "4",
|
||||
"digest": null
|
||||
},
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 17,
|
||||
"end": 18,
|
||||
"value": 5,
|
||||
"raw": "5",
|
||||
"digest": null
|
||||
},
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 17,
|
||||
"end": 18,
|
||||
"value": 6,
|
||||
"raw": "6",
|
||||
"digest": null
|
||||
},
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 17,
|
||||
"end": 18,
|
||||
"value": 7,
|
||||
"raw": "7",
|
||||
"digest": null
|
||||
},
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 17,
|
||||
"end": 18,
|
||||
"value": 8,
|
||||
"raw": "8",
|
||||
"digest": null
|
||||
},
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 17,
|
||||
"end": 18,
|
||||
"value": 9,
|
||||
"raw": "9",
|
||||
"digest": null
|
||||
},
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 17,
|
||||
"end": 18,
|
||||
"value": 10,
|
||||
"raw": "10",
|
||||
"digest": null
|
||||
}
|
||||
],
|
||||
"startElement": {
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 17,
|
||||
"end": 18,
|
||||
"value": 0,
|
||||
"raw": "0",
|
||||
"digest": null
|
||||
},
|
||||
"endElement": {
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 20,
|
||||
"end": 22,
|
||||
"value": 10,
|
||||
"raw": "10",
|
||||
"digest": null
|
||||
},
|
||||
"endInclusive": true,
|
||||
"digest": null
|
||||
},
|
||||
"digest": null
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3423
|
||||
expression: actual
|
||||
---
|
||||
{
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3470
|
||||
expression: actual
|
||||
---
|
||||
{
|
||||
|
@ -0,0 +1,65 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
expression: actual
|
||||
---
|
||||
{
|
||||
"start": 0,
|
||||
"end": 14,
|
||||
"body": [
|
||||
{
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration",
|
||||
"start": 0,
|
||||
"end": 14,
|
||||
"declarations": [
|
||||
{
|
||||
"type": "VariableDeclarator",
|
||||
"start": 4,
|
||||
"end": 14,
|
||||
"id": {
|
||||
"type": "Identifier",
|
||||
"start": 4,
|
||||
"end": 5,
|
||||
"name": "x",
|
||||
"digest": null
|
||||
},
|
||||
"init": {
|
||||
"type": "BinaryExpression",
|
||||
"type": "BinaryExpression",
|
||||
"start": 8,
|
||||
"end": 14,
|
||||
"operator": "==",
|
||||
"left": {
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 8,
|
||||
"end": 9,
|
||||
"value": 3,
|
||||
"raw": "3",
|
||||
"digest": null
|
||||
},
|
||||
"right": {
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 13,
|
||||
"end": 14,
|
||||
"value": 3,
|
||||
"raw": "3",
|
||||
"digest": null
|
||||
},
|
||||
"digest": null
|
||||
},
|
||||
"digest": null
|
||||
}
|
||||
],
|
||||
"kind": "const",
|
||||
"digest": null
|
||||
}
|
||||
],
|
||||
"nonCodeMeta": {
|
||||
"nonCodeNodes": {},
|
||||
"start": [],
|
||||
"digest": null
|
||||
},
|
||||
"digest": null
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
expression: actual
|
||||
---
|
||||
{
|
||||
"start": 0,
|
||||
"end": 14,
|
||||
"body": [
|
||||
{
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration",
|
||||
"start": 0,
|
||||
"end": 14,
|
||||
"declarations": [
|
||||
{
|
||||
"type": "VariableDeclarator",
|
||||
"start": 4,
|
||||
"end": 14,
|
||||
"id": {
|
||||
"type": "Identifier",
|
||||
"start": 4,
|
||||
"end": 5,
|
||||
"name": "x",
|
||||
"digest": null
|
||||
},
|
||||
"init": {
|
||||
"type": "BinaryExpression",
|
||||
"type": "BinaryExpression",
|
||||
"start": 8,
|
||||
"end": 14,
|
||||
"operator": "!=",
|
||||
"left": {
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 8,
|
||||
"end": 9,
|
||||
"value": 3,
|
||||
"raw": "3",
|
||||
"digest": null
|
||||
},
|
||||
"right": {
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 13,
|
||||
"end": 14,
|
||||
"value": 3,
|
||||
"raw": "3",
|
||||
"digest": null
|
||||
},
|
||||
"digest": null
|
||||
},
|
||||
"digest": null
|
||||
}
|
||||
],
|
||||
"kind": "const",
|
||||
"digest": null
|
||||
}
|
||||
],
|
||||
"nonCodeMeta": {
|
||||
"nonCodeNodes": {},
|
||||
"start": [],
|
||||
"digest": null
|
||||
},
|
||||
"digest": null
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
---
|
||||
source: kcl/src/parser/parser_impl.rs
|
||||
assertion_line: 3718
|
||||
expression: actual
|
||||
---
|
||||
{
|
||||
"start": 0,
|
||||
"end": 42,
|
||||
"body": [
|
||||
{
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration",
|
||||
"start": 0,
|
||||
"end": 42,
|
||||
"declarations": [
|
||||
{
|
||||
"type": "VariableDeclarator",
|
||||
"start": 6,
|
||||
"end": 42,
|
||||
"id": {
|
||||
"type": "Identifier",
|
||||
"start": 6,
|
||||
"end": 9,
|
||||
"name": "obj",
|
||||
"digest": null
|
||||
},
|
||||
"init": {
|
||||
"type": "ObjectExpression",
|
||||
"type": "ObjectExpression",
|
||||
"start": 12,
|
||||
"end": 42,
|
||||
"properties": [
|
||||
{
|
||||
"type": "ObjectProperty",
|
||||
"start": 13,
|
||||
"end": 30,
|
||||
"key": {
|
||||
"type": "Identifier",
|
||||
"start": 13,
|
||||
"end": 19,
|
||||
"name": "center",
|
||||
"digest": null
|
||||
},
|
||||
"value": {
|
||||
"type": "ArrayExpression",
|
||||
"type": "ArrayExpression",
|
||||
"start": 22,
|
||||
"end": 30,
|
||||
"elements": [
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 23,
|
||||
"end": 25,
|
||||
"value": 10,
|
||||
"raw": "10",
|
||||
"digest": null
|
||||
},
|
||||
{
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 27,
|
||||
"end": 29,
|
||||
"value": 10,
|
||||
"raw": "10",
|
||||
"digest": null
|
||||
}
|
||||
],
|
||||
"digest": null
|
||||
},
|
||||
"digest": null
|
||||
},
|
||||
{
|
||||
"type": "ObjectProperty",
|
||||
"start": 32,
|
||||
"end": 41,
|
||||
"key": {
|
||||
"type": "Identifier",
|
||||
"start": 32,
|
||||
"end": 38,
|
||||
"name": "radius",
|
||||
"digest": null
|
||||
},
|
||||
"value": {
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"start": 40,
|
||||
"end": 41,
|
||||
"value": 5,
|
||||
"raw": "5",
|
||||
"digest": null
|
||||
},
|
||||
"digest": null
|
||||
}
|
||||
],
|
||||
"digest": null
|
||||
},
|
||||
"digest": null
|
||||
}
|
||||
],
|
||||
"kind": "const",
|
||||
"digest": null
|
||||
}
|
||||
],
|
||||
"nonCodeMeta": {
|
||||
"nonCodeNodes": {},
|
||||
"start": [],
|
||||
"digest": null
|
||||
},
|
||||
"digest": null
|
||||
}
|
@ -7,7 +7,7 @@ use serde::de::DeserializeOwned;
|
||||
use serde_json::Value as JValue;
|
||||
|
||||
use crate::{
|
||||
ast::types::{parse_json_number_as_f64, TagDeclarator},
|
||||
ast::types::{execute::parse_json_number_as_f64, TagDeclarator},
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{
|
||||
ExecState, ExecutorContext, ExtrudeSurface, KclValue, Metadata, Sketch, SketchSet, SketchSurface, Solid,
|
||||
|
@ -28,6 +28,18 @@ pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
|
||||
memory: *f.memory,
|
||||
};
|
||||
let new_array = inner_map(array, map_fn, exec_state, &args).await?;
|
||||
let unwrapped = new_array
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|k| match k {
|
||||
KclValue::UserVal(user_val) => Ok(user_val.value),
|
||||
_ => Err(()),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>();
|
||||
if let Ok(unwrapped) = unwrapped {
|
||||
let uv = UserVal::new(vec![args.source_range.into()], unwrapped);
|
||||
return Ok(KclValue::UserVal(uv));
|
||||
}
|
||||
let uv = UserVal::new(vec![args.source_range.into()], new_array);
|
||||
Ok(KclValue::UserVal(uv))
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
//! Standard library patterns.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use kcmc::{
|
||||
@ -23,15 +25,18 @@ use crate::{
|
||||
std::{types::Uint, Args},
|
||||
};
|
||||
|
||||
const MUST_HAVE_ONE_INSTANCE: &str = "There must be at least 1 instance of your geometry";
|
||||
|
||||
/// Data for a linear pattern on a 2D sketch.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LinearPattern2dData {
|
||||
/// The number of repetitions. Must be greater than 0.
|
||||
/// This excludes the original entity. For example, if `repetitions` is 1,
|
||||
/// the original entity will be copied once.
|
||||
pub repetitions: Uint,
|
||||
/// The number of total instances. Must be greater than or equal to 1.
|
||||
/// This includes the original entity. For example, if instances is 2,
|
||||
/// there will be two copies -- the original, and one new copy.
|
||||
/// If instances is 1, this has no effect.
|
||||
pub instances: Uint,
|
||||
/// The distance between each repetition. This can also be referred to as spacing.
|
||||
pub distance: f64,
|
||||
/// The axis of the pattern. This is a 2D vector.
|
||||
@ -43,10 +48,11 @@ pub struct LinearPattern2dData {
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LinearPattern3dData {
|
||||
/// The number of repetitions. Must be greater than 0.
|
||||
/// This excludes the original entity. For example, if `repetitions` is 1,
|
||||
/// the original entity will be copied once.
|
||||
pub repetitions: Uint,
|
||||
/// The number of total instances. Must be greater than or equal to 1.
|
||||
/// This includes the original entity. For example, if instances is 2,
|
||||
/// there will be two copies -- the original, and one new copy.
|
||||
/// If instances is 1, this has no effect.
|
||||
pub instances: Uint,
|
||||
/// The distance between each repetition. This can also be referred to as spacing.
|
||||
pub distance: f64,
|
||||
/// The axis of the pattern.
|
||||
@ -66,11 +72,12 @@ impl LinearPattern {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn repetitions(&self) -> u32 {
|
||||
match self {
|
||||
LinearPattern::TwoD(lp) => lp.repetitions.u32(),
|
||||
LinearPattern::ThreeD(lp) => lp.repetitions.u32(),
|
||||
}
|
||||
fn repetitions(&self) -> RepetitionsNeeded {
|
||||
let n = match self {
|
||||
LinearPattern::TwoD(lp) => lp.instances.u32(),
|
||||
LinearPattern::ThreeD(lp) => lp.instances.u32(),
|
||||
};
|
||||
RepetitionsNeeded::from(n)
|
||||
}
|
||||
|
||||
pub fn distance(&self) -> f64 {
|
||||
@ -278,6 +285,12 @@ async fn inner_pattern_transform<'a>(
|
||||
) -> Result<Vec<Box<Solid>>, KclError> {
|
||||
// Build the vec of transforms, one for each repetition.
|
||||
let mut transform = Vec::with_capacity(usize::try_from(total_instances).unwrap());
|
||||
if total_instances < 1 {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![args.source_range],
|
||||
message: MUST_HAVE_ONE_INSTANCE.to_owned(),
|
||||
}));
|
||||
}
|
||||
for i in 1..total_instances {
|
||||
let t = make_transform(i, &transform_function, args.source_range, exec_state).await?;
|
||||
transform.push(t);
|
||||
@ -498,7 +511,7 @@ pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result
|
||||
/// |> circle({ center: [0, 0], radius: 1 }, %)
|
||||
/// |> patternLinear2d({
|
||||
/// axis: [1, 0],
|
||||
/// repetitions: 6,
|
||||
/// instances: 7,
|
||||
/// distance: 4
|
||||
/// }, %)
|
||||
///
|
||||
@ -573,7 +586,7 @@ pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result
|
||||
/// const example = extrude(1, exampleSketch)
|
||||
/// |> patternLinear3d({
|
||||
/// axis: [1, 0, 1],
|
||||
/// repetitions: 6,
|
||||
/// instances: 7,
|
||||
/// distance: 6
|
||||
/// }, %)
|
||||
/// ```
|
||||
@ -629,13 +642,26 @@ async fn pattern_linear(
|
||||
) -> Result<Geometries, KclError> {
|
||||
let id = exec_state.id_generator.next_uuid();
|
||||
|
||||
let num_repetitions = match data.repetitions() {
|
||||
RepetitionsNeeded::More(n) => n,
|
||||
RepetitionsNeeded::None => {
|
||||
return Ok(Geometries::from(geometry));
|
||||
}
|
||||
RepetitionsNeeded::Invalid => {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![args.source_range],
|
||||
message: MUST_HAVE_ONE_INSTANCE.to_owned(),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
let resp = args
|
||||
.send_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::from(mcmd::EntityLinearPattern {
|
||||
axis: kcmc::shared::Point3d::from(data.axis()),
|
||||
entity_id: geometry.id(),
|
||||
num_repetitions: data.repetitions(),
|
||||
num_repetitions,
|
||||
spacing: LengthUnit(data.distance()),
|
||||
}),
|
||||
)
|
||||
@ -680,10 +706,11 @@ async fn pattern_linear(
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CircularPattern2dData {
|
||||
/// The number of repetitions. Must be greater than 0.
|
||||
/// This excludes the original entity. For example, if `repetitions` is 1,
|
||||
/// the original entity will be copied once.
|
||||
pub repetitions: Uint,
|
||||
/// The number of total instances. Must be greater than or equal to 1.
|
||||
/// This includes the original entity. For example, if instances is 2,
|
||||
/// there will be two copies -- the original, and one new copy.
|
||||
/// If instances is 1, this has no effect.
|
||||
pub instances: Uint,
|
||||
/// The center about which to make the pattern. This is a 2D vector.
|
||||
pub center: [f64; 2],
|
||||
/// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
|
||||
@ -697,10 +724,11 @@ pub struct CircularPattern2dData {
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CircularPattern3dData {
|
||||
/// The number of repetitions. Must be greater than 0.
|
||||
/// This excludes the original entity. For example, if `repetitions` is 1,
|
||||
/// the original entity will be copied once.
|
||||
pub repetitions: Uint,
|
||||
/// The number of total instances. Must be greater than or equal to 1.
|
||||
/// This includes the original entity. For example, if instances is 2,
|
||||
/// there will be two copies -- the original, and one new copy.
|
||||
/// If instances is 1, this has no effect.
|
||||
pub instances: Uint,
|
||||
/// The axis around which to make the pattern. This is a 3D vector.
|
||||
pub axis: [f64; 3],
|
||||
/// The center about which to make the pattern. This is a 3D vector.
|
||||
@ -716,6 +744,25 @@ pub enum CircularPattern {
|
||||
TwoD(CircularPattern2dData),
|
||||
}
|
||||
|
||||
enum RepetitionsNeeded {
|
||||
/// Add this number of repetitions
|
||||
More(u32),
|
||||
/// No repetitions needed
|
||||
None,
|
||||
/// Invalid number of total instances.
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl From<u32> for RepetitionsNeeded {
|
||||
fn from(n: u32) -> Self {
|
||||
match n.cmp(&1) {
|
||||
Ordering::Less => Self::Invalid,
|
||||
Ordering::Equal => Self::None,
|
||||
Ordering::Greater => Self::More(n - 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CircularPattern {
|
||||
pub fn axis(&self) -> [f64; 3] {
|
||||
match self {
|
||||
@ -731,11 +778,12 @@ impl CircularPattern {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn repetitions(&self) -> u32 {
|
||||
match self {
|
||||
CircularPattern::TwoD(lp) => lp.repetitions.u32(),
|
||||
CircularPattern::ThreeD(lp) => lp.repetitions.u32(),
|
||||
}
|
||||
fn repetitions(&self) -> RepetitionsNeeded {
|
||||
let n = match self {
|
||||
CircularPattern::TwoD(lp) => lp.instances.u32(),
|
||||
CircularPattern::ThreeD(lp) => lp.instances.u32(),
|
||||
};
|
||||
RepetitionsNeeded::from(n)
|
||||
}
|
||||
|
||||
pub fn arc_degrees(&self) -> f64 {
|
||||
@ -775,7 +823,7 @@ pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Resu
|
||||
/// |> close(%)
|
||||
/// |> patternCircular2d({
|
||||
/// center: [0, 0],
|
||||
/// repetitions: 12,
|
||||
/// instances: 13,
|
||||
/// arcDegrees: 360,
|
||||
/// rotateDuplicates: true
|
||||
/// }, %)
|
||||
@ -841,7 +889,7 @@ pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Resu
|
||||
/// |> patternCircular3d({
|
||||
/// axis: [1, -1, 0],
|
||||
/// center: [10, -20, 0],
|
||||
/// repetitions: 10,
|
||||
/// instances: 11,
|
||||
/// arcDegrees: 360,
|
||||
/// rotateDuplicates: true
|
||||
/// }, %)
|
||||
@ -897,6 +945,18 @@ async fn pattern_circular(
|
||||
args: Args,
|
||||
) -> Result<Geometries, KclError> {
|
||||
let id = exec_state.id_generator.next_uuid();
|
||||
let num_repetitions = match data.repetitions() {
|
||||
RepetitionsNeeded::More(n) => n,
|
||||
RepetitionsNeeded::None => {
|
||||
return Ok(Geometries::from(geometry));
|
||||
}
|
||||
RepetitionsNeeded::Invalid => {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![args.source_range],
|
||||
message: MUST_HAVE_ONE_INSTANCE.to_owned(),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
let center = data.center();
|
||||
let resp = args
|
||||
@ -910,7 +970,7 @@ async fn pattern_circular(
|
||||
y: LengthUnit(center[1]),
|
||||
z: LengthUnit(center[2]),
|
||||
},
|
||||
num_repetitions: data.repetitions(),
|
||||
num_repetitions,
|
||||
arc_degrees: data.arc_degrees(),
|
||||
rotate_duplicates: data.rotate_duplicates(),
|
||||
}),
|
||||
|
@ -30,7 +30,7 @@ async fn do_execute_and_snapshot(ctx: &ExecutorContext, code: &str) -> anyhow::R
|
||||
let program = parser.ast()?;
|
||||
|
||||
let snapshot = ctx
|
||||
.execute_and_prepare_snapshot(&program, IdGenerator::default())
|
||||
.execute_and_prepare_snapshot(&program, IdGenerator::default(), None)
|
||||
.await?;
|
||||
|
||||
// Create a temporary file to write the output to.
|
||||
|