Compare commits

..

25 Commits

Author SHA1 Message Date
c84c4ab578 Stress Grackle with lambda calculus gymnastics 2024-01-23 09:32:17 -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
bc6407be6e Cut release v0.14.0 (#1229)
* Cut release v0.14.0

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

* To revert: add test-json stage

* Revert "To revert: add test-json stage"

This reverts commit cf04583e7a.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-20 11:26:11 -05:00
038409124a Frank branding tweaks (#1235)
* Naming tweaks

* Update heading font to be owners

* Update app icon

* Update Tauri App title

* Fix sign in page (#1232)

* Change to Zoo Modeling App, CI fixes (#1238)

* Replace website urls for dl.zoo.dev

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2023-12-19 14:19:34 -05:00
d5567f8602 Use derive-docs from crates.io (#1237) 2023-12-19 11:24:44 -06:00
80 changed files with 3583 additions and 337 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,6 +1,6 @@
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://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
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224

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

@ -104,7 +104,7 @@ jobs:
if: github.event_name == 'schedule'
run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
echo "$(jq --arg url 'https://dl.kittycad.io/releases/modeling-app/nightly/last_update.json' \
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json' \
'.tauri.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- uses: actions/upload-artifact@v3
@ -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' }}
@ -257,7 +259,7 @@ jobs:
export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri
env:
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/kittycad-modeling"
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app"
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
@ -271,26 +273,24 @@ jobs:
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
steps:
- uses: actions/download-artifact@v3
- name: Generate the update static endpoint
run: |
ls -l artifact/*/*itty*
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://${BUCKET_DIR}/${VERSION}
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
--arg linux_sig "$LINUX_SIG" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
--arg darwin_url "$RELEASE_DIR/macos/Zoo%20Modeling%20App.app.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi.zip" \
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi.zip" \
'{
"version": $version,
"pub_date": $pub_date,
@ -304,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
@ -318,14 +314,13 @@ jobs:
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage" \
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi" \
--arg darwin_url "$RELEASE_DIR/dmg/Zoo%20Modeling%20App_${VERSION_NO_V}_universal.dmg" \
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi" \
'{
"version": $version,
"pub_date": $pub_date,
@ -334,9 +329,6 @@ jobs:
"dmg-universal": {
"url": $darwin_url
},
"appimage-x86_64": {
"url": $linux_url
},
"msi-x86_64": {
"url": $windows_url
}
@ -358,7 +350,7 @@ jobs:
uses: google-github-actions/upload-cloud-storage@v2.0.0
with:
path: artifact
glob: '*/*itty*'
glob: '*/Zoo*'
parent: false
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
@ -378,4 +370,4 @@ jobs:
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v1
with:
files: artifact/*/*itty*
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 57 KiB

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

@ -11,8 +11,13 @@
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<script defer data-domain="app.kittycad.io" src="https://plausible.corp.kittycad.io/js/script.js"></script>
<title>Modeling App</title>
<link rel="stylesheet" href="https://use.typekit.net/zzv8rvm.css" />
<script
defer
data-domain="app.zoo.dev"
src="https://plausible.corp.zoo.dev/js/script.js"
></script>
<title>Zoo Modeling App</title>
</head>
<body class="body-bg">
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.13.0",
"version": "0.14.0",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.10.2",
@ -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",

View File

@ -4,9 +4,9 @@
First off, thank you so much for your interest in being a part of the closed Alpha program! We are thrilled to have others use our product and see what you build with it (and truthfully, how you break it too).
### KittyCAD Modeling App (KCMA)
### Zoo Modeling App (ZMA)
What we are introducing to you is our KittyCAD Modeling App (KCMA). KCMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. KCMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
What we are introducing to you is our Zoo Modeling App (ZMA). ZMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. ZMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
### Why Code?
@ -18,11 +18,11 @@ Plenty of you have professional CAD experience, and may not understand why codin
- Reproducibility
- Easier integration with other tools
### Before You Use KCMA
### Before You Use ZMA
Before you dive straight into the app, we wanted to lay some expectations out for you.
- KCMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. KCMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
- ZMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. ZMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
- For a list of all scripting functions, please reference our [documentation](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md). For a basic rundown of our types, please reference [this document](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/types.md).
- With that being said, we have created an external new features list in [GH Discussions](https://github.com/KittyCAD/modeling-app/discussions). For our current priority list, please click [here](https://github.com/KittyCAD/modeling-app/blob/main/public/roadmap.md). Please upvote any features in the GH Discussions page that you would like to see implemented first. We will prioritize the highest upvoted items or items that are foundational for other features on the list. You can also add your own, but we will review it to make sure its not a duplicate or its feasible for the current state of the app.
- Please report any and all bugs/issues you find. Even the smallest bugs are important! You can report them in a GH Issue [here](https://github.com/KittyCAD/modeling-app/issues/new). You are more than welcome to link your GH Issue in the **bugs** section of our Discord, but if you want to discuss the bug further, please keep that in the GH Issue thread. Please include the severity of the bug in your GH Issue ticket (High, Medium, or Low). If you are having trouble deciding what severity the bug is, use this guideline:

View File

@ -1,6 +1,6 @@
{
"short_name": "KCMA",
"name": "KittyCAD Modeling App",
"short_name": "ZMA",
"name": "Zoo Modeling App",
"icons": [
{
"src": "favicon.ico",

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@ -6,8 +6,8 @@
"distDir": "../build"
},
"package": {
"productName": "kittycad-modeling",
"version": "0.13.0"
"productName": "zoo-modeling-app",
"version": "0.14.0"
},
"tauri": {
"allowlist": {
@ -84,7 +84,7 @@
"fullscreen": false,
"height": 1200,
"resizable": true,
"title": "KittyCAD Modeling",
"title": "Zoo Modeling App",
"width": 1800
}
]

View File

@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "KittyCAD Modeling"
"productName": "Zoo Modeling App"
}
}

View File

@ -4,7 +4,7 @@
"updater": {
"active": true,
"endpoints": [
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
"https://dl.zoo.dev/releases/modeling-app/last_update.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"

View File

@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "KittyCAD Modeling"
"productName": "Zoo Modeling App"
}
}

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

@ -19,7 +19,7 @@ const DownloadAppBanner = () => {
<div className="max-w-3xl mx-auto">
<div className="flex gap-2 justify-between items-start">
<h2 className="text-xl font-bold mb-4">
Zoo Modeling App is better as a desktop app!
Modeling App is better as a desktop app!
</h2>
<ActionButton
Element="button"

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

@ -6,9 +6,7 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
@apply font-sans;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@apply text-chalkboard-110;
@ -17,6 +15,15 @@ body {
scrollbar-color: var(--color-chalkboard-20) var(--color-chalkboard-40);
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-display;
}
.body-bg {
@apply bg-chalkboard-10;
}

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

@ -10,7 +10,7 @@ import { isTauri } from './isTauri'
import { ProjectWithEntryPointMetadata } from '../Router'
import { metadata } from 'tauri-plugin-fs-extra-api'
const PROJECT_FOLDER = 'kittycad-modeling-projects'
const PROJECT_FOLDER = 'zoo-modeling-app-projects'
export const FILE_EXT = '.kcl'
export const PROJECT_ENTRYPOINT = 'main' + FILE_EXT
const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s
@ -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: {
@ -177,7 +184,7 @@ const Home = () => {
<AppHeader showToolbar={false} />
<div className="w-full max-w-5xl px-4 mx-auto my-24 overflow-y-auto lg:px-0">
<section className="flex justify-between">
<h1 className="text-3xl text-bold">Your Projects</h1>
<h1 className="text-3xl font-bold">Your Projects</h1>
<div className="flex gap-2 items-center">
<small>Sort by</small>
<ActionButton
@ -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

@ -18,7 +18,7 @@ export default function CmdK() {
(buttonDownInStream ? '' : ' pointer-events-auto')
}
>
<h2 className="text-2xl">Command Bar</h2>
<h2 className="text-2xl font-bold">Command Bar</h2>
<p className="my-4">
Press{' '}
{platformName === 'darwin' ? (

View File

@ -22,7 +22,7 @@ export default function CodeEditor() {
}
>
<section className="flex-1">
<h2 className="text-2xl">
<h2 className="text-2xl font-bold">
Editing code with <code>kcl</code>
</h2>
<p className="my-4">

View File

@ -18,7 +18,7 @@ export default function Export() {
}
>
<section className="flex-1">
<h2 className="text-2xl">Export</h2>
<h2 className="text-2xl font-bold">Export</h2>
<p className="my-4">
Try opening the project menu and clicking "Export Model".
</p>

View File

@ -22,7 +22,7 @@ export default function InteractiveNumbers() {
}
>
<section className="flex-1 overflow-y-auto mb-6">
<h2 className="text-2xl">Interactive Numbers</h2>
<h2 className="text-2xl font-bold">Interactive Numbers</h2>
<p className="my-4">
Let's do a little bit of hybrid editing to this part.
</p>

View File

@ -34,7 +34,9 @@ export default function ParametricModeling() {
}
>
<section className="flex-1 overflow-y-auto mb-6">
<h2 className="text-2xl">Towards true parametric modeling</h2>
<h2 className="text-2xl font-bold">
Towards true parametric modeling
</h2>
<p className="my-4">
This example script shows how having access to the code
representation of a part can allow us to do things that are tedious

View File

@ -18,12 +18,12 @@ export default function ProjectMenu() {
}
>
<section className="flex-1">
<h2 className="text-2xl">Project Menu</h2>
<h2 className="text-2xl font-bold">Project Menu</h2>
<p className="my-4">
Click on Kitt in the upper left to open the project menu. You can
only {isTauri() && 'go home or '}export your modelwhich we'll talk
about next—for now. We'll add more options here soon, especially as
we add support for multi-file assemblies.
Click on the Zoo logo in the upper left to open the project menu.
You can only {isTauri() && 'go home or '}export your modelwhich
we'll talk about next—for now. We'll add more options here soon,
especially as we add support for multi-file assemblies.
</p>
</section>
<OnboardingButtons

View File

@ -17,23 +17,23 @@ export default function Streaming() {
}
>
<section className="flex-1">
<h2 className="text-2xl">Streaming Video</h2>
<h2 className="text-2xl font-bold">Streaming Video</h2>
<p className="my-4">
The 3D view is not running on your computer. Instead, our
infrastructure spins up the Zoo Geometry Engine on a remote GPU, Zoo
infrastructure spins up our Geometry Engine on a remote GPU,
Modeling App sends it a series of commands via Websockets and
WebRTC, and the Geometry Engine sends back a video stream of the 3D
view.
</p>
<p className="my-4">
This means that you could run Zoo Modeling App on a Chromebook, a
This means that you could run our Modeling App on a Chromebook, a
tablet, or even a phone, as long as you have a good internet
connection.
</p>
<p className="my-4">
It also means that whatever tools you build on top of the Zoo
Geometry Engine will be able to run on any device with a browser,
and you won't have to worry about the performance of the device.
It also means that whatever tools you build on top of our Geometry
Engine will be able to run on any device with a browser, and you
won't have to worry about the performance of the device.
</p>
</section>
<OnboardingButtons

View File

@ -17,7 +17,7 @@ export default function UserMenu() {
}
>
<section className="flex-1">
<h2 className="text-2xl">User Menu</h2>
<h2 className="text-2xl font-bold">User Menu</h2>
<p className="my-4">
Click your avatar on the upper right to open the user menu. You can
change your settings, sign out, or request a feature.

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>
</>
@ -326,7 +329,7 @@ export function SettingsSection({
}
>
<div className="w-80">
<h2 className="text-2xl">{title}</h2>
<h2 className="text-2xl font-bold">{title}</h2>
<p className="mt-2 text-sm">{description}</p>
</div>
<div>{children}</div>

View File

@ -8,6 +8,11 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { APP_NAME } from 'lib/constants'
const SignIn = () => {
const getLogoTheme = () =>
theme === Themes.Light ||
(theme === Themes.System && getSystemTheme() === Themes.Light)
? '-dark'
: ''
const {
auth: { send },
settings: {
@ -35,15 +40,10 @@ const SignIn = () => {
<div className="max-w-2xl mx-auto">
<div>
<img
src={`/kittycad-logomark${
appliedTheme === Themes.Dark ? '-light' : ''
}.svg`}
alt="KittyCAD"
src={`/zma-logomark${getLogoTheme()}.svg`}
alt="Zoo Modeling App"
className="w-48 inline-block"
/>
<span className="text-3xl leading-none w-auto inline-block align-middle ml-2">
Modeling App
</span>
</div>
<h1 className="font-bold text-2xl mt-12 mb-6">
Sign in to get started with the {APP_NAME}

1733
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,87 @@
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>),
}
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>),
}
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(_) => todo!(),
}
}
}
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),
},
KclValueGroup::ArrayExpression(e) => ast::types::Value::ArrayExpression(e),
KclValueGroup::ObjectExpression(e) => ast::types::Value::ObjectExpression(e),
}
}
}

View File

@ -0,0 +1,564 @@
mod binding_scope;
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, RequiredParamAfterOptionalParam},
};
use kittycad_execution_plan as ep;
use kittycad_execution_plan::{Address, ExecutionError, 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::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 instructions_for_this_node = match item {
BodyItem::ExpressionStatement(node) => match KclValueGroup::from(node.expression) {
KclValueGroup::Single(value) => self.plan_to_compute_single(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(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, 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) => {
// This 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(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(SingleValue::from(expr.left))?;
let r = self.plan_to_compute_single(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(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::PipeExpression(_) => todo!(),
}
}
/// 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> {
declarations
.declarations
.into_iter()
.try_fold(Vec::new(), |mut acc, declaration| {
let (instrs, binding) = self.plan_to_bind_one(declaration.init)?;
self.binding_scope.bind(declaration.id.name, binding);
acc.extend(instrs);
Ok(acc)
})
}
fn plan_to_bind_one(
&mut self,
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(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(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(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(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(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(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(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)))
}
}
}
}
#[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,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
Compile(#[from] CompileError),
#[error("{0}")]
Execution(#[from] ExecutionError),
}
/// 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),
}

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,838 @@
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 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()
}]
)
}
fn use_kcl_function_y_combinator() {
let program = "
// TRUE := λx.λy.x
fn _TRUE = (x) => {
return (y) => { return x }
}
// FALSE := λx.λy.y
fn _FALSE = (x) => {
return (y) => { return y }
}
// constant false (no matter what is applied, the falsey value is returned)
fn cFalse = (x) => {
return _FALSE
}
// ISZERO := λn.n (λx.FALSE) TRUE
fn is_zero = (n) => {
let fa = n(cFalse)
return fa(_TRUE)
}
// IFTHENELSE := λp.λa.λb.p a b
fn ifthenelse = (p) => {
return (a) => {
return (b) => {
let fa = p(a)
return fa(b)
}
}
}
// SUCC := λn.λf.λx.f (n f x)
// Inserts another (f x) in the church numeral chain
fn succ = (n) => {
return (f) => {
return (x) => {
let fa = n(f)
let fb = fa(x)
return f(fb)
}
}
}
// PLUS := λm.λn.m SUCC n
fn plus = (m) => {
return (n) => {
let fa = m(succ)
return fa(n)
}
}
// 0 := λf.λx.x
fn _0 = (f) => {
return (x) => { return x }
}
fn cZero = (x) => {
return _0
}
// 1 := λf.λx.f x
fn _1 = (f) => {
return (x) => { return f(x) }
}
let _2 = succ(_1)
let _3 = succ(_2)
let _4 = succ(_3)
let _5 = succ(_4)
let _6 = succ(_5)
// ...
// PRED := λn.n (λg.λk.ISZERO (g 1) k (PLUS (g k) 1)) (λv.0) 0
fn pred = (n) => {
fn f1 = (g) => {
return (k) => {
let fa = is_zero(g(_1))
let fb = fa(k)
let fc1 = plus(g(k))
let fc2 = fc1(_1)
let fc = fb(fc2)
return fc
}
}
let f2 = n(f1)
let f3 = f2(cZero)
let f4 = f3(_0)
return f4
}
// MUL := λm.λn.m (PLUS n) 0
fn mul = (m) => {
return (n) => {
let fa = m(plus(n))
let fb = fa(_0)
return fb
}
}
// G := λr. λn.(1, if n = 0; else n × (r (n1)))
fn G = (r) => {
return (n) => {
let fa = ifthenelse(n)
let fb = fa(_1)
let fc1 = mul(n)
let fc2 = fc1(r(pred(n)))
let fc = fb(fc2)
return fc
}
}
// Y := λg.(λx.g (x x)) (λx.g (x x))
fn Y = (g) => {
fn f1 = (x) => { return g(x(x)) }
let f2 = g(f1)
let f3 = f2(f1)
return f3
}
fn fact = (n) => {
let fa = Y(G)
return fa(n)
}
// x should be _6
let x = fact(_3)
";
let (plan, scope) = must_plan(program);
// Somehow check the result is the same as _6 definition
}
#[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 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

@ -18,16 +18,18 @@ async-trait = "0.1.73"
clap = { version = "4.4.8", features = ["cargo", "derive", "env", "unicode"], optional = true }
dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] }
# derive-docs = { version = "0.1.4" }
derive-docs = { path = "../derive-docs" }
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"

View File

@ -2631,7 +2631,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 +2655,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 +2680,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,6 +5,7 @@ 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};
@ -286,6 +287,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 +612,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,

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

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

@ -34,6 +34,14 @@ module.exports = {
colors: {
...themeColors,
},
fontFamily: {
display: `'owners', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif`,
sans: `-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif`,
},
},
},
darkMode: 'class',

View File

@ -5,7 +5,7 @@ import { spawn, ChildProcess } from 'child_process'
let tauriDriver: ChildProcess
const application =
process.env.E2E_APPLICATION || `./src-tauri/target/release/kittycad-modeling`
process.env.E2E_APPLICATION || `./src-tauri/target/release/zoo-modeling-app`
export const config = {
port: 4444,

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"