Compare commits
22 Commits
v0.14.0
...
grackle-la
Author | SHA1 | Date | |
---|---|---|---|
c84c4ab578 | |||
e04b09fcd8 | |||
4903f6b9fc | |||
ef8149f03a | |||
1b75321bf1 | |||
3ed263da6b | |||
d59c4a2258 | |||
9c8351ea40 | |||
db98bcf2a0 | |||
15d96a072d | |||
088968c664 | |||
4bbf98bc34 | |||
ca08f5b337 | |||
a3649d09c0 | |||
635cb58036 | |||
7f050b114f | |||
c999819450 | |||
82905caad6 | |||
519e6d74ac | |||
edb7d68c05 | |||
345dd45caa | |||
b6a5f133f3 |
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime
|
||||
skip: **/target,node_modules,build
|
||||
skip: **/target,node_modules,build,**/Cargo.lock
|
||||
|
@ -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=
|
||||
|
@ -1,5 +1,5 @@
|
||||
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
|
||||
VITE_KC_API_BASE_URL=https://api.kittycad.io
|
||||
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
|
||||
VITE_KC_API_BASE_URL=https://api.zoo.dev
|
||||
VITE_KC_SITE_BASE_URL=https://zoo.dev
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||
|
11
.github/workflows/cargo-clippy.yml
vendored
11
.github/workflows/cargo-clippy.yml
vendored
@ -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 }}"
|
||||
|
10
.github/workflows/cargo-test.yml
vendored
10
.github/workflows/cargo-test.yml
vendored
@ -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: |-
|
||||
|
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@ -123,6 +123,7 @@ jobs:
|
||||
needs: [prepare-json-files]
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
steps:
|
||||
@ -243,6 +244,7 @@ jobs:
|
||||
args: "${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: matrix.os != 'ubuntu-latest'
|
||||
env:
|
||||
PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
|
||||
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
|
||||
@ -279,7 +281,6 @@ jobs:
|
||||
run: |
|
||||
ls -l artifact/*/*oo*
|
||||
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
||||
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
|
||||
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
|
||||
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
|
||||
jq --null-input \
|
||||
@ -288,8 +289,6 @@ jobs:
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_sig "$DARWIN_SIG" \
|
||||
--arg darwin_url "$RELEASE_DIR/macos/Zoo%20Modeling%20App.app.tar.gz" \
|
||||
--arg linux_sig "$LINUX_SIG" \
|
||||
--arg linux_url "$RELEASE_DIR/appimage/zoo-modeling-app_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
|
||||
--arg windows_sig "$WINDOWS_SIG" \
|
||||
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
||||
'{
|
||||
@ -305,10 +304,6 @@ jobs:
|
||||
"signature": $darwin_sig,
|
||||
"url": $darwin_url
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"signature": $linux_sig,
|
||||
"url": $linux_url
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"signature": $windows_sig,
|
||||
"url": $windows_url
|
||||
@ -325,7 +320,6 @@ jobs:
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_url "$RELEASE_DIR/dmg/Zoo%20Modeling%20App_${VERSION_NO_V}_universal.dmg" \
|
||||
--arg linux_url "$RELEASE_DIR/appimage/zoo-modeling-app_${VERSION_NO_V}_amd64.AppImage" \
|
||||
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi" \
|
||||
'{
|
||||
"version": $version,
|
||||
@ -335,9 +329,6 @@ jobs:
|
||||
"dmg-universal": {
|
||||
"url": $darwin_url
|
||||
},
|
||||
"appimage-x86_64": {
|
||||
"url": $linux_url
|
||||
},
|
||||
"msi-x86_64": {
|
||||
"url": $windows_url
|
||||
}
|
||||
@ -359,7 +350,7 @@ jobs:
|
||||
uses: google-github-actions/upload-cloud-storage@v2.0.0
|
||||
with:
|
||||
path: artifact
|
||||
glob: '*/*oo*'
|
||||
glob: '*/Zoo*'
|
||||
parent: false
|
||||
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
||||
|
||||
@ -379,4 +370,4 @@ jobs:
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: artifact/*/*oo*
|
||||
files: 'artifact/*/Zoo*'
|
||||
|
@ -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
|
||||
|
||||
|
@ -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')
|
||||
})
|
||||
|
@ -14,8 +14,8 @@
|
||||
<link rel="stylesheet" href="https://use.typekit.net/zzv8rvm.css" />
|
||||
<script
|
||||
defer
|
||||
data-domain="app.kittycad.io"
|
||||
src="https://plausible.corp.kittycad.io/js/script.js"
|
||||
data-domain="app.zoo.dev"
|
||||
src="https://plausible.corp.zoo.dev/js/script.js"
|
||||
></script>
|
||||
<title>Zoo Modeling App</title>
|
||||
</head>
|
||||
|
@ -136,7 +136,7 @@
|
||||
"prettier": "^2.8.0",
|
||||
"setimmediate": "^1.0.5",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"vite": "^4.5.0",
|
||||
"vite": "^4.5.2",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"wait-on": "^7.2.0",
|
||||
|
6
src-tauri/Cargo.lock
generated
6
src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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: [
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -128,6 +128,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
: paths.HOME + paths.SETTINGS
|
||||
navigate(targetPath)
|
||||
}}
|
||||
data-testid="settings-button"
|
||||
>
|
||||
Settings
|
||||
</ActionButton>
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -37,13 +37,20 @@ import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
|
||||
const Home = () => {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const navigate = useNavigate()
|
||||
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
|
||||
const { projects: loadedProjects, newDefaultDirectory } =
|
||||
useLoaderData() as HomeLoaderData
|
||||
const {
|
||||
settings: {
|
||||
context: { defaultDirectory, defaultProjectName },
|
||||
send: sendToSettings,
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
if (newDefaultDirectory) {
|
||||
sendToSettings({
|
||||
type: 'Set Default Directory',
|
||||
data: { defaultDirectory: newDefaultDirectory },
|
||||
})
|
||||
}
|
||||
|
||||
const [state, send] = useMachine(homeMachine, {
|
||||
context: {
|
||||
@ -222,7 +229,7 @@ const Home = () => {
|
||||
</ActionButton>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<section data-testid="home-section">
|
||||
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
|
||||
Loaded from{' '}
|
||||
<span className="text-energy-70 dark:text-energy-40">
|
||||
|
@ -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>
|
||||
</>
|
||||
|
1717
src/wasm-lib/Cargo.lock
generated
1717
src/wasm-lib/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
18
src/wasm-lib/grackle/Cargo.toml
Normal file
18
src/wasm-lib/grackle/Cargo.toml
Normal 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"
|
164
src/wasm-lib/grackle/src/binding_scope.rs
Normal file
164
src/wasm-lib/grackle/src/binding_scope.rs
Normal 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,
|
||||
}
|
87
src/wasm-lib/grackle/src/kcl_value_group.rs
Normal file
87
src/wasm-lib/grackle/src/kcl_value_group.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
564
src/wasm-lib/grackle/src/lib.rs
Normal file
564
src/wasm-lib/grackle/src/lib.rs
Normal 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),
|
||||
}
|
112
src/wasm-lib/grackle/src/native_functions.rs
Normal file
112
src/wasm-lib/grackle/src/native_functions.rs
Normal 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),
|
||||
})
|
||||
}
|
||||
}
|
838
src/wasm-lib/grackle/src/tests.rs
Normal file
838
src/wasm-lib/grackle/src/tests.rs
Normal 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 (n−1)))
|
||||
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);
|
||||
}
|
@ -21,13 +21,15 @@ databake = { version = "0.1.7", features = ["derive"] }
|
||||
derive-docs = { version = "0.1.5" }
|
||||
# derive-docs = { path = "../derive-docs" }
|
||||
kittycad = { workspace = true }
|
||||
kittycad-execution-plan-macros = { workspace = true }
|
||||
kittycad-execution-plan-traits = { workspace = true }
|
||||
lazy_static = "1.4.0"
|
||||
parse-display = "0.8.2"
|
||||
schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] }
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
serde_json = "1.0.108"
|
||||
thiserror = "1.0.50"
|
||||
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] }
|
||||
ts-rs = { version = "7", features = ["uuid-impl"] }
|
||||
uuid = { version = "1.6.1", features = ["v4", "js", "serde"] }
|
||||
winnow = "0.5.18"
|
||||
|
||||
|
@ -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] {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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");
|
||||
|
||||
|
21
yarn.lock
21
yarn.lock
@ -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"
|
||||
|
Reference in New Issue
Block a user