Compare commits

..

27 Commits

Author SHA1 Message Date
7a7a83c835 path to node progress 2024-01-25 09:53:14 +11:00
de63e4f19f Grackle: Refactor: Move error types into their own module (#1319)
Refactor: Move error types into their own submodule
2024-01-24 05:47:56 +00:00
b70b271e6b Grackle: compile KCL bools to EP bools (#1318) 2024-01-24 05:36:09 +00:00
08b7cdc5f6 Grackle: pipeline expressions (#1315)
Grackle can now compile |> pipelines. This means that these two programs compile to identical execution plans:

```kcl
fn double = (x) => { return x * 2 }
fn triple = (x) => { return x * 3 }
let x = 1 |> double(%) |> triple(%) // should be 6
```
```kcl
fn double = (x) => { return x * 2 }
fn triple = (x) => { return x * 3 }
let x = triple(double(1)) // should be 6
```

This required adding passing "what should % actually resolve to" through the program. This required modifying every call site of `plan_to_bind` and `plan_to_compute` to pass the data. To avoid doing this again, I wrapped that data into a struct called `Context` so that when we have more data like it, we can just add a new field and won't need to change every call site.
2024-01-24 10:05:40 +11:00
6efe6b54c0 Fix typo in onboarding (#1316)
fix typo
2024-01-23 17:46:34 -05:00
69f72d62e0 Rework initial engine connection logic (#1205) (#1221)
Rework EngineConnection class (#1205)

Co-authored-by: lf94 <inbox@leefallat.ca>
2024-01-23 13:13:43 -05:00
e04b09fcd8 Grackle: unary operations (#1308)
Support compiling logical not and sign-flipping negation.
2024-01-23 13:57:09 +11:00
4903f6b9fc Grackle: compile and execute user-defined KCL functions (#1306)
* Grackle: compile KCL function definitions

Definitions like `fn x = () => { return 1 }` can now be compiled. These functions can't be _called_ yet, but just defining them and mapping them to names works now.

* Failing test for executing a user-defined function

* Refactor: KclFunction is now an enum, not a trait

It's a pain in the ass to work with trait objects in Rust, so I'm refactoring to avoid needing traits at all. We can just use enums. This simplifies future work.

* Zero-parameter functions can be called

Finally, Grackle can actually run user-defined KCL functions! It basically treats them as a new, separate program (with its own scope of variables, nested within the existing parent scope).

* Failing test for multi-param KCL functions

* Execute user-defined functions which declare parameters

Previous commits in this PR got user-defined functions working, but only if they had zero parameters. In this commit, call arguments are bound to function parameters, so you can now compile functions with params.

* Users get a compile error if they try to pass more args to a function than it has parameters

This will help users get clear error messages.

* More test coverage

Among other things, this verify that Grackle compiles KCL functions which themselves either return or accept functions
2024-01-23 11:30:00 +11:00
ef8149f03a Bump vite from 4.5.1 to 4.5.2 (#1302)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 17:00:02 +11:00
1b75321bf1 Rust: Update h2 (#1304) 2024-01-21 23:54:04 +00:00
3ed263da6b Grackle: Tests for computed properties (#1303)
These tests don't pass, because Grackle doesn't support computed properties yet. But they're worth committing anyway, so I put "#[ignore]" on them.
2024-01-22 10:45:48 +11:00
d59c4a2258 Grackle: Compile member expressions (#1290)
Member expressions like "obj.property" just look up "property" under the binding for "obj".
2024-01-12 14:42:42 -06:00
9c8351ea40 get off ts-rs fork (#1288)
* get off ts-rs fork

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-01-11 15:31:35 -08:00
db98bcf2a0 throttle scroll zoom (#1287) 2024-01-12 09:14:37 +11:00
15d96a072d Tiny refactors to Grackle (#1286)
- Move bindings into their own scope
- Remove visitor type
2024-01-11 12:38:08 -06:00
088968c664 Grackle (KCL to EP compiler) (#1270)
* Start Grackle (KCL-to-EP compiler)

This begins work on a second, different executor. The old executor is a tree-walk interpreter, this executor compiles the KCL programs into the Execution Plan virtual machine defined in its [own crate](https://github.com/KittyCAD/modeling-api/tree/main/execution-plan). This executor is called "Grackle", after an Austin bird, and it's got its own module in wasm-lib so that I can keep merging small PRs and developing incrementally, rather than building a complete executor which replaces the old executor in one PR.

Grackle's "Planner" walks the AST, like the tree-walk executor. But it doesn't actually execute code. Instead, as it walks each AST node, it outputs a sequence of Execution Plan instructions which, when run, can compute that node's value. It also notes which Execution Plan virtual machine address will eventually contain each KCL variable.

Done:
 - Storing KCL variables
 - Computing primitives, literals, binary expressions
 - Calling native (i.e. Rust) functions from KCL
 - Storing arrays

Todo:
- KCL functions (i.e. user-defined functions)
- Member expressions
- Port over existing executor's native funtions (e.g. `lineTo`, `extrude` and `startSketchAt`)
2024-01-11 09:25:10 -06:00
4bbf98bc34 Bump follow-redirects from 1.15.2 to 1.15.4 (#1278)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 15:00:24 -06:00
ca08f5b337 Ignore test that stack overflows (#1282)
Execution plans will eventually fix this bug.
2024-01-09 14:58:31 -06:00
a3649d09c0 no more need for ffmpeg (#1277)
twenty-twenty 0.7 makes the ffmpeg support optional and puts it behind a feature flag. We aren't using its ffmpeg support here.
2024-01-08 21:22:53 -06:00
635cb58036 Bump vite from 4.5.0 to 4.5.1 (#1180)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.1/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2024-01-04 22:33:03 -06:00
7f050b114f Bump unsafe-libyaml from 0.2.9 to 0.2.10 in /src/wasm-lib (#1247)
Bumps [unsafe-libyaml](https://github.com/dtolnay/unsafe-libyaml) from 0.2.9 to 0.2.10.
- [Release notes](https://github.com/dtolnay/unsafe-libyaml/releases)
- [Commits](https://github.com/dtolnay/unsafe-libyaml/compare/0.2.9...0.2.10)

---
updated-dependencies:
- dependency-name: unsafe-libyaml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-04 22:32:52 -06:00
c999819450 Tauri e2e coverage: check filesystem settings, create/open file (#1191)
* Create a file and expect stream to fail on Linux
Fixes #1190

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

* Try to add @franknoirot's suggestion

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

* Check settings first

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

* Working test

* Clean up

* Linux fix

* Linux fix attempt #2

* BUILD_RELEASE true temporarily

* Revert "BUILD_RELEASE true temporarily"

This reverts commit 42b2d5f6bb.

* Better comment

* Home checks, and proj name check

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

* Open proj

* Fix defaultDir in test

* WIP signout

* Workaround to recover from error

* Typo

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-04 04:54:07 -05:00
82905caad6 Bump kittycad (#1262) 2024-01-02 19:13:41 +00:00
519e6d74ac fix domain (#1263)
* fix domain

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

* fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-01-02 11:10:06 -08:00
edb7d68c05 A failed build-test-apps job on a specific OS should cancel all the other jobs (#1258)
Fixes #1257
2024-01-02 04:49:35 -05:00
345dd45caa Stop the upload of broken Linux builds (#1256)
* Stop the upload of broken Linux builds
Fixes #1255

* Back to Zoo
2024-01-02 04:43:18 -05:00
b6a5f133f3 Migrate env variables to zoo.dev (#1243) 2023-12-20 22:43:13 +00:00
39 changed files with 4080 additions and 583 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime
skip: **/target,node_modules,build
skip: **/target,node_modules,build,**/Cargo.lock

View File

@ -1,6 +1,6 @@
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000
VITE_KC_SENTRY_DSN=

View File

@ -1,5 +1,5 @@
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.zoo.dev
VITE_KC_SITE_BASE_URL=https://zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000

View File

@ -43,17 +43,6 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Install ffmpeg
run: |
sudo apt update
sudo apt install \
ffmpeg \
libavformat-dev \
libavutil-dev \
libclang-dev \
libswscale-dev \
--no-install-recommends
- name: Run clippy
run: |
cd "${{ matrix.dir }}"

View File

@ -44,16 +44,6 @@ jobs:
- uses: taiki-e/install-action@nextest
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Install ffmpeg
run: |
sudo apt update
sudo apt install \
ffmpeg \
libavformat-dev \
libavutil-dev \
libclang-dev \
libswscale-dev \
--no-install-recommends
- name: cargo test
shell: bash
run: |-

View File

@ -123,6 +123,7 @@ jobs:
needs: [prepare-json-files]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
@ -243,6 +244,7 @@ jobs:
args: "${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
- uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest'
env:
PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
@ -279,7 +281,6 @@ jobs:
run: |
ls -l artifact/*/*oo*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
@ -288,8 +289,6 @@ jobs:
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/Zoo%20Modeling%20App.app.tar.gz" \
--arg linux_sig "$LINUX_SIG" \
--arg linux_url "$RELEASE_DIR/appimage/zoo-modeling-app_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi.zip" \
'{
@ -305,10 +304,6 @@ jobs:
"signature": $darwin_sig,
"url": $darwin_url
},
"linux-x86_64": {
"signature": $linux_sig,
"url": $linux_url
},
"windows-x86_64": {
"signature": $windows_sig,
"url": $windows_url
@ -325,7 +320,6 @@ jobs:
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/Zoo%20Modeling%20App_${VERSION_NO_V}_universal.dmg" \
--arg linux_url "$RELEASE_DIR/appimage/zoo-modeling-app_${VERSION_NO_V}_amd64.AppImage" \
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi" \
'{
"version": $version,
@ -335,9 +329,6 @@ jobs:
"dmg-universal": {
"url": $darwin_url
},
"appimage-x86_64": {
"url": $linux_url
},
"msi-x86_64": {
"url": $windows_url
}
@ -359,7 +350,7 @@ jobs:
uses: google-github-actions/upload-cloud-storage@v2.0.0
with:
path: artifact
glob: '*/*oo*'
glob: '*/Zoo*'
parent: false
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
@ -379,4 +370,4 @@ jobs:
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v1
with:
files: artifact/*/*oo*
files: 'artifact/*/Zoo*'

View File

@ -94,7 +94,6 @@ For running the rust (not tauri rust though) only, you can
cd src/wasm-lib
cargo test
```
but you will need to have install ffmpeg prior to.
## Tauri

View File

@ -1,19 +1,26 @@
import { browser, $, expect } from '@wdio/globals'
import fs from 'fs/promises'
describe('KCMA (Tauri, Linux)', () => {
it('opens the auth page, signs in, and signs out', async () => {
// Clean up previous tests
const defaultDir = `${process.env.HOME}/Documents/zoo-modeling-app-projects`
const userCodeDir = '/tmp/kittycad_user_code'
async function click(element: WebdriverIO.Element): Promise<void> {
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
await element.waitForClickable()
await browser.execute('arguments[0].click();', element)
}
describe('ZMA (Tauri, Linux)', () => {
it('opens the auth page and signs in', async () => {
// Clean up filesystem from previous tests
await new Promise((resolve) => setTimeout(resolve, 100))
await fs.rm('/tmp/kittycad_user_code', { force: true })
await browser.execute('window.localStorage.clear()')
await fs.rm(defaultDir, { force: true, recursive: true })
await fs.rm(userCodeDir, { force: true })
const signInButton = await $('[data-testid="sign-in-button"]')
expect(await signInButton.getText()).toEqual('Sign in')
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
await signInButton.waitForClickable()
await browser.execute('arguments[0].click();', signInButton)
await click(signInButton)
await new Promise((resolve) => setTimeout(resolve, 2000))
// Get from main.rs
@ -49,14 +56,51 @@ describe('KCMA (Tauri, Linux)', () => {
// Now should be signed in
const newFileButton = await $('[data-testid="home-new-file"]')
expect(await newFileButton.getText()).toEqual('New file')
})
// So let's sign out!
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await menuButton.waitForClickable()
await browser.execute('arguments[0].click();', menuButton)
await click(menuButton)
const settingsButton = await $('[data-testid="settings-button"]')
await click(settingsButton)
const defaultDirInput = await $('[data-testid="default-directory-input"]')
expect(await defaultDirInput.getValue()).toEqual(defaultDir)
const nameInput = await $('[data-testid="name-input"]')
expect(await nameInput.getValue()).toEqual('project-$nnn')
const closeButton = await $('[data-testid="close-button"]')
await click(closeButton)
})
it('checks that no file exists, creates a new file', async () => {
const homeSection = await $('[data-testid="home-section"]')
expect(await homeSection.getText()).toContain('No Projects found')
const newFileButton = await $('[data-testid="home-new-file"]')
await click(newFileButton)
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await homeSection.getText()).toContain('project-000')
})
it('opens the new file and expects an error on Linux', async () => {
const projectLink = await $('[data-testid="project-link"]')
await click(projectLink)
const error = await $('h3')
expect(await error.getText()).toContain(
"Can't find variable: RTCPeerConnection"
)
await browser.execute('window.location.href = "tauri://localhost/home"')
})
it('signs out', async () => {
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await click(menuButton)
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
await signoutButton.waitForClickable()
await browser.execute('arguments[0].click();', signoutButton)
await click(signoutButton)
const newSignInButton = await $('[data-testid="sign-in-button"]')
expect(await newSignInButton.getText()).toEqual('Sign in')
})

View File

@ -14,8 +14,8 @@
<link rel="stylesheet" href="https://use.typekit.net/zzv8rvm.css" />
<script
defer
data-domain="app.kittycad.io"
src="https://plausible.corp.kittycad.io/js/script.js"
data-domain="app.zoo.dev"
src="https://plausible.corp.zoo.dev/js/script.js"
></script>
<title>Zoo Modeling App</title>
</head>

View File

@ -136,7 +136,7 @@
"prettier": "^2.8.0",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.3.6",
"vite": "^4.5.0",
"vite": "^4.5.2",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.1",
"wait-on": "^7.2.0",

6
src-tauri/Cargo.lock generated
View File

@ -1242,9 +1242,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.20"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
dependencies = [
"bytes",
"fnv",
@ -1252,7 +1252,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap 1.9.3",
"indexmap 2.0.0",
"slab",
"tokio",
"tokio-util",

View File

@ -112,6 +112,7 @@ export type ProjectWithEntryPointMetadata = FileEntry & {
}
export type HomeLoaderData = {
projects: ProjectWithEntryPointMetadata[]
newDefaultDirectory?: string
}
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
@ -259,6 +260,7 @@ const router = createBrowserRouter(
const projectDir = await initializeProjectDirectory(
persistedSettings.defaultDirectory || ''
)
let newDefaultDirectory: string | undefined = undefined
if (projectDir !== persistedSettings.defaultDirectory) {
localStorage.setItem(
SETTINGS_PERSIST_KEY,
@ -267,6 +269,7 @@ const router = createBrowserRouter(
defaultDirectory: projectDir,
})
)
newDefaultDirectory = projectDir
}
const projectsNoMeta = (await readDir(projectDir)).filter(
isProjectDirectory
@ -282,6 +285,7 @@ const router = createBrowserRouter(
return {
projects,
newDefaultDirectory,
}
},
children: [

View File

@ -107,6 +107,7 @@ function ProjectCard({
<Link
className="flex-1 text-liquid-100 after:content-[''] after:absolute after:inset-0"
to={`${paths.FILE}/${encodeURIComponent(project.path)}`}
data-testid="project-link"
>
{project.name?.replace(FILE_EXT, '')}
</Link>

View File

@ -7,7 +7,7 @@ import {
} from 'react'
import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore'
import { getNormalisedCoordinates } from '../lib/utils'
import { getNormalisedCoordinates, throttle } from '../lib/utils'
import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
@ -115,9 +115,9 @@ export const Stream = ({ className = '' }) => {
setClickCoords({ x, y })
}
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
const fps = 60
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
@ -126,7 +126,7 @@ export const Stream = ({ className = '' }) => {
},
cmd_id: uuidv4(),
})
}
}, Math.round(1000 / fps))
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({
clientX,

View File

@ -128,6 +128,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
: paths.HOME + paths.SETTINGS
navigate(targetPath)
}}
data-testid="settings-button"
>
Settings
</ActionButton>

View File

@ -48,6 +48,72 @@ type Timeout = ReturnType<typeof setTimeout>
type ClientMetrics = Models['ClientMetrics_type']
type Value<T, U> = U extends undefined
? { type: T; value: U }
: U extends void
? { type: T }
: { type: T; value: U }
type State<T, U> = Value<T, U>
enum EngineConnectionStateType {
Fresh = 'fresh',
Connecting = 'connecting',
ConnectionEstablished = 'connection-established',
Disconnected = 'disconnected',
}
enum DisconnectedType {
Error = 'error',
Timeout = 'timeout',
Quit = 'quit',
}
type DisconnectedValue =
| State<DisconnectedType.Error, Error | undefined>
| State<DisconnectedType.Timeout, void>
| State<DisconnectedType.Quit, void>
// These are ordered by the expected sequence.
enum ConnectingType {
WebSocketConnecting = 'websocket-connecting',
WebSocketEstablished = 'websocket-established',
PeerConnectionCreated = 'peer-connection-created',
ICEServersSet = 'ice-servers-set',
SetLocalDescription = 'set-local-description',
OfferedSdp = 'offered-sdp',
ReceivedSdp = 'received-sdp',
SetRemoteDescription = 'set-remote-description',
WebRTCConnecting = 'webrtc-connecting',
ICECandidateReceived = 'ice-candidate-received',
TrackReceived = 'track-received',
DataChannelRequested = 'data-channel-requested',
DataChannelConnecting = 'data-channel-connecting',
DataChannelEstablished = 'data-channel-established',
}
type ConnectingValue =
| State<ConnectingType.WebSocketConnecting, void>
| State<ConnectingType.WebSocketEstablished, void>
| State<ConnectingType.PeerConnectionCreated, void>
| State<ConnectingType.ICEServersSet, void>
| State<ConnectingType.SetLocalDescription, void>
| State<ConnectingType.OfferedSdp, void>
| State<ConnectingType.ReceivedSdp, void>
| State<ConnectingType.SetRemoteDescription, void>
| State<ConnectingType.WebRTCConnecting, void>
| State<ConnectingType.TrackReceived, void>
| State<ConnectingType.ICECandidateReceived, void>
| State<ConnectingType.DataChannelRequested, string>
| State<ConnectingType.DataChannelConnecting, string>
| State<ConnectingType.DataChannelEstablished, void>
type EngineConnectionState =
| State<EngineConnectionStateType.Fresh, void>
| State<EngineConnectionStateType.Connecting, ConnectingValue>
| State<EngineConnectionStateType.ConnectionEstablished, void>
| State<EngineConnectionStateType.Disconnected, DisconnectedValue>
// EngineConnection encapsulates the connection(s) to the Engine
// for the EngineCommandManager; namely, the underlying WebSocket
// and WebRTC connections.
@ -55,10 +121,28 @@ class EngineConnection {
websocket?: WebSocket
pc?: RTCPeerConnection
unreliableDataChannel?: RTCDataChannel
mediaStream?: MediaStream
private _state: EngineConnectionState = {
type: EngineConnectionStateType.Fresh,
}
get state(): EngineConnectionState {
return this._state
}
set state(next: EngineConnectionState) {
console.log(`${JSON.stringify(this.state)}${JSON.stringify(next)}`)
if (next.type === EngineConnectionStateType.Disconnected) {
console.trace()
const sub = next.value
if (sub.type === DisconnectedType.Error) {
console.error(sub.value)
}
}
this._state = next
}
private ready: boolean
private connecting: boolean
private dead: boolean
private failedConnTimeout: Timeout | null
readonly url: string
@ -94,74 +178,77 @@ class EngineConnection {
}) {
this.url = url
this.token = token
this.ready = false
this.connecting = false
this.dead = false
this.failedConnTimeout = null
this.onWebsocketOpen = onWebsocketOpen
this.onDataChannelOpen = onDataChannelOpen
this.onEngineConnectionOpen = onEngineConnectionOpen
this.onConnectionStarted = onConnectionStarted
this.onClose = onClose
this.onNewTrack = onNewTrack
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 10000
// Without an interval ping, our connection will timeout.
let pingInterval = setInterval(() => {
if (this.dead) {
clearInterval(pingInterval)
}
if (this.isReady()) {
// When we're online, every 10 seconds, we'll attempt to put a 'ping'
// command through the WebSocket connection. This will help both ends
// of the connection maintain the TCP connection without hitting a
// timeout condition.
switch (this.state.type as EngineConnectionStateType) {
case EngineConnectionStateType.ConnectionEstablished:
this.send({ type: 'ping' })
break
case EngineConnectionStateType.Disconnected:
clearInterval(pingInterval)
break
default:
break
}
}, pingIntervalMs)
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
let connectInterval = setInterval(() => {
if (this.dead) {
clearInterval(connectInterval)
return
}
if (this.isReady()) {
return
}
console.log('connecting via retry')
let connectRetryInterval = setInterval(() => {
if (this.state.type !== EngineConnectionStateType.Disconnected) return
switch (this.state.value.type) {
case DisconnectedType.Error:
clearInterval(connectRetryInterval)
break
case DisconnectedType.Timeout:
console.log('Trying to reconnect')
this.connect()
break
default:
break
}
}, connectionTimeoutMs)
}
// isConnecting will return true when connect has been called, but the full
// WebRTC is not online.
isConnecting() {
return this.connecting
return this.state.type === EngineConnectionStateType.Connecting
}
// isReady will return true only when the WebRTC *and* WebSocket connection
// are connected. During setup, the WebSocket connection comes online first,
// which is used to establish the WebRTC connection. The EngineConnection
// is not "Ready" until both are connected.
isReady() {
return this.ready
return this.state.type === EngineConnectionStateType.ConnectionEstablished
}
tearDown() {
this.dead = true
this.close()
this.disconnectAll()
this.state = {
type: EngineConnectionStateType.Disconnected,
value: { type: DisconnectedType.Quit },
}
}
// shouldTrace will return true when Sentry should be used to instrument
// the Engine.
shouldTrace() {
return Sentry.getCurrentHub()?.getClient()?.getOptions()?.sendClientReports
}
// connect will attempt to connect to the Engine over a WebSocket, and
// establish the WebRTC connections.
//
// This will attempt the full handshake, and retry if the connection
// did not establish.
connect() {
console.log('connect was called')
if (this.isConnecting() || this.isReady()) {
return
}
@ -195,228 +282,98 @@ class EngineConnection {
let handshakeSpan: SpanPromise
let iceSpan: SpanPromise
const spanStart = (op: string) =>
new SpanPromise(webrtcMediaTransaction.startChild({ op }))
if (this.shouldTrace()) {
webrtcMediaTransaction = Sentry.startTransaction({
name: 'webrtc-media',
})
websocketSpan = new SpanPromise(
webrtcMediaTransaction.startChild({ op: 'websocket' })
)
webrtcMediaTransaction = Sentry.startTransaction({ name: 'webrtc-media' })
websocketSpan = spanStart('websocket')
}
this.websocket = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer'
const createPeerConnection = () => {
this.pc = new RTCPeerConnection()
this.pc.createDataChannel('unreliable_modeling_cmds')
this.websocket.addEventListener('open', (event) => {
console.log('Connected to websocket, waiting for ICE servers')
if (this.token) {
this.send({ headers: { Authorization: `Bearer ${this.token}` } })
// Data channels MUST BE specified before SDP offers because requesting
// them affects what our needs are!
const DATACHANNEL_NAME_UMC = 'unreliable_modeling_cmds'
this.pc.createDataChannel(DATACHANNEL_NAME_UMC)
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.DataChannelRequested,
value: DATACHANNEL_NAME_UMC,
},
}
this.pc.addEventListener('icecandidate', (event) => {
if (event.candidate === null) {
return
}
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.ICECandidateReceived,
},
}
// Request a candidate to use
this.send({
type: 'trickle_ice',
candidate: event.candidate.toJSON(),
})
})
this.pc.addEventListener('icecandidateerror', (_event) => {
this.pc.addEventListener('icecandidateerror', (_event: Event) => {
const event = _event as RTCPeerConnectionIceErrorEvent
console.error(
console.warn(
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
)
})
this.pc.addEventListener('connectionstatechange', (event) => {
if (this.pc?.iceConnectionState === 'connected') {
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
// Event type: generic Event type...
this.pc.addEventListener('connectionstatechange', (event: any) => {
console.log('connectionstatechange: ' + event.target?.connectionState)
switch (event.target?.connectionState) {
// From what I understand, only after have we done the ICE song and
// dance is it safest to connect the video tracks / stream
case 'connected':
if (this.shouldTrace()) {
iceSpan.resolve?.()
}
} else if (this.pc?.iceConnectionState === 'failed') {
// failed is a terminal state; let's explicitly kill the
// connection to the server at this point.
console.log('failed to negotiate ice connection; restarting')
this.close()
// Let the browser attach to the video stream now
this.onNewTrack({ conn: this, mediaStream: this.mediaStream! })
break
case 'failed':
this.disconnectAll()
this.state = {
type: EngineConnectionStateType.Disconnected,
value: {
type: DisconnectedType.Error,
value: new Error(
'failed to negotiate ice connection; restarting'
),
},
}
})
this.websocket.addEventListener('open', (event) => {
if (this.shouldTrace()) {
websocketSpan.resolve?.()
handshakeSpan = new SpanPromise(
webrtcMediaTransaction.startChild({ op: 'handshake' })
)
iceSpan = new SpanPromise(
webrtcMediaTransaction.startChild({ op: 'ice' })
)
dataChannelSpan = new SpanPromise(
webrtcMediaTransaction.startChild({
op: 'data-channel',
})
)
mediaTrackSpan = new SpanPromise(
webrtcMediaTransaction.startChild({
op: 'media-track',
})
)
}
if (this.shouldTrace()) {
Promise.all([
handshakeSpan.promise,
iceSpan.promise,
dataChannelSpan.promise,
mediaTrackSpan.promise,
]).then(() => {
console.log('All spans finished, reporting')
webrtcMediaTransaction?.finish()
})
}
this.onWebsocketOpen(this)
})
this.websocket.addEventListener('close', (event) => {
console.log('websocket connection closed', event)
this.close()
})
this.websocket.addEventListener('error', (event) => {
console.log('websocket connection error', event)
this.close()
})
this.websocket.addEventListener('message', (event) => {
// In the EngineConnection, we're looking for messages to/from
// the server that relate to the ICE handshake, or WebRTC
// negotiation. There may be other messages (including ArrayBuffer
// messages) that are intended for the GUI itself, so be careful
// when assuming we're the only consumer or that all messages will
// be carefully formatted here.
if (typeof event.data !== 'string') {
return
}
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
if (!message.success) {
const errorsString = message?.errors
?.map((error) => {
return ` - ${error.error_code}: ${error.message}`
})
.join('\n')
if (message.request_id) {
console.error(
`Error in response to request ${message.request_id}:\n${errorsString}`
)
} else {
console.error(`Error from server:\n${errorsString}`)
}
return
}
let resp = message.resp
if (!resp) {
// If there's no body to the response, we can bail here.
return
}
if (resp.type === 'sdp_answer') {
let answer = resp.data?.answer
if (!answer || answer.type === 'unspecified') {
return
}
if (this.pc?.signalingState !== 'stable') {
// If the connection is stable, we shouldn't bother updating the
// SDP, since we have a stable connection to the backend. If we
// need to renegotiate, the whole PeerConnection needs to get
// tore down.
this.pc?.setRemoteDescription(
new RTCSessionDescription({
type: answer.type,
sdp: answer.sdp,
})
)
if (this.shouldTrace()) {
// When both ends have a local and remote SDP, we've been able to
// set up successfully. We'll still need to find the right ICE
// servers, but this is hand-shook.
handshakeSpan.resolve?.()
}
}
} else if (resp.type === 'trickle_ice') {
let candidate = resp.data?.candidate
this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
} else if (resp.type === 'ice_server_info' && this.pc) {
console.log('received ice_server_info')
let ice_servers = resp.data?.ice_servers
if (ice_servers?.length > 0) {
// When we set the Configuration, we want to always force
// iceTransportPolicy to 'relay', since we know the topology
// of the ICE/STUN/TUN server and the engine. We don't wish to
// talk to the engine in any configuration /other/ than relay
// from a infra POV.
this.pc.setConfiguration({
iceServers: ice_servers,
iceTransportPolicy: 'relay',
})
} else {
this.pc?.setConfiguration({})
}
// We have an ICE Servers set now. We just setConfiguration, so let's
// start adding things we care about to the PeerConnection and let
// ICE negotiation happen in the background. Everything from here
// until the end of this function is setup of our end of the
// PeerConnection and waiting for events to fire our callbacks.
this.pc.addEventListener('icecandidate', (event) => {
if (!this.pc || !this.websocket) return
if (event.candidate !== null) {
console.log('sending trickle ice candidate')
const { candidate } = event
this.send({
type: 'trickle_ice',
candidate: candidate.toJSON(),
})
}
})
// Offer to receive 1 video track
this.pc.addTransceiver('video', {})
// Finally (but actually firstly!), to kick things off, we're going to
// generate our SDP, set it on our PeerConnection, and let the server
// know about our capabilities.
this.pc
.createOffer()
.then(async (descriptionInit) => {
await this?.pc?.setLocalDescription(descriptionInit)
console.log('sent sdp_offer begin')
this.send({
type: 'sdp_offer',
offer: this.pc?.localDescription,
})
})
.catch(console.log)
} else if (resp.type === 'metrics_request') {
if (this.webrtcStatsCollector === undefined) {
// TODO: Error message here?
return
}
this.webrtcStatsCollector().then((client_metrics) => {
this.send({
type: 'metrics_response',
metrics: client_metrics,
})
})
break
default:
break
}
})
this.pc.addEventListener('track', (event) => {
const mediaStream = event.streams[0]
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.TrackReceived,
},
}
if (this.shouldTrace()) {
let mediaStreamTrack = mediaStream.getVideoTracks()[0]
mediaStreamTrack.addEventListener('unmute', () => {
@ -436,7 +393,7 @@ class EngineConnection {
}
let videoTrack = mediaStream.getVideoTracks()[0]
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
void this.pc?.getStats(videoTrack).then((videoTrackStats) => {
let client_metrics: ClientMetrics = {
rtc_frames_decoded: 0,
rtc_frames_dropped: 0,
@ -481,56 +438,357 @@ class EngineConnection {
})
}
this.onNewTrack({
conn: this,
mediaStream: mediaStream,
})
// The app is eager to use the MediaStream; as soon as onNewTrack is
// called, the following sequence happens:
// EngineConnection.onNewTrack -> StoreState.setMediaStream ->
// Stream.tsx reacts to mediaStream change, setting a video element.
// We wait until connectionstatechange changes to "connected"
// to pass it to the rest of the application.
this.mediaStream = mediaStream
})
this.pc.addEventListener('datachannel', (event) => {
this.unreliableDataChannel = event.channel
console.log('accepted unreliable data channel', event.channel.label)
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.DataChannelConnecting,
value: event.channel.label,
},
}
this.unreliableDataChannel.addEventListener('open', (event) => {
console.log('unreliable data channel opened', event)
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.DataChannelEstablished,
},
}
if (this.shouldTrace()) {
dataChannelSpan.resolve?.()
}
this.onDataChannelOpen(this)
this.ready = true
this.connecting = false
// Do this after we set the connection is ready to avoid errors when
// we try to send messages before the connection is ready.
// Everything is now connected.
this.state = { type: EngineConnectionStateType.ConnectionEstablished }
this.onEngineConnectionOpen(this)
})
this.unreliableDataChannel.addEventListener('close', (event) => {
console.log(event)
console.log('unreliable data channel closed')
this.close()
this.disconnectAll()
this.unreliableDataChannel = undefined
if (this.areAllConnectionsClosed()) {
this.state = {
type: EngineConnectionStateType.Disconnected,
value: { type: DisconnectedType.Quit },
}
}
})
this.unreliableDataChannel.addEventListener('error', (event) => {
console.log('unreliable data channel error')
this.close()
this.disconnectAll()
this.state = {
type: EngineConnectionStateType.Disconnected,
value: {
type: DisconnectedType.Error,
value: new Error(event.toString()),
},
}
})
})
}
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.WebSocketConnecting,
},
}
this.websocket = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer'
this.websocket.addEventListener('open', (event) => {
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.WebSocketEstablished,
},
}
this.onWebsocketOpen(this)
// This is required for when KCMA is running stand-alone / within Tauri.
// Otherwise when run in a browser, the token is sent implicitly via
// the Cookie header.
if (this.token) {
this.send({ headers: { Authorization: `Bearer ${this.token}` } })
}
if (this.shouldTrace()) {
websocketSpan.resolve?.()
handshakeSpan = spanStart('handshake')
iceSpan = spanStart('ice')
dataChannelSpan = spanStart('data-channel')
mediaTrackSpan = spanStart('media-track')
}
if (this.shouldTrace()) {
void Promise.all([
handshakeSpan.promise,
iceSpan.promise,
dataChannelSpan.promise,
mediaTrackSpan.promise,
]).then(() => {
console.log('All spans finished, reporting')
webrtcMediaTransaction?.finish()
})
}
})
this.websocket.addEventListener('close', (event) => {
this.disconnectAll()
this.websocket = undefined
if (this.areAllConnectionsClosed()) {
this.state = {
type: EngineConnectionStateType.Disconnected,
value: { type: DisconnectedType.Quit },
}
}
})
this.websocket.addEventListener('error', (event) => {
this.disconnectAll()
this.state = {
type: EngineConnectionStateType.Disconnected,
value: {
type: DisconnectedType.Error,
value: new Error(event.toString()),
},
}
})
this.websocket.addEventListener('message', (event) => {
// In the EngineConnection, we're looking for messages to/from
// the server that relate to the ICE handshake, or WebRTC
// negotiation. There may be other messages (including ArrayBuffer
// messages) that are intended for the GUI itself, so be careful
// when assuming we're the only consumer or that all messages will
// be carefully formatted here.
if (typeof event.data !== 'string') {
return
}
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
if (!message.success) {
const errorsString = message?.errors
?.map((error) => {
return ` - ${error.error_code}: ${error.message}`
})
.join('\n')
if (message.request_id) {
console.error(
`Error in response to request ${message.request_id}:\n${errorsString}`
)
} else {
console.error(`Error from server:\n${errorsString}`)
}
return
}
let resp = message.resp
// If there's no body to the response, we can bail here.
// !resp.type is usually "pong" response for our "ping"
if (!resp || !resp.type) {
return
}
console.log('received', resp)
switch (resp.type) {
case 'ice_server_info':
let ice_servers = resp.data?.ice_servers
// Now that we have some ICE servers it makes sense
// to start initializing the RTCPeerConnection. RTCPeerConnection
// will begin the ICE process.
createPeerConnection()
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.PeerConnectionCreated,
},
}
// No ICE servers can be valid in a local dev. env.
if (ice_servers?.length === 0) {
console.warn('No ICE servers')
this.pc?.setConfiguration({})
} else {
// When we set the Configuration, we want to always force
// iceTransportPolicy to 'relay', since we know the topology
// of the ICE/STUN/TUN server and the engine. We don't wish to
// talk to the engine in any configuration /other/ than relay
// from a infra POV.
this.pc?.setConfiguration({
iceServers: ice_servers,
iceTransportPolicy: 'relay',
})
}
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.ICEServersSet,
},
}
// We have an ICE Servers set now. We just setConfiguration, so let's
// start adding things we care about to the PeerConnection and let
// ICE negotiation happen in the background. Everything from here
// until the end of this function is setup of our end of the
// PeerConnection and waiting for events to fire our callbacks.
// Add a transceiver to our SDP offer
this.pc?.addTransceiver('video', {
direction: 'recvonly',
})
// Create a session description offer based on our local environment
// that we will send to the remote end. The remote will send back
// what it supports via sdp_answer.
this.pc
?.createOffer()
.then((offer: RTCSessionDescriptionInit) => {
console.log(offer)
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.SetLocalDescription,
},
}
return this.pc?.setLocalDescription(offer).then(() => {
this.send({
type: 'sdp_offer',
offer,
})
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.OfferedSdp,
},
}
})
})
.catch((error: Error) => {
console.error(error)
// The local description is invalid, so there's no point continuing.
this.disconnectAll()
this.state = {
type: EngineConnectionStateType.Disconnected,
value: {
type: DisconnectedType.Error,
value: error,
},
}
})
break
case 'sdp_answer':
let answer = resp.data?.answer
if (!answer || answer.type === 'unspecified') {
return
}
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.ReceivedSdp,
},
}
// As soon as this is set, RTCPeerConnection tries to
// establish a connection.
// @ts-ignore
// Have to ignore because dom.ts doesn't have the right type
void this.pc?.setRemoteDescription(answer)
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.SetRemoteDescription,
},
}
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.WebRTCConnecting,
},
}
if (this.shouldTrace()) {
// When both ends have a local and remote SDP, we've been able to
// set up successfully. We'll still need to find the right ICE
// servers, but this is hand-shook.
handshakeSpan.resolve?.()
}
break
case 'trickle_ice':
let candidate = resp.data?.candidate
console.log('trickle_ice: using this candidate: ', candidate)
void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
break
case 'metrics_request':
if (this.webrtcStatsCollector === undefined) {
// TODO: Error message here?
return
}
void this.webrtcStatsCollector().then((client_metrics) => {
this.send({
type: 'metrics_response',
metrics: client_metrics,
})
})
break
}
})
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
if (this.failedConnTimeout) {
console.log('clearing timeout before set')
clearTimeout(this.failedConnTimeout)
this.failedConnTimeout = null
}
console.log('timeout set')
this.failedConnTimeout = setTimeout(() => {
if (this.isReady()) {
return
}
console.log('engine connection timeout on connection, closing')
this.close()
this.failedConnTimeout = null
this.disconnectAll()
this.state = {
type: EngineConnectionStateType.Disconnected,
value: {
type: DisconnectedType.Timeout,
},
}
}, connectionTimeoutMs)
this.onConnectionStarted(this)
@ -549,23 +807,15 @@ class EngineConnection {
typeof message === 'string' ? message : JSON.stringify(message)
)
}
close() {
disconnectAll() {
this.websocket?.close()
this.pc?.close()
this.unreliableDataChannel?.close()
this.websocket = undefined
this.pc = undefined
this.unreliableDataChannel = undefined
this.pc?.close()
this.webrtcStatsCollector = undefined
if (this.failedConnTimeout) {
console.log('closed timeout in close')
clearTimeout(this.failedConnTimeout)
this.failedConnTimeout = null
}
this.onClose(this)
this.ready = false
this.connecting = false
areAllConnectionsClosed() {
console.log(this.websocket, this.pc, this.unreliableDataChannel)
return !this.websocket && !this.pc && !this.unreliableDataChannel
}
}
@ -685,7 +935,7 @@ export class EngineCommandManager {
// We also do this here because we want to ensure we create the gizmo
// and execute the code everytime the stream is restarted.
const gizmoId = uuidv4()
this.sendSceneCommand({
void this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: gizmoId,
cmd: {
@ -698,7 +948,7 @@ export class EngineCommandManager {
})
// Initialize the planes.
this.initPlanes().then(() => {
void this.initPlanes().then(() => {
// We execute the code here to make sure if the stream was to
// restart in a session, we want to make sure to execute the code.
// We force it to re-execute the code because we want to make sure
@ -745,7 +995,7 @@ export class EngineCommandManager {
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
exportSave(event.data)
void exportSave(event.data)
} else {
const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data

View File

@ -45,7 +45,7 @@ export function getCoordsFromPaths(skGroup: SketchGroup, index = 0): Coords2d {
} else if (!currentPath) {
return [0, 0]
}
if (currentPath.type === 'toPoint') {
if (currentPath.type === 'topoint') {
return [currentPath.to[0], currentPath.to[1]]
}
return [0, 0]

View File

@ -38,7 +38,7 @@ export async function initializeProjectDirectory(directory: string) {
docDirectory = await documentDir()
} catch (e) {
console.log('error', e)
docDirectory = await homeDir() // seems to work better on Linux
docDirectory = `${await homeDir()}Documents/` // for headless Linux (eg. Github Actions)
}
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER

View File

@ -37,13 +37,20 @@ import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
const Home = () => {
const { commandBarSend } = useCommandsContext()
const navigate = useNavigate()
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
const { projects: loadedProjects, newDefaultDirectory } =
useLoaderData() as HomeLoaderData
const {
settings: {
context: { defaultDirectory, defaultProjectName },
send: sendToSettings,
},
} = useGlobalStateContext()
if (newDefaultDirectory) {
sendToSettings({
type: 'Set Default Directory',
data: { defaultDirectory: newDefaultDirectory },
})
}
const [state, send] = useMachine(homeMachine, {
context: {
@ -222,7 +229,7 @@ const Home = () => {
</ActionButton>
</div>
</section>
<section>
<section data-testid="home-section">
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
Loaded from{' '}
<span className="text-energy-70 dark:text-energy-40">

View File

@ -29,7 +29,7 @@ export default function CodeEditor() {
The left pane is where you write your code. It's a code editor with
syntax highlighting and autocompletion. We've decided to take the
difficult route of writing our own languagecalled <code>kcl</code>
for describing geometry, because don't want to inherit all the
for describing geometry, because we don't want to inherit all the
other functionality from existing languages. We have a lot of ideas
about how <code>kcl</code> will evolve, and we want to hear your
thoughts on it.

View File

@ -113,6 +113,7 @@ export const Settings = () => {
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
}}
className="hover:border-destroy-40"
data-testid="close-button"
>
Close
</ActionButton>
@ -178,6 +179,7 @@ export const Settings = () => {
className="flex-1 px-2 bg-transparent"
value={defaultDirectory}
disabled
data-testid="default-directory-input"
/>
<ActionButton
Element="button"
@ -209,6 +211,7 @@ export const Settings = () => {
}}
autoCapitalize="off"
autoComplete="off"
data-testid="name-input"
/>
</SettingsSection>
</>

1718
src/wasm-lib/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -25,8 +25,8 @@ image = "0.24.7"
kittycad = { workspace = true, default-features = true }
pretty_assertions = "1.4.0"
reqwest = { version = "0.11.22", default-features = false }
tokio = { version = "1.34.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.6.1"
tokio = { version = "1.35.1", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.7"
uuid = { version = "1.6.1", features = ["v4", "js", "serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
@ -52,12 +52,17 @@ debug = true
[workspace]
members = [
"derive-docs",
"grackle",
"kcl",
"kcl-macros",
]
[workspace.dependencies]
kittycad = { version = "0.2.43", default-features = false, features = ["js"] }
kittycad = { version = "0.2.45", default-features = false, features = ["js"] }
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-traits = "0.1.2"
kittycad-modeling-session = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
[[test]]
name = "executor"

View File

@ -0,0 +1,18 @@
[package]
name = "grackle"
version = "0.1.0"
edition = "2021"
description = "A new executor for KCL which compiles to Execution Plans"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
kcl-lib = { path = "../kcl" }
kittycad-execution-plan = { workspace = true }
kittycad-execution-plan-traits = { workspace = true }
kittycad-modeling-session = { workspace = true }
thiserror = "1.0.56"
[dev-dependencies]
pretty_assertions = "1"

View File

@ -0,0 +1,164 @@
use kcl_lib::ast::types::LiteralIdentifier;
use kcl_lib::ast::types::LiteralValue;
use crate::CompileError;
use crate::KclFunction;
use super::native_functions;
use super::Address;
use std::collections::HashMap;
/// KCL values which can be written to KCEP memory.
/// This is recursive. For example, the bound value might be an array, which itself contains bound values.
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub enum EpBinding {
/// A KCL value which gets stored in a particular address in KCEP memory.
Single(Address),
/// A sequence of KCL values, indexed by their position in the sequence.
Sequence(Vec<EpBinding>),
/// A sequence of KCL values, indexed by their identifier.
Map(HashMap<String, EpBinding>),
/// Not associated with a KCEP address.
Function(KclFunction),
}
impl From<KclFunction> for EpBinding {
fn from(f: KclFunction) -> Self {
Self::Function(f)
}
}
impl EpBinding {
/// Look up the given property of this binding.
pub fn property_of(&self, property: LiteralIdentifier) -> Result<&Self, CompileError> {
match property {
LiteralIdentifier::Identifier(_) => todo!("Support identifier properties"),
LiteralIdentifier::Literal(litval) => match litval.value {
// Arrays can be indexed by integers.
LiteralValue::IInteger(i) => match self {
EpBinding::Sequence(seq) => {
let i = usize::try_from(i).map_err(|_| CompileError::InvalidIndex(i.to_string()))?;
seq.get(i).ok_or(CompileError::IndexOutOfBounds { i, len: seq.len() })
}
EpBinding::Map(_) => Err(CompileError::CannotIndex),
EpBinding::Single(_) => Err(CompileError::CannotIndex),
EpBinding::Function(_) => Err(CompileError::CannotIndex),
},
// Objects can be indexed by string properties.
LiteralValue::String(property) => match self {
EpBinding::Single(_) => Err(CompileError::NoProperties),
EpBinding::Function(_) => Err(CompileError::NoProperties),
EpBinding::Sequence(_) => Err(CompileError::ArrayDoesNotHaveProperties),
EpBinding::Map(map) => map.get(&property).ok_or(CompileError::UndefinedProperty { property }),
},
// It's never valid to index by a fractional number.
LiteralValue::Fractional(num) => Err(CompileError::InvalidIndex(num.to_string())),
},
}
}
}
/// A set of bindings in a particular scope.
/// Bindings are KCL values that get "compiled" into KCEP values, which are stored in KCEP memory
/// at a particular KCEP address.
/// Bindings are referenced by the name of their KCL identifier.
///
/// KCL has multiple scopes -- each function has a scope for its own local variables and parameters.
/// So when referencing a variable, it might be in this scope, or the parent scope. So, each environment
/// has to keep track of parent environments. The root environment has no parent, and is used for KCL globals
/// (e.g. the prelude of stdlib functions).
///
/// These are called "Environments" in the "Crafting Interpreters" book.
#[derive(Debug)]
pub struct BindingScope {
// KCL value which are stored in EP memory.
ep_bindings: HashMap<String, EpBinding>,
/// KCL functions. They do NOT get stored in EP memory.
parent: Option<Box<BindingScope>>,
}
impl BindingScope {
/// The parent scope for every program, before the user has defined anything.
/// Only includes some stdlib functions.
/// This is usually known as the "prelude" in other languages. It's the stdlib functions that
/// are already imported for you when you start coding.
pub fn prelude() -> Self {
Self {
// TODO: Actually put the stdlib prelude in here,
// things like `startSketchAt` and `line`.
ep_bindings: HashMap::from([
("id".into(), EpBinding::from(KclFunction::Id(native_functions::Id))),
("add".into(), EpBinding::from(KclFunction::Add(native_functions::Add))),
(
"startSketchAt".into(),
EpBinding::from(KclFunction::StartSketchAt(native_functions::StartSketchAt)),
),
]),
parent: None,
}
}
/// Add a new scope, e.g. for new function calls.
pub fn add_scope(&mut self) {
// Move all data from `self` into `this`.
let this_parent = self.parent.take();
let this_ep_bindings = self.ep_bindings.drain().collect();
let this = Self {
ep_bindings: this_ep_bindings,
parent: this_parent,
};
// Turn `self` into a new scope, with the old `self` as its parent.
self.parent = Some(Box::new(this));
}
//// Remove a scope, e.g. when exiting a function call.
pub fn remove_scope(&mut self) {
// The scope is finished, so erase all its local variables.
self.ep_bindings.clear();
// Pop the stack -- the parent scope is now the current scope.
let p = self.parent.take().expect("cannot remove the root scope");
self.parent = p.parent;
self.ep_bindings = p.ep_bindings;
}
/// Add a binding (e.g. defining a new variable)
pub fn bind(&mut self, identifier: String, binding: EpBinding) {
self.ep_bindings.insert(identifier, binding);
}
/// Look up a binding.
pub fn get(&self, identifier: &str) -> Option<&EpBinding> {
if let Some(b) = self.ep_bindings.get(identifier) {
// The name was found in this scope.
Some(b)
} else if let Some(ref parent) = self.parent {
// Check the next scope outwards.
parent.get(identifier)
} else {
// There's no outer scope, and it wasn't found, so there's nowhere else to look.
None
}
}
/// Look up a function bound to the given identifier.
pub fn get_fn(&self, identifier: &str) -> GetFnResult {
if let Some(x) = self.get(identifier) {
match x {
EpBinding::Function(f) => GetFnResult::Found(f),
_ => GetFnResult::NonCallable,
}
} else if let Some(ref parent) = self.parent {
parent.get_fn(identifier)
} else {
GetFnResult::NotFound
}
}
}
pub enum GetFnResult<'a> {
Found(&'a KclFunction),
NonCallable,
NotFound,
}

View File

@ -0,0 +1,56 @@
use kcl_lib::ast::types::RequiredParamAfterOptionalParam;
use kittycad_execution_plan::ExecutionError;
use crate::String2;
#[derive(Debug, thiserror::Error, PartialEq, Clone)]
pub enum CompileError {
#[error("the name {name} was not defined")]
Undefined { name: String },
#[error("the function {fn_name} requires at least {required} arguments but you only supplied {actual}")]
NotEnoughArgs {
fn_name: String2,
required: usize,
actual: usize,
},
#[error("the function {fn_name} accepts at most {maximum} arguments but you supplied {actual}")]
TooManyArgs {
fn_name: String2,
maximum: usize,
actual: usize,
},
#[error("you tried to call {name} but it's not a function")]
NotCallable { name: String },
#[error("you're trying to use an operand that isn't compatible with the given arithmetic operator: {0}")]
InvalidOperand(&'static str),
#[error("you cannot use the value {0} as an index")]
InvalidIndex(String),
#[error("you tried to index into a value that isn't an array. Only arrays have numeric indices!")]
CannotIndex,
#[error("you tried to get the element {i} but that index is out of bounds. The array only has a length of {len}")]
IndexOutOfBounds { i: usize, len: usize },
#[error("you tried to access the property of a value that doesn't have any properties")]
NoProperties,
#[error("you tried to access a property of an array, but arrays don't have properties. They do have numeric indexes though, try using an index e.g. [0]")]
ArrayDoesNotHaveProperties,
#[error(
"you tried to read the '.{property}' of an object, but the object doesn't have any properties with that key"
)]
UndefinedProperty { property: String },
#[error("{0}")]
BadParamOrder(RequiredParamAfterOptionalParam),
#[error("A KCL function cannot have anything after its return value")]
MultipleReturns,
#[error("A KCL function must end with a return statement, but your function doesn't have one.")]
NoReturnStmt,
#[error("You used the %, which means \"substitute this argument for the value to the left in this |> pipeline\". But there is no such value, because you're not calling a pipeline.")]
NotInPipeline,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
Compile(#[from] CompileError),
#[error("{0}")]
Execution(#[from] ExecutionError),
}

View File

@ -0,0 +1,90 @@
use kcl_lib::ast::{self, types::BinaryPart};
/// Basically the same enum as `kcl_lib::ast::types::Value`, but grouped according to whether the
/// value is singular or composite.
/// You can convert losslessly between KclValueGroup and `kcl_lib::ast::types::Value` with From/Into.
pub enum KclValueGroup {
Single(SingleValue),
ArrayExpression(Box<ast::types::ArrayExpression>),
ObjectExpression(Box<ast::types::ObjectExpression>),
}
#[derive(Debug)]
pub enum SingleValue {
Literal(Box<ast::types::Literal>),
Identifier(Box<ast::types::Identifier>),
BinaryExpression(Box<ast::types::BinaryExpression>),
CallExpression(Box<ast::types::CallExpression>),
PipeExpression(Box<ast::types::PipeExpression>),
UnaryExpression(Box<ast::types::UnaryExpression>),
KclNoneExpression(ast::types::KclNone),
MemberExpression(Box<ast::types::MemberExpression>),
FunctionExpression(Box<ast::types::FunctionExpression>),
PipeSubstitution(Box<ast::types::PipeSubstitution>),
}
impl From<ast::types::BinaryPart> for KclValueGroup {
fn from(value: ast::types::BinaryPart) -> Self {
match value {
BinaryPart::Literal(e) => Self::Single(SingleValue::Literal(e)),
BinaryPart::Identifier(e) => Self::Single(SingleValue::Identifier(e)),
BinaryPart::BinaryExpression(e) => Self::Single(SingleValue::BinaryExpression(e)),
BinaryPart::CallExpression(e) => Self::Single(SingleValue::CallExpression(e)),
BinaryPart::UnaryExpression(e) => Self::Single(SingleValue::UnaryExpression(e)),
BinaryPart::MemberExpression(e) => Self::Single(SingleValue::MemberExpression(e)),
}
}
}
impl From<ast::types::BinaryPart> for SingleValue {
fn from(value: ast::types::BinaryPart) -> Self {
match value {
BinaryPart::Literal(e) => Self::Literal(e),
BinaryPart::Identifier(e) => Self::Identifier(e),
BinaryPart::BinaryExpression(e) => Self::BinaryExpression(e),
BinaryPart::CallExpression(e) => Self::CallExpression(e),
BinaryPart::UnaryExpression(e) => Self::UnaryExpression(e),
BinaryPart::MemberExpression(e) => Self::MemberExpression(e),
}
}
}
impl From<ast::types::Value> for KclValueGroup {
fn from(value: ast::types::Value) -> Self {
match value {
ast::types::Value::Literal(e) => Self::Single(SingleValue::Literal(e)),
ast::types::Value::Identifier(e) => Self::Single(SingleValue::Identifier(e)),
ast::types::Value::BinaryExpression(e) => Self::Single(SingleValue::BinaryExpression(e)),
ast::types::Value::CallExpression(e) => Self::Single(SingleValue::CallExpression(e)),
ast::types::Value::PipeExpression(e) => Self::Single(SingleValue::PipeExpression(e)),
ast::types::Value::None(e) => Self::Single(SingleValue::KclNoneExpression(e)),
ast::types::Value::UnaryExpression(e) => Self::Single(SingleValue::UnaryExpression(e)),
ast::types::Value::ArrayExpression(e) => Self::ArrayExpression(e),
ast::types::Value::ObjectExpression(e) => Self::ObjectExpression(e),
ast::types::Value::MemberExpression(e) => Self::Single(SingleValue::MemberExpression(e)),
ast::types::Value::FunctionExpression(e) => Self::Single(SingleValue::FunctionExpression(e)),
ast::types::Value::PipeSubstitution(e) => Self::Single(SingleValue::PipeSubstitution(e)),
}
}
}
impl From<KclValueGroup> for ast::types::Value {
fn from(value: KclValueGroup) -> Self {
match value {
KclValueGroup::Single(e) => match e {
SingleValue::Literal(e) => ast::types::Value::Literal(e),
SingleValue::Identifier(e) => ast::types::Value::Identifier(e),
SingleValue::BinaryExpression(e) => ast::types::Value::BinaryExpression(e),
SingleValue::CallExpression(e) => ast::types::Value::CallExpression(e),
SingleValue::PipeExpression(e) => ast::types::Value::PipeExpression(e),
SingleValue::UnaryExpression(e) => ast::types::Value::UnaryExpression(e),
SingleValue::KclNoneExpression(e) => ast::types::Value::None(e),
SingleValue::MemberExpression(e) => ast::types::Value::MemberExpression(e),
SingleValue::FunctionExpression(e) => ast::types::Value::FunctionExpression(e),
SingleValue::PipeSubstitution(e) => ast::types::Value::PipeSubstitution(e),
},
KclValueGroup::ArrayExpression(e) => ast::types::Value::ArrayExpression(e),
KclValueGroup::ObjectExpression(e) => ast::types::Value::ObjectExpression(e),
}
}
}

View File

@ -0,0 +1,594 @@
mod binding_scope;
mod error;
mod kcl_value_group;
mod native_functions;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use kcl_lib::{
ast,
ast::types::{BodyItem, FunctionExpressionParts, KclNone, LiteralValue, Program},
};
use kittycad_execution_plan as ep;
use kittycad_execution_plan::{Address, Instruction};
use kittycad_execution_plan_traits as ept;
use kittycad_execution_plan_traits::NumericPrimitive;
use kittycad_modeling_session::Session;
use self::binding_scope::{BindingScope, EpBinding, GetFnResult};
use self::error::{CompileError, Error};
use self::kcl_value_group::{KclValueGroup, SingleValue};
/// Execute a KCL program by compiling into an execution plan, then running that.
pub async fn execute(ast: Program, session: Session) -> Result<(), Error> {
let mut planner = Planner::new();
let (plan, _retval) = planner.build_plan(ast)?;
let mut mem = kittycad_execution_plan::Memory::default();
kittycad_execution_plan::execute(&mut mem, plan, session).await?;
Ok(())
}
/// Compiles KCL programs into Execution Plans.
struct Planner {
/// Maps KCL identifiers to what they hold, and where in KCEP virtual memory they'll be written to.
binding_scope: BindingScope,
/// Next available KCEP virtual machine memory address.
next_addr: Address,
}
impl Planner {
pub fn new() -> Self {
Self {
binding_scope: BindingScope::prelude(),
next_addr: Address::ZERO,
}
}
/// If successful, return the KCEP instructions for executing the given program.
/// If the program is a function with a return, then it also returns the KCL function's return value.
fn build_plan(&mut self, program: Program) -> Result<(Vec<Instruction>, Option<EpBinding>), CompileError> {
program
.body
.into_iter()
.try_fold((Vec::new(), None), |(mut instructions, mut retval), item| {
if retval.is_some() {
return Err(CompileError::MultipleReturns);
}
let mut ctx = Context::default();
let instructions_for_this_node = match item {
BodyItem::ExpressionStatement(node) => match KclValueGroup::from(node.expression) {
KclValueGroup::Single(value) => self.plan_to_compute_single(&mut ctx, value)?.instructions,
KclValueGroup::ArrayExpression(_) => todo!(),
KclValueGroup::ObjectExpression(_) => todo!(),
},
BodyItem::VariableDeclaration(node) => self.plan_to_bind(node)?,
BodyItem::ReturnStatement(node) => match KclValueGroup::from(node.argument) {
KclValueGroup::Single(value) => {
let EvalPlan { instructions, binding } = self.plan_to_compute_single(&mut ctx, value)?;
retval = Some(binding);
instructions
}
KclValueGroup::ArrayExpression(_) => todo!(),
KclValueGroup::ObjectExpression(_) => todo!(),
},
};
instructions.extend(instructions_for_this_node);
Ok((instructions, retval))
})
}
/// Emits instructions which, when run, compute a given KCL value and store it in memory.
/// Returns the instructions, and the destination address of the value.
fn plan_to_compute_single(&mut self, ctx: &mut Context, value: SingleValue) -> Result<EvalPlan, CompileError> {
match value {
SingleValue::KclNoneExpression(KclNone { start: _, end: _ }) => {
let address = self.next_addr.offset_by(1);
Ok(EvalPlan {
instructions: vec![Instruction::SetPrimitive {
address,
value: ept::Primitive::Nil,
}],
binding: EpBinding::Single(address),
})
}
SingleValue::FunctionExpression(expr) => {
let FunctionExpressionParts {
start: _,
end: _,
params_required,
params_optional,
body,
} = expr.into_parts().map_err(CompileError::BadParamOrder)?;
Ok(EvalPlan {
instructions: Vec::new(),
binding: EpBinding::from(KclFunction::UserDefined(UserDefinedFunction {
params_optional,
params_required,
body,
})),
})
}
SingleValue::Literal(expr) => {
let kcep_val = kcl_literal_to_kcep_literal(expr.value);
// KCEP primitives always have size of 1, because each address holds 1 primitive.
let size = 1;
let address = self.next_addr.offset_by(size);
Ok(EvalPlan {
instructions: vec![Instruction::SetPrimitive {
address,
value: kcep_val,
}],
binding: EpBinding::Single(address),
})
}
SingleValue::Identifier(expr) => {
// The KCL parser interprets bools as identifiers.
// Consider changing them to be KCL literals instead.
let b = if expr.name == "true" {
Some(true)
} else if expr.name == "false" {
Some(false)
} else {
None
};
if let Some(b) = b {
let address = self.next_addr.offset_by(1);
return Ok(EvalPlan {
instructions: vec![Instruction::SetPrimitive {
address,
value: ept::Primitive::Bool(b),
}],
binding: EpBinding::Single(address),
});
}
// This identifier is just duplicating a binding.
// So, don't emit any instructions, because the value has already been computed.
// Just return the address that it was stored at after being computed.
let previously_bound_to = self
.binding_scope
.get(&expr.name)
.ok_or(CompileError::Undefined { name: expr.name })?;
Ok(EvalPlan {
instructions: Vec::new(),
binding: previously_bound_to.clone(),
})
}
SingleValue::UnaryExpression(expr) => {
let operand = self.plan_to_compute_single(ctx, SingleValue::from(expr.argument))?;
let EpBinding::Single(binding) = operand.binding else {
return Err(CompileError::InvalidOperand(
"you tried to use a composite value (e.g. array or object) as the operand to some math",
));
};
let destination = self.next_addr.offset_by(1);
let mut plan = operand.instructions;
plan.push(Instruction::UnaryArithmetic {
arithmetic: ep::UnaryArithmetic {
operation: match expr.operator {
ast::types::UnaryOperator::Neg => ep::UnaryOperation::Neg,
ast::types::UnaryOperator::Not => ep::UnaryOperation::Not,
},
operand: ep::Operand::Reference(binding),
},
destination,
});
Ok(EvalPlan {
instructions: plan,
binding: EpBinding::Single(destination),
})
}
SingleValue::BinaryExpression(expr) => {
let l = self.plan_to_compute_single(ctx, SingleValue::from(expr.left))?;
let r = self.plan_to_compute_single(ctx, SingleValue::from(expr.right))?;
let EpBinding::Single(l_binding) = l.binding else {
return Err(CompileError::InvalidOperand(
"you tried to use a composite value (e.g. array or object) as the operand to some math",
));
};
let EpBinding::Single(r_binding) = r.binding else {
return Err(CompileError::InvalidOperand(
"you tried to use a composite value (e.g. array or object) as the operand to some math",
));
};
let destination = self.next_addr.offset_by(1);
let mut plan = Vec::with_capacity(l.instructions.len() + r.instructions.len() + 1);
plan.extend(l.instructions);
plan.extend(r.instructions);
plan.push(Instruction::BinaryArithmetic {
arithmetic: ep::BinaryArithmetic {
operation: match expr.operator {
ast::types::BinaryOperator::Add => ep::BinaryOperation::Add,
ast::types::BinaryOperator::Sub => ep::BinaryOperation::Sub,
ast::types::BinaryOperator::Mul => ep::BinaryOperation::Mul,
ast::types::BinaryOperator::Div => ep::BinaryOperation::Div,
ast::types::BinaryOperator::Mod => {
todo!("execution plan instruction set doesn't support Mod yet")
}
ast::types::BinaryOperator::Pow => {
todo!("execution plan instruction set doesn't support Pow yet")
}
},
operand0: ep::Operand::Reference(l_binding),
operand1: ep::Operand::Reference(r_binding),
},
destination,
});
Ok(EvalPlan {
instructions: plan,
binding: EpBinding::Single(destination),
})
}
SingleValue::CallExpression(expr) => {
// Make a plan to compute all the arguments to this call.
let (mut instructions, args) = expr.arguments.into_iter().try_fold(
(Vec::new(), Vec::new()),
|(mut acc_instrs, mut acc_args), argument| {
let EvalPlan {
instructions: new_instructions,
binding: arg,
} = match KclValueGroup::from(argument) {
KclValueGroup::Single(value) => self.plan_to_compute_single(ctx, value)?,
KclValueGroup::ArrayExpression(_) => todo!(),
KclValueGroup::ObjectExpression(_) => todo!(),
};
acc_instrs.extend(new_instructions);
acc_args.push(arg);
Ok((acc_instrs, acc_args))
},
)?;
// Look up the function being called.
let callee = match self.binding_scope.get_fn(&expr.callee.name) {
GetFnResult::Found(f) => f,
GetFnResult::NonCallable => {
return Err(CompileError::NotCallable {
name: expr.callee.name.clone(),
});
}
GetFnResult::NotFound => {
return Err(CompileError::Undefined {
name: expr.callee.name.clone(),
})
}
};
// Emit instructions to call that function with the given arguments.
use native_functions::Callable;
let EvalPlan {
instructions: eval_instrs,
binding,
} = match callee {
KclFunction::Id(f) => f.call(&mut self.next_addr, args)?,
KclFunction::StartSketchAt(f) => f.call(&mut self.next_addr, args)?,
KclFunction::Add(f) => f.call(&mut self.next_addr, args)?,
KclFunction::UserDefined(f) => {
let UserDefinedFunction {
params_optional,
params_required,
body: function_body,
} = f.clone();
let num_required_params = params_required.len();
self.binding_scope.add_scope();
// Bind the call's arguments to the names of the function's parameters.
let num_actual_params = args.len();
let mut arg_iter = args.into_iter();
let max_params = params_required.len() + params_optional.len();
if num_actual_params > max_params {
return Err(CompileError::TooManyArgs {
fn_name: "".into(),
maximum: max_params,
actual: num_actual_params,
});
}
// Bind required parameters
for param in params_required {
let arg = arg_iter.next().ok_or(CompileError::NotEnoughArgs {
fn_name: "".into(),
required: num_required_params,
actual: num_actual_params,
})?;
self.binding_scope.bind(param.identifier.name, arg);
}
// Bind optional parameters
for param in params_optional {
let Some(arg) = arg_iter.next() else {
break;
};
self.binding_scope.bind(param.identifier.name, arg);
}
let (instructions, retval) = self.build_plan(function_body)?;
let Some(retval) = retval else {
return Err(CompileError::NoReturnStmt);
};
self.binding_scope.remove_scope();
EvalPlan {
instructions,
binding: retval,
}
}
};
// Combine the "evaluate arguments" plan with the "call function" plan.
instructions.extend(eval_instrs);
Ok(EvalPlan { instructions, binding })
}
SingleValue::MemberExpression(mut expr) => {
let parse = move || {
let mut stack = Vec::new();
loop {
stack.push((expr.property, expr.computed));
match expr.object {
ast::types::MemberObject::MemberExpression(subexpr) => {
expr = subexpr;
}
ast::types::MemberObject::Identifier(id) => return (stack, id),
}
}
};
let (properties, id) = parse();
let name = id.name;
let mut binding = self.binding_scope.get(&name).ok_or(CompileError::Undefined { name })?;
for (property, computed) in properties {
if computed {
todo!("Support computed properties like '{:?}'", property);
} else {
binding = binding.property_of(property)?;
}
}
Ok(EvalPlan {
instructions: Vec::new(),
binding: binding.clone(),
})
}
SingleValue::PipeSubstitution(_expr) => {
if let Some(ref binding) = ctx.pipe_substitution {
Ok(EvalPlan {
instructions: Vec::new(),
binding: binding.clone(),
})
} else {
Err(CompileError::NotInPipeline)
}
}
SingleValue::PipeExpression(expr) => {
let mut bodies = expr.body.into_iter();
// Get the first expression (i.e. body) of the pipeline.
let first = bodies.next().expect("Pipe expression must have > 1 item");
let EvalPlan {
mut instructions,
binding: mut current_value,
} = match KclValueGroup::from(first) {
KclValueGroup::Single(v) => self.plan_to_compute_single(ctx, v)?,
KclValueGroup::ArrayExpression(_) => todo!(),
KclValueGroup::ObjectExpression(_) => todo!(),
};
// Handle the remaining bodies.
for body in bodies {
let value = match KclValueGroup::from(body) {
KclValueGroup::Single(v) => v,
KclValueGroup::ArrayExpression(_) => todo!(),
KclValueGroup::ObjectExpression(_) => todo!(),
};
// This body will probably contain a % (pipe substitution character).
// So it needs to know what the previous pipeline body's value is,
// to replace the % with that value.
ctx.pipe_substitution = Some(current_value.clone());
let EvalPlan {
instructions: instructions_for_this_body,
binding,
} = self.plan_to_compute_single(ctx, value)?;
instructions.extend(instructions_for_this_body);
current_value = binding;
}
// Before we return, clear the pipe substitution, because nothing outside this
// pipeline should be able to use it anymore.
ctx.pipe_substitution = None;
Ok(EvalPlan {
instructions,
binding: current_value,
})
}
}
}
/// Emits instructions which, when run, compute a given KCL value and store it in memory.
/// Returns the instructions.
/// Also binds the value to a name.
fn plan_to_bind(
&mut self,
declarations: ast::types::VariableDeclaration,
) -> Result<Vec<Instruction>, CompileError> {
let mut ctx = Context::default();
declarations
.declarations
.into_iter()
.try_fold(Vec::new(), |mut acc, declaration| {
let (instrs, binding) = self.plan_to_bind_one(&mut ctx, declaration.init)?;
self.binding_scope.bind(declaration.id.name, binding);
acc.extend(instrs);
Ok(acc)
})
}
fn plan_to_bind_one(
&mut self,
ctx: &mut Context,
value_being_bound: ast::types::Value,
) -> Result<(Vec<Instruction>, EpBinding), CompileError> {
match KclValueGroup::from(value_being_bound) {
KclValueGroup::Single(init_value) => {
// Simple! Just evaluate it, note where the final value will be stored in KCEP memory,
// and bind it to the KCL identifier.
let EvalPlan { instructions, binding } = self.plan_to_compute_single(ctx, init_value)?;
Ok((instructions, binding))
}
KclValueGroup::ArrayExpression(expr) => {
// First, emit a plan to compute each element of the array.
// Collect all the bindings from each element too.
let (instructions, bindings) = expr.elements.into_iter().try_fold(
(Vec::new(), Vec::new()),
|(mut acc_instrs, mut acc_bindings), element| {
match KclValueGroup::from(element) {
KclValueGroup::Single(value) => {
// If this element of the array is a single value, then binding it is
// straightforward -- you got a single binding, no need to change anything.
let EvalPlan { instructions, binding } = self.plan_to_compute_single(ctx, value)?;
acc_instrs.extend(instructions);
acc_bindings.push(binding);
}
KclValueGroup::ArrayExpression(expr) => {
// If this element of the array is _itself_ an array, then we need to
// emit a plan to calculate each element of this child array.
// Then we collect the child array's bindings, and bind them to one
// element of the parent array.
let binding = expr
.elements
.into_iter()
.try_fold(Vec::new(), |mut seq, child_element| {
let (instructions, binding) = self.plan_to_bind_one(ctx, child_element)?;
acc_instrs.extend(instructions);
seq.push(binding);
Ok(seq)
})
.map(EpBinding::Sequence)?;
acc_bindings.push(binding);
}
KclValueGroup::ObjectExpression(expr) => {
// If this element of the array is an object, then we need to
// emit a plan to calculate each value of each property of the object.
// Then we collect the bindings for each child value, and bind them to one
// element of the parent array.
let map = HashMap::with_capacity(expr.properties.len());
let binding = expr
.properties
.into_iter()
.try_fold(map, |mut map, property| {
let (instructions, binding) = self.plan_to_bind_one(ctx, property.value)?;
map.insert(property.key.name, binding);
acc_instrs.extend(instructions);
Ok(map)
})
.map(EpBinding::Map)?;
acc_bindings.push(binding);
}
};
Ok((acc_instrs, acc_bindings))
},
)?;
Ok((instructions, EpBinding::Sequence(bindings)))
}
KclValueGroup::ObjectExpression(expr) => {
// Convert the object to a sequence of key-value pairs.
let mut kvs = expr.properties.into_iter().map(|prop| (prop.key, prop.value));
let (instructions, each_property_binding) = kvs.try_fold(
(Vec::new(), HashMap::new()),
|(mut acc_instrs, mut acc_bindings), (key, value)| {
match KclValueGroup::from(value) {
KclValueGroup::Single(value) => {
let EvalPlan { instructions, binding } = self.plan_to_compute_single(ctx, value)?;
acc_instrs.extend(instructions);
acc_bindings.insert(key.name, binding);
}
KclValueGroup::ArrayExpression(expr) => {
// If this value of the object is an array, then emit a plan to calculate
// each element of that array. Collect their bindings, and bind them all
// under one property of the parent object.
let n = expr.elements.len();
let binding = expr
.elements
.into_iter()
.try_fold(Vec::with_capacity(n), |mut seq, child_element| {
let (instructions, binding) = self.plan_to_bind_one(ctx, child_element)?;
seq.push(binding);
acc_instrs.extend(instructions);
Ok(seq)
})
.map(EpBinding::Sequence)?;
acc_bindings.insert(key.name, binding);
}
KclValueGroup::ObjectExpression(expr) => {
// If this value of the object is _itself_ an object, then we need to
// emit a plan to calculate each value of each property of the child object.
// Then we collect the bindings for each child value, and bind them to one
// property of the parent object.
let n = expr.properties.len();
let binding = expr
.properties
.into_iter()
.try_fold(HashMap::with_capacity(n), |mut map, property| {
let (instructions, binding) = self.plan_to_bind_one(ctx, property.value)?;
map.insert(property.key.name, binding);
acc_instrs.extend(instructions);
Ok(map)
})
.map(EpBinding::Map)?;
acc_bindings.insert(key.name, binding);
}
};
Ok((acc_instrs, acc_bindings))
},
)?;
Ok((instructions, EpBinding::Map(each_property_binding)))
}
}
}
}
/// Every KCL literal value is equivalent to an Execution Plan value, and therefore can be
/// bound to some KCL name and Execution Plan address.
fn kcl_literal_to_kcep_literal(expr: LiteralValue) -> ept::Primitive {
match expr {
LiteralValue::IInteger(x) => ept::Primitive::NumericValue(NumericPrimitive::Integer(x)),
LiteralValue::Fractional(x) => ept::Primitive::NumericValue(NumericPrimitive::Float(x)),
LiteralValue::String(x) => ept::Primitive::String(x),
}
}
/// Instructions that can compute some value.
struct EvalPlan {
/// The instructions which will compute the value.
instructions: Vec<Instruction>,
/// Where the value will be stored.
binding: EpBinding,
}
/// Either an owned string, or a static string. Either way it can be read and moved around.
pub type String2 = std::borrow::Cow<'static, str>;
#[derive(Debug, Clone)]
struct UserDefinedFunction {
params_optional: Vec<ast::types::Parameter>,
params_required: Vec<ast::types::Parameter>,
body: ast::types::Program,
}
impl PartialEq for UserDefinedFunction {
fn eq(&self, other: &Self) -> bool {
self.params_optional == other.params_optional && self.params_required == other.params_required
}
}
impl Eq for UserDefinedFunction {}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
enum KclFunction {
Id(native_functions::Id),
StartSketchAt(native_functions::StartSketchAt),
Add(native_functions::Add),
UserDefined(UserDefinedFunction),
}
/// Context used when compiling KCL.
#[derive(Default, Debug)]
struct Context {
pipe_substitution: Option<EpBinding>,
}

View File

@ -0,0 +1,112 @@
//! Defines functions which are written in Rust, but called from KCL.
//! This includes some of the stdlib, e.g. `startSketchAt`.
//! But some other stdlib functions will be written in KCL.
use kcl_lib::std::sketch::PlaneData;
use kittycad_execution_plan::{Address, BinaryArithmetic, Instruction};
use kittycad_execution_plan_traits::Value;
use crate::{CompileError, EpBinding, EvalPlan};
/// The identity function. Always returns its first input.
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Id;
pub trait Callable {
fn call(&self, next_addr: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError>;
}
impl Callable for Id {
fn call(&self, _: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
if args.len() > 1 {
return Err(CompileError::TooManyArgs {
fn_name: "id".into(),
maximum: 1,
actual: args.len(),
});
}
let arg = args
.first()
.ok_or(CompileError::NotEnoughArgs {
fn_name: "id".into(),
required: 1,
actual: 0,
})?
.clone();
Ok(EvalPlan {
instructions: Vec::new(),
binding: arg,
})
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct StartSketchAt;
impl Callable for StartSketchAt {
fn call(&self, next_addr: &mut Address, _args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
let mut instructions = Vec::new();
// Store the plane.
let plane = PlaneData::XY.into_parts();
instructions.push(Instruction::SetValue {
address: next_addr.offset_by(plane.len()),
value_parts: plane,
});
// TODO: Get the plane ID from global context.
// TODO: Send this command:
// ModelingCmd::SketchModeEnable {
// animated: false,
// ortho: false,
// plane_id: plane.id,
// // We pass in the normal for the plane here.
// disable_camera_with_plane: Some(plane.z_axis.clone().into()),
// },
// TODO: Send ModelingCmd::StartPath at the given point.
// TODO (maybe): Store the SketchGroup in KCEP memory.
todo!()
}
}
/// A test function that adds two numbers.
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Add;
impl Callable for Add {
fn call(&self, next_address: &mut Address, mut args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
let len = args.len();
if len > 2 {
return Err(CompileError::TooManyArgs {
fn_name: "add".into(),
maximum: 2,
actual: len,
});
}
let not_enough_args = CompileError::NotEnoughArgs {
fn_name: "add".into(),
required: 2,
actual: len,
};
const ERR: &str = "cannot use composite values (e.g. array) as arguments to Add";
let EpBinding::Single(arg1) = args.pop().ok_or(not_enough_args.clone())? else {
return Err(CompileError::InvalidOperand(ERR));
};
let EpBinding::Single(arg0) = args.pop().ok_or(not_enough_args)? else {
return Err(CompileError::InvalidOperand(ERR));
};
let destination = next_address.offset_by(1);
Ok(EvalPlan {
instructions: vec![Instruction::BinaryArithmetic {
arithmetic: BinaryArithmetic {
operation: kittycad_execution_plan::BinaryOperation::Add,
operand0: kittycad_execution_plan::Operand::Reference(arg0),
operand1: kittycad_execution_plan::Operand::Reference(arg1),
},
destination,
}],
binding: EpBinding::Single(destination),
})
}
}

View File

@ -0,0 +1,745 @@
use ep::UnaryArithmetic;
use pretty_assertions::assert_eq;
use super::*;
fn must_plan(program: &str) -> (Vec<Instruction>, BindingScope) {
let tokens = kcl_lib::token::lexer(program);
let parser = kcl_lib::parser::Parser::new(tokens);
let ast = parser.ast().unwrap();
let mut p = Planner::new();
let (instrs, _) = p.build_plan(ast).unwrap();
(instrs, p.binding_scope)
}
fn should_not_compile(program: &str) -> CompileError {
let tokens = kcl_lib::token::lexer(program);
let parser = kcl_lib::parser::Parser::new(tokens);
let ast = parser.ast().unwrap();
let mut p = Planner::new();
p.build_plan(ast).unwrap_err()
}
#[test]
fn assignments() {
let program = "
let x = 1
let y = 2";
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 2i64.into(),
}
]
);
}
#[test]
fn bind_array() {
let program = r#"let x = [44, 55, "sixty-six"]"#;
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 44i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 55i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(2),
value: "sixty-six".to_owned().into(),
}
]
);
}
#[test]
fn bind_nested_array() {
let program = r#"let x = [44, [55, "sixty-six"]]"#;
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 44i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 55i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(2),
value: "sixty-six".to_owned().into(),
}
]
);
}
#[test]
fn bind_arrays_with_objects_elements() {
let program = r#"let x = [44, {a: 55, b: "sixty-six"}]"#;
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 44i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 55i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(2),
value: "sixty-six".to_owned().into(),
}
]
);
}
#[test]
fn statement_after_return() {
let program = "fn f = () => {
return 1
let x = 2
}
f()";
let err = should_not_compile(program);
assert_eq!(err, CompileError::MultipleReturns);
}
#[test]
fn name_not_found() {
// Users can't assign `y` to anything because `y` is undefined.
let err = should_not_compile("let x = y");
assert_eq!(err, CompileError::Undefined { name: "y".to_owned() });
}
#[test]
fn assign_bool() {
// Check that Grackle properly compiles KCL bools to EP bools.
for (str, val) in [("true", true), ("false", false)] {
let program = format!("let x = {str}");
let (plan, scope) = must_plan(&program);
assert_eq!(
plan,
vec![Instruction::SetPrimitive {
address: Address::ZERO,
value: val.into(),
}]
);
assert_eq!(scope.get("x"), Some(&EpBinding::Single(Address::ZERO)));
}
}
#[test]
fn aliases() {
let program = "
let x = 1
let y = x";
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into(),
}]
);
}
#[test]
fn use_native_function_add() {
let program = "let x = add(1,2)";
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into()
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 2i64.into()
},
Instruction::BinaryArithmetic {
arithmetic: ep::BinaryArithmetic {
operation: ep::BinaryOperation::Add,
operand0: ep::Operand::Reference(Address::ZERO),
operand1: ep::Operand::Reference(Address::ZERO.offset(1))
},
destination: Address::ZERO.offset(2),
}
]
);
}
#[test]
fn use_native_function_id() {
let program = "let x = id(2)";
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![Instruction::SetPrimitive {
address: Address::ZERO,
value: 2i64.into()
}]
);
}
#[test]
#[ignore = "haven't done computed properties yet"]
fn computed_array_index() {
let program = r#"
let array = ["a", "b", "c"]
let index = 1+1
let prop = array[index]
"#;
let (_plan, scope) = must_plan(program);
match scope.get("prop").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(*addr, Address::ZERO + 1);
}
other => {
panic!("expected 'prop' bound to 0x0 but it was bound to {other:?}");
}
}
}
#[test]
#[ignore = "haven't done computed properties yet"]
fn computed_member_expressions() {
let program = r#"
let obj = {x: 1, y: 2}
let index = "x"
let prop = obj[index]
"#;
let (_plan, scope) = must_plan(program);
match scope.get("prop").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(*addr, Address::ZERO + 1);
}
other => {
panic!("expected 'prop' bound to 0x0 but it was bound to {other:?}");
}
}
}
#[test]
fn member_expressions_object() {
let program = r#"
let obj = {x: 1, y: 2}
let prop = obj["y"]
"#;
let (_plan, scope) = must_plan(program);
match scope.get("prop").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(*addr, Address::ZERO + 1);
}
other => {
panic!("expected 'prop' bound to 0x0 but it was bound to {other:?}");
}
}
}
#[test]
fn member_expressions_array() {
let program = "
let array = [[1,2],[3,4]]
let first = array[0][0]
let last = array[1][1]
";
let (_plan, scope) = must_plan(program);
match scope.get("first").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(*addr, Address::ZERO);
}
other => {
panic!("expected 'number' bound to 0x0 but it was bound to {other:?}");
}
}
match scope.get("last").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(*addr, Address::ZERO + 3);
}
other => {
panic!("expected 'number' bound to 0x3 but it was bound to {other:?}");
}
}
}
#[test]
fn compile_flipped_sign() {
let program = "let x = 3
let y = -x";
let (plan, _scope) = must_plan(program);
let expected = vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 3i64.into(),
},
Instruction::UnaryArithmetic {
arithmetic: UnaryArithmetic {
operation: ep::UnaryOperation::Neg,
operand: ep::Operand::Reference(Address::ZERO),
},
destination: Address::ZERO + 1,
},
];
assert_eq!(plan, expected);
}
#[test]
fn add_literals() {
let program = "let x = 1 + 2";
let (plan, _scope) = must_plan(program);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into()
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 2i64.into()
},
Instruction::BinaryArithmetic {
arithmetic: ep::BinaryArithmetic {
operation: ep::BinaryOperation::Add,
operand0: ep::Operand::Reference(Address::ZERO),
operand1: ep::Operand::Reference(Address::ZERO.offset(1)),
},
destination: Address::ZERO.offset(2),
}
]
);
}
#[test]
fn add_vars() {
let program = "
let one = 1
let two = 2
let x = one + two";
let (plan, _bindings) = must_plan(program);
let addr0 = Address::ZERO;
let addr1 = Address::ZERO.offset(1);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: addr0,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: addr1,
value: 2i64.into(),
},
Instruction::BinaryArithmetic {
arithmetic: ep::BinaryArithmetic {
operation: ep::BinaryOperation::Add,
operand0: ep::Operand::Reference(addr0),
operand1: ep::Operand::Reference(addr1),
},
destination: Address::ZERO.offset(2),
}
]
);
}
#[test]
fn composite_binary_exprs() {
let program = "
let x = 1
let y = 2
let z = 3
let six = x + y + z
";
let (plan, _bindings) = must_plan(program);
let addr0 = Address::ZERO;
let addr1 = Address::ZERO.offset(1);
let addr2 = Address::ZERO.offset(2);
let addr3 = Address::ZERO.offset(3);
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: addr0,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: addr1,
value: 2i64.into(),
},
Instruction::SetPrimitive {
address: addr2,
value: 3i64.into(),
},
// Adds 1 + 2
Instruction::BinaryArithmetic {
arithmetic: ep::BinaryArithmetic {
operation: ep::BinaryOperation::Add,
operand0: ep::Operand::Reference(addr0),
operand1: ep::Operand::Reference(addr1),
},
destination: addr3,
},
// Adds `x` + 3, where `x` is (1 + 2)
Instruction::BinaryArithmetic {
arithmetic: ep::BinaryArithmetic {
operation: ep::BinaryOperation::Add,
operand0: ep::Operand::Reference(addr3),
operand1: ep::Operand::Reference(addr2),
},
destination: Address::ZERO.offset(4),
}
]
);
}
#[test]
fn use_kcl_functions_zero_params() {
let (plan, scope) = must_plan(
"fn triple = () => { return 123 }
let x = triple()",
);
assert_eq!(
plan,
vec![Instruction::SetPrimitive {
address: Address::ZERO,
value: 123i64.into()
}]
);
match scope.get("x").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(addr, &Address::ZERO);
}
other => {
panic!("expected 'x' bound to an address but it was bound to {other:?}");
}
}
}
#[test]
fn use_kcl_functions_with_optional_params() {
for (i, program) in ["fn triple = (x, y?) => { return x*3 }
let x = triple(1, 888)"]
.into_iter()
.enumerate()
{
let (plan, scope) = must_plan(program);
let destination = Address::ZERO + 3;
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 1,
value: 888i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 2,
value: 3i64.into(),
},
Instruction::BinaryArithmetic {
arithmetic: ep::BinaryArithmetic {
operation: ep::BinaryOperation::Mul,
operand0: ep::Operand::Reference(Address::ZERO),
operand1: ep::Operand::Reference(Address::ZERO + 2)
},
destination,
}
],
"failed test {i}"
);
match scope.get("x").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(addr, &destination, "failed test {i}");
}
other => {
panic!("expected 'x' bound to an address but it was bound to {other:?}, so failed test {i}");
}
}
}
}
#[test]
fn use_kcl_functions_with_too_many_params() {
let program = "fn triple = (x, y?) => { return x*3 }
let x = triple(1, 2, 3)";
let err = should_not_compile(program);
assert!(matches!(
err,
CompileError::TooManyArgs {
maximum: 2,
actual: 3,
..
}
))
}
#[test]
fn use_kcl_function_as_return_value() {
let program = "fn twotwotwo = () => {
return () => { return 222 }
}
let f = twotwotwo()
let x = f()";
let (plan, scope) = must_plan(program);
match scope.get("x").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(addr, &Address::ZERO);
}
other => {
panic!("expected 'x' bound to an address but it was bound to {other:?}, so failed test");
}
}
assert_eq!(
plan,
vec![Instruction::SetPrimitive {
address: Address::ZERO,
value: 222i64.into()
}]
)
}
#[test]
fn define_recursive_function() {
let program = "fn add_infinitely = (i) => {
return add_infinitely(i+1)
}";
let (plan, _scope) = must_plan(program);
assert_eq!(plan, Vec::new())
}
#[test]
fn use_kcl_function_as_param() {
let program = "fn wrapper = (f) => {
return f()
}
fn twotwotwo = () => {
return 222
}
let x = wrapper(twotwotwo)";
let (plan, scope) = must_plan(program);
match scope.get("x").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(addr, &Address::ZERO);
}
other => {
panic!("expected 'x' bound to an address but it was bound to {other:?}, so failed test");
}
}
assert_eq!(
plan,
vec![Instruction::SetPrimitive {
address: Address::ZERO,
value: 222i64.into()
}]
)
}
#[test]
fn use_kcl_functions_with_params() {
for (i, program) in [
"fn triple = (x) => { return x*3 }
let x = triple(1)",
"fn triple = (x,y?) => { return x*3 }
let x = triple(1)",
]
.into_iter()
.enumerate()
{
let (plan, scope) = must_plan(program);
let destination = Address::ZERO + 2;
assert_eq!(
plan,
vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 1,
value: 3i64.into(),
},
Instruction::BinaryArithmetic {
arithmetic: ep::BinaryArithmetic {
operation: ep::BinaryOperation::Mul,
operand0: ep::Operand::Reference(Address::ZERO),
operand1: ep::Operand::Reference(Address::ZERO.offset(1))
},
destination,
}
],
"failed test {i}"
);
match scope.get("x").unwrap() {
EpBinding::Single(addr) => {
assert_eq!(addr, &destination, "failed test {i}");
}
other => {
panic!("expected 'x' bound to an address but it was bound to {other:?}, so failed test {i}");
}
}
}
}
#[test]
fn pipe_substitution_outside_pipe_expression() {
let program = "let x = add(1, %)";
let err = should_not_compile(program);
assert!(matches!(err, CompileError::NotInPipeline));
}
#[test]
fn unsugar_pipe_expressions() {
// These two programs should be equivalent,
// because that's just the definition of the |> operator.
let program2 = "
fn double = (x) => { return x * 2 }
fn triple = (x) => { return x * 3 }
let x = 1 |> double(%) |> triple(%) // should be 6
";
let program1 = "
fn double = (x) => { return x * 2 }
fn triple = (x) => { return x * 3 }
let x = triple(double(1)) // should be 6
";
// So, check that they are.
let (plan1, _) = must_plan(program1);
let (plan2, _) = must_plan(program2);
assert_eq!(plan1, plan2);
}
#[test]
fn define_kcl_functions() {
let (plan, scope) = must_plan("fn triple = (x) => { return x * 3 }");
assert!(plan.is_empty());
match scope.get("triple").unwrap() {
EpBinding::Function(KclFunction::UserDefined(expr)) => {
assert!(expr.params_optional.is_empty());
assert_eq!(expr.params_required.len(), 1);
}
other => {
panic!("expected 'triple' bound to a user-defined KCL function but it was bound to {other:?}");
}
}
}
#[test]
fn aliases_dont_affect_plans() {
let (plan1, _) = must_plan(
"let one = 1
let two = 2
let x = one + two",
);
let (plan2, _) = must_plan(
"let one = 1
let two = 2
let y = two
let x = one + y",
);
assert_eq!(plan1, plan2);
}
#[test]
fn store_object() {
let program = "const x0 = {a: 1, b: 2, c: {d: 3}}";
let (actual, bindings) = must_plan(program);
let expected = vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 2i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(2),
value: 3i64.into(),
},
];
assert_eq!(actual, expected);
eprintln!("{bindings:#?}");
assert_eq!(
bindings.get("x0").unwrap(),
&EpBinding::Map(HashMap::from([
("a".to_owned(), EpBinding::Single(Address::ZERO),),
("b".to_owned(), EpBinding::Single(Address::ZERO.offset(1))),
(
"c".to_owned(),
EpBinding::Map(HashMap::from([(
"d".to_owned(),
EpBinding::Single(Address::ZERO.offset(2))
)]))
),
]))
)
}
#[test]
fn store_object_with_array_property() {
let program = "const x0 = {a: 1, b: [2, 3]}";
let (actual, bindings) = must_plan(program);
let expected = vec![
Instruction::SetPrimitive {
address: Address::ZERO,
value: 1i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(1),
value: 2i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO.offset(2),
value: 3i64.into(),
},
];
assert_eq!(actual, expected);
eprintln!("{bindings:#?}");
assert_eq!(
bindings.get("x0").unwrap(),
&EpBinding::Map(HashMap::from([
("a".to_owned(), EpBinding::Single(Address::ZERO),),
(
"b".to_owned(),
EpBinding::Sequence(vec![
EpBinding::Single(Address::ZERO.offset(1)),
EpBinding::Single(Address::ZERO.offset(2)),
])
),
]))
)
}
#[ignore = "haven't done API calls or stdlib yet"]
#[test]
fn stdlib_api_calls() {
let program = "const x0 = startSketchAt([0, 0])
const x1 = line([0, 10], x0)
const x2 = line([10, 0], x1)
const x3 = line([0, -10], x2)
const x4 = line([0, 0], x3)
const x5 = close(x4)
const x6 = extrude(20, x5)
show(x6)";
must_plan(program);
}

View File

@ -21,15 +21,18 @@ databake = { version = "0.1.7", features = ["derive"] }
derive-docs = { version = "0.1.5" }
# derive-docs = { path = "../derive-docs" }
kittycad = { workspace = true }
kittycad-execution-plan-macros = { workspace = true }
kittycad-execution-plan-traits = { workspace = true }
lazy_static = "1.4.0"
parse-display = "0.8.2"
schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] }
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
thiserror = "1.0.50"
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] }
ts-rs = { version = "7", features = ["uuid-impl"] }
uuid = { version = "1.6.1", features = ["v4", "js", "serde"] }
winnow = "0.5.18"
either = "1.6.1"
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.65" }

View File

@ -19,6 +19,7 @@ use crate::{
parser::PIPE_OPERATOR,
std::{kcl_stdlib::KclStdLibFn, FunctionKind},
};
use crate::executor::PathToNode;
mod literal_value;
mod none;
@ -1433,6 +1434,7 @@ impl From<Literal> for MemoryItem {
value: JValue::from(literal.value.clone()),
meta: vec![Metadata {
source_range: literal.into(),
path_to_node: vec![],
}],
})
}
@ -1444,6 +1446,7 @@ impl From<&Box<Literal>> for MemoryItem {
value: JValue::from(literal.value.clone()),
meta: vec![Metadata {
source_range: literal.into(),
path_to_node: vec![],
}],
})
}
@ -1641,7 +1644,7 @@ impl ArrayExpression {
Value::UnaryExpression(unary_expression) => unary_expression.get_result(memory, pipe_info, ctx).await?,
Value::ObjectExpression(object_expression) => object_expression.execute(memory, pipe_info, ctx).await?,
Value::ArrayExpression(array_expression) => array_expression.execute(memory, pipe_info, ctx).await?,
Value::PipeExpression(pipe_expression) => pipe_expression.get_result(memory, pipe_info, ctx).await?,
Value::PipeExpression(pipe_expression) => pipe_expression.get_result(memory, pipe_info, ctx, vec![]).await?,
Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
@ -1665,6 +1668,7 @@ impl ArrayExpression {
value: results.into(),
meta: vec![Metadata {
source_range: self.into(),
path_to_node: vec![],
}],
}))
}
@ -1794,7 +1798,7 @@ impl ObjectExpression {
Value::UnaryExpression(unary_expression) => unary_expression.get_result(memory, pipe_info, ctx).await?,
Value::ObjectExpression(object_expression) => object_expression.execute(memory, pipe_info, ctx).await?,
Value::ArrayExpression(array_expression) => array_expression.execute(memory, pipe_info, ctx).await?,
Value::PipeExpression(pipe_expression) => pipe_expression.get_result(memory, pipe_info, ctx).await?,
Value::PipeExpression(pipe_expression) => pipe_expression.get_result(memory, pipe_info, ctx, vec![]).await?,
Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
@ -1822,6 +1826,7 @@ impl ObjectExpression {
value: object.into(),
meta: vec![Metadata {
source_range: self.into(),
path_to_node: vec![],
}],
}))
}
@ -2031,6 +2036,7 @@ impl MemberExpression {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
path_to_node: vec![],
}],
}))
} else {
@ -2087,6 +2093,7 @@ impl MemberExpression {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
path_to_node: vec![],
}],
}))
} else {
@ -2251,6 +2258,7 @@ impl BinaryExpression {
value,
meta: vec![Metadata {
source_range: self.into(),
path_to_node: vec![],
}],
}));
}
@ -2272,6 +2280,7 @@ impl BinaryExpression {
value,
meta: vec![Metadata {
source_range: self.into(),
path_to_node: vec![],
}],
}))
}
@ -2435,6 +2444,7 @@ impl UnaryExpression {
value: (-(num)).into(),
meta: vec![Metadata {
source_range: self.into(),
path_to_node: vec![],
}],
}))
}
@ -2564,11 +2574,12 @@ impl PipeExpression {
memory: &mut ProgramMemory,
pipe_info: &mut PipeInfo,
ctx: &ExecutorContext,
path_to_node: PathToNode,
) -> Result<MemoryItem, KclError> {
// Reset the previous results.
pipe_info.previous_results = vec![];
pipe_info.index = 0;
execute_pipe_body(memory, &self.body, pipe_info, self.into(), ctx).await
execute_pipe_body(memory, &self.body, pipe_info, self.into(), ctx, path_to_node).await
}
/// Rename all identifiers that have the old name to the new given name.
@ -2586,6 +2597,8 @@ async fn execute_pipe_body(
pipe_info: &mut PipeInfo,
source_range: SourceRange,
ctx: &ExecutorContext,
path_to_node: PathToNode,
) -> Result<MemoryItem, KclError> {
if pipe_info.index == body.len() {
pipe_info.is_in_pipe = false;
@ -2631,7 +2644,7 @@ async fn execute_pipe_body(
}
/// Parameter of a KCL function.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
#[serde(tag = "type")]
@ -2655,6 +2668,23 @@ pub struct FunctionExpression {
impl_value_meta!(FunctionExpression);
pub struct FunctionExpressionParts {
pub start: usize,
pub end: usize,
pub params_required: Vec<Parameter>,
pub params_optional: Vec<Parameter>,
pub body: Program,
}
#[derive(Debug, PartialEq, Clone)]
pub struct RequiredParamAfterOptionalParam(pub Parameter);
impl std::fmt::Display for RequiredParamAfterOptionalParam {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "KCL functions must declare any optional parameters after all the required parameters. But your required parameter {} is _after_ an optional parameter. You must move it to before the optional parameters instead.", self.0.identifier.name)
}
}
impl FunctionExpression {
/// Function expressions don't really apply.
pub fn get_constraint_level(&self) -> ConstraintLevel {
@ -2663,6 +2693,36 @@ impl FunctionExpression {
}
}
pub fn into_parts(self) -> Result<FunctionExpressionParts, RequiredParamAfterOptionalParam> {
let Self {
start,
end,
params,
body,
} = self;
let mut params_required = Vec::with_capacity(params.len());
let mut params_optional = Vec::with_capacity(params.len());
for param in params {
if param.optional {
params_optional.push(param);
} else {
if !params_optional.is_empty() {
return Err(RequiredParamAfterOptionalParam(param));
}
params_required.push(param);
}
}
params_required.shrink_to_fit();
params_optional.shrink_to_fit();
Ok(FunctionExpressionParts {
start,
end,
params_required,
params_optional,
body,
})
}
/// Required parameters must be declared before optional parameters.
/// This gets all the required parameters.
pub fn required_params(&self) -> &[Parameter] {

View File

@ -42,6 +42,7 @@ pub struct StdLibFnArg {
/// The type of the argument.
pub type_: String,
/// The schema of the argument.
#[ts(type = "any")]
pub schema: schemars::schema::Schema,
/// If the argument is required.
pub required: bool,

View File

@ -5,11 +5,13 @@ use std::{collections::HashMap, sync::Arc};
use anyhow::Result;
use async_recursion::async_recursion;
use kittycad::types::{Color, ModelingCmd, Point3D};
use kittycad_execution_plan_macros::ExecutionPlanValue;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value as JValue;
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
// use either::Either;
use crate::{
ast::types::{BodyItem, FunctionExpression, KclNone, Value},
@ -286,6 +288,7 @@ impl DefaultPlanes {
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct UserVal {
#[ts(type = "any")]
pub value: serde_json::Value,
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
@ -610,7 +613,7 @@ impl Point2d {
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema, ExecutionPlanValue)]
#[ts(export)]
pub struct Point3d {
pub x: f64,
@ -630,6 +633,18 @@ impl From<Point3d> for kittycad::types::Point3D {
}
}
/// number or string
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub enum NumberOrString {
Num(i32), // assuming 'number' is equivalent to a 32-bit integer
Str(String),
}
/// PathToNode
pub type PathToNode = Vec<(NumberOrString, String)>;
/// Metadata.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -637,11 +652,16 @@ impl From<Point3d> for kittycad::types::Point3D {
pub struct Metadata {
/// The source range.
pub source_range: SourceRange,
/// The path to node for this memory Item
pub path_to_node: PathToNode,
}
impl From<SourceRange> for Metadata {
fn from(source_range: SourceRange) -> Self {
Self { source_range }
Self {
source_range,
path_to_node: Vec::new()
}
}
}
@ -827,12 +847,17 @@ pub async fn execute(
) -> Result<ProgramMemory, KclError> {
let mut pipe_info = PipeInfo::default();
// let path_to_Node: PathToNode = vec![("body".to_string(), "".to_string())];
let path_to_node: PathToNode = vec![(NumberOrString::Str("body".to_string()), "".to_string())];
// Iterate over the body of the program.
for statement in &program.body {
for (index, statement) in program.body.iter().enumerate() {
let mut with_body_path_to_node = path_to_node.clone();
with_body_path_to_node.push((NumberOrString::Num(index as i32), "index".to_string()));
match statement {
BodyItem::ExpressionStatement(expression_statement) => {
if let Value::PipeExpression(pipe_expr) = &expression_statement.expression {
pipe_expr.get_result(memory, &mut pipe_info, ctx).await?;
pipe_expr.get_result(memory, &mut pipe_info, ctx, with_body_path_to_node).await?;
} else if let Value::CallExpression(call_expr) = &expression_statement.expression {
let fn_name = call_expr.callee.name.to_string();
let mut args: Vec<MemoryItem> = Vec::new();
@ -903,10 +928,15 @@ pub async fn execute(
}
}
BodyItem::VariableDeclaration(variable_declaration) => {
for declaration in &variable_declaration.declarations {
for (index, declaration) in variable_declaration.declarations.iter().enumerate() {
let var_name = declaration.id.name.to_string();
let source_range: SourceRange = declaration.init.clone().into();
let metadata = Metadata { source_range };
let mut with_dec_path_to_node = with_body_path_to_node.clone();
with_dec_path_to_node.push((NumberOrString::Str("declarations".to_string()), "VariableDeclaration".to_string()));
with_dec_path_to_node.push((NumberOrString::Num(index as i32), "index".to_string()));
with_dec_path_to_node.push((NumberOrString::Str("init".to_string()), "".to_string()));
let metadata = Metadata { source_range, path_to_node: with_dec_path_to_node.clone() };
match &declaration.init {
Value::None(none) => {
@ -961,7 +991,7 @@ pub async fn execute(
memory.add(&var_name, result, source_range)?;
}
Value::PipeExpression(pipe_expression) => {
let result = pipe_expression.get_result(memory, &mut pipe_info, ctx).await?;
let result = pipe_expression.get_result(memory, &mut pipe_info, ctx, with_dec_path_to_node).await?;
memory.add(&var_name, result, source_range)?;
}
Value::PipeSubstitution(pipe_substitution) => {
@ -1025,7 +1055,7 @@ pub async fn execute(
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::PipeExpression(pipe_expr) => {
let result = pipe_expr.get_result(memory, &mut pipe_info, ctx).await?;
let result = pipe_expr.get_result(memory, &mut pipe_info, ctx, with_body_path_to_node).await?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::PipeSubstitution(_) => {}

View File

@ -2721,7 +2721,7 @@ show(b1)
show(b2)"#;
let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
parser.ast().unwrap();
dbg!(parser.ast().unwrap());
}
#[test]

View File

@ -190,6 +190,7 @@ impl Args {
value: j,
meta: vec![Metadata {
source_range: self.source_range,
path_to_node: Vec::new()
}],
}))
}

View File

@ -3,6 +3,7 @@
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::{Angle, ModelingCmd, Point3D};
use kittycad_execution_plan_macros::ExecutionPlanValue;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -648,7 +649,7 @@ async fn inner_start_sketch_at(data: LineData, args: Args) -> Result<Box<SketchG
}
/// Data for a plane.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, ExecutionPlanValue)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum PlaneData {
@ -1031,10 +1032,8 @@ async fn inner_arc(data: ArcData, sketch_group: Box<SketchGroup>, args: Args) ->
ModelingCmd::ExtendPath {
path: sketch_group.id,
segment: kittycad::types::PathSegment::Arc {
angle_start: angle_start.degrees(),
angle_end: angle_end.degrees(),
start: Some(angle_start),
end: Some(angle_end),
start: angle_start,
end: angle_end,
center: center.into(),
radius,
relative: false,

View File

@ -237,6 +237,7 @@ async fn serial_test_execute_cylinder() {
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "currently stack overflows"]
async fn serial_test_execute_kittycad_svg() {
let code = include_str!("inputs/kittycad_svg.kcl");

View File

@ -4590,15 +4590,10 @@ flux@^4.0.1:
fbemitter "^3.0.0"
fbjs "^3.0.1"
follow-redirects@^1.0.0, follow-redirects@^1.14.8:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
follow-redirects@^1.15.0:
version "1.15.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
follow-redirects@^1.0.0, follow-redirects@^1.14.8, follow-redirects@^1.15.0:
version "1.15.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
for-each@^0.3.3:
version "0.3.3"
@ -8193,10 +8188,10 @@ vite-tsconfig-paths@^4.2.1:
globrex "^0.1.2"
tsconfck "^2.1.0"
"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0", vite@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26"
integrity sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==
"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0", vite@^4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.2.tgz#d6ea8610e099851dad8c7371599969e0f8b97e82"
integrity sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==
dependencies:
esbuild "^0.18.10"
postcss "^8.4.27"