Compare commits

..

46 Commits

Author SHA1 Message Date
233d79e84d A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-07-19 21:58:39 +00:00
754abd0407 Trigger CI 2024-07-19 05:30:38 -04:00
afeb6ebb87 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-07-19 02:10:15 +00:00
e5aeb92258 Merge branch 'main' into pierremtb/issue2805 2024-07-18 20:06:36 -04:00
e9ecdb5b8b Trigger CI 2024-07-18 07:40:48 -04:00
45cc51c6dc A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-07-18 11:34:30 +00:00
63178bbc76 Update pierremtb/issue2805 from main 2024-07-18 07:28:59 -04:00
bc8788b2e9 Clean up of tests 2024-07-17 18:47:42 -04:00
c3a3f1d6ce Add env vars 2024-07-17 07:54:41 -04:00
912df5f859 updater-test for arm too 2024-07-17 06:15:48 -04:00
b50891a28f A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-07-17 10:08:35 +00:00
4711b57028 Merge branch 'main' into pierremtb/issue2805 2024-07-17 06:03:06 -04:00
2179a3728c Check updater-tes 2024-07-17 05:39:51 -04:00
a185005f5c Update last_update / last_download gen for win arm 2024-07-16 19:44:56 -04:00
21c72073a9 Add createUpdaterArtifacts property (breaking change in cli) 2024-07-16 19:42:10 -04:00
fe21d66c0d Add missed if case 2024-07-16 18:39:07 -04:00
e4567b43f8 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-07-16 22:00:55 +00:00
90e7741e47 Fix macos release build 2024-07-16 17:51:56 -04:00
a60f0d1355 Test build release 2024-07-16 17:38:33 -04:00
9e336b4654 Cleaning up upload 2024-07-16 16:54:31 -04:00
bef7467703 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-07-16 11:09:34 +00:00
6444fe50e7 Trigger CI 2024-07-16 06:59:52 -04:00
93c7a6a293 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-07-16 10:51:24 +00:00
eaadc0e39e Trigger CI 2024-07-16 06:45:52 -04:00
933850e13b A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-07-16 09:11:55 +00:00
07b9401f2d Merge branch 'main' into pierremtb/issue2805 2024-07-16 05:06:18 -04:00
dc5277c48b Merge branch 'main' into pierremtb/issue2805 2024-07-15 05:27:50 -04:00
3a4e323c45 Rename binary to zoo-modeling-app, enable msi bundle for windows-aarch64 2024-07-12 17:11:55 -04:00
bbe4f50b17 Merge branch 'main' into pierremtb/issue2805 2024-07-12 08:15:41 -04:00
e376e397db Win aarch64 bundle 2024-07-12 07:55:22 -04:00
c7c747a109 Clean up 2024-07-12 07:11:16 -04:00
dd3d604924 Merge branch 'main' into pierremtb/issue2805 2024-07-11 08:20:58 -04:00
b19e89effc More clean up, fix CUT_RELEASE_PR eval 2024-07-11 07:53:13 -04:00
416002e84a Clean up 2024-07-11 07:23:50 -04:00
5c3571226b Clean up 2024-07-11 07:22:56 -04:00
d9eead1641 Break apart ci.yml into build-test-web and build-test-publish-apps 2024-07-11 07:21:54 -04:00
468e86a864 Add windows arm64 releases
Fixes #2805
2024-07-11 07:02:45 -04:00
3f977c7bf9 Merge branch 'main' into pierremtb/issue2805 2024-07-10 07:42:43 -04:00
7d7572b36a Merge branch 'main' into pierremtb/issue2805 2024-07-03 08:24:42 -04:00
e05bce6145 Upload the right app.exe 2024-06-26 08:15:17 -04:00
c58c0e7089 WIP 2024-06-26 06:50:20 -04:00
83ece965d9 No bundles for arm 2024-06-26 06:04:11 -04:00
de7e958e22 Add aarch64 target for Windows 2024-06-26 05:07:51 -04:00
1beadc063d Break down build-test-apps per platform 2024-06-26 05:00:41 -04:00
3bc2a00f4e Merge branch 'pierremtb/fix-ci-runners-versions' into pierremtb/issue2805 2024-06-26 04:34:33 -04:00
0f256ea6c5 Pin CI runner versions (I don't love 'latest') 2024-06-11 10:07:44 +02:00
46 changed files with 2716 additions and 1536 deletions

View File

@ -1,4 +1,4 @@
name: CI
name: build-test-publish-apps
on:
pull_request:
@ -21,75 +21,8 @@ concurrency:
cancel-in-progress: true
jobs:
check-format:
runs-on: 'ubuntu-latest'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn fmt-check
check-types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn xstate:typegen
- run: yarn tsc
check-typos:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
- name: Install codespell
run: |
python -m pip install codespell
- name: Run codespell
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
build-test-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn simpleserver:ci
- run: yarn test:nowatch
prepare-json-files:
runs-on: ubuntu-latest # seperate job on Ubuntu for easy string manipulations (compared to Windows)
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs:
version: ${{ steps.export_version.outputs.version }}
steps:
@ -114,7 +47,7 @@ jobs:
- name: Set updater test version
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/test/last_update.json' \
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/updater-test/last_update.json' \
'.plugins.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- uses: actions/upload-artifact@v3
@ -129,18 +62,9 @@ jobs:
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
build-test-apps:
build-test-app-macos:
needs: [prepare-json-files]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-14, ubuntu-latest, windows-latest]
env:
# Specific Apple Universal target for macos
TAURI_ARGS_MACOS: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
# Only build executable on linux (no appimage or deb)
TAURI_ARGS_UBUNTU: ${{ matrix.os == 'ubuntu-latest' && '--bundles' || '' }}
runs-on: macos-14
steps:
- uses: actions/checkout@v4
@ -155,28 +79,6 @@ jobs:
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
- name: Update WebView2 on Windows
if: matrix.os == 'windows-latest'
# Workaround needed to build the tauri windows app with matching edge version.
# From https://github.com/actions/runner-images/issues/9538
run: |
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
Start-Process -FilePath setup.exe -Verb RunAs -Wait
- name: Install ubuntu system dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y \
libgtk-3-dev \
libayatana-appindicator3-dev \
webkit2gtk-driver \
libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev \
libwebkit2gtk-4.1-dev \
at-spi2-core \
xvfb
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
@ -197,60 +99,25 @@ jobs:
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm manually
shell: bash
env:
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
run: |
mkdir src/wasm-lib/pkg; cd src/wasm-lib
echo "building with ${{ env.MODE }}"
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
cd ../../
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
- name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
- name: Run vite build (build:both)
- name: Run vite build
run: yarn vite build --mode ${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
- name: Fix format
run: yarn fmt
- name: Install x86 target for Universal builds (MacOS only)
if: matrix.os == 'macos-14'
- name: Install x86 target for Universal builds
run: |
rustup target add x86_64-apple-darwin
- name: Prepare certificate and variables (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
shell: bash
- name: Setup certicate with SSM KSP (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn
smksp_registrar.exe list
smctl.exe keypair ls
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
shell: cmd
- name: Build the app (debug)
if: ${{ env.BUILD_RELEASE == 'false' }}
run: "yarn tauri build --debug ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
run: "yarn tauri build --debug --target universal-apple-darwin"
- name: Build for Mac TestFlight (nightly)
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
if: ${{ github.event_name == 'schedule' }}
shell: bash
run: |
unset APPLE_SIGNING_IDENTITY
@ -314,7 +181,7 @@ jobs:
- name: 'Upload to Mac TestFlight (nightly)'
uses: apple-actions/upload-testflight-build@v1
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
if: ${{ github.event_name == 'schedule' }}
with:
app-path: 'src-tauri/target/universal-apple-darwin/release/bundle/macos/Zoo Modeling App.pkg'
issuer-id: ${{ secrets.APPLE_STORE_ISSUER_ID }}
@ -324,7 +191,7 @@ jobs:
- name: Clean up after Mac TestFlight (nightly)
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
if: ${{ github.event_name == 'schedule' }}
shell: bash
run: |
git status
@ -350,35 +217,165 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
run: "yarn tauri build --config src-tauri/tauri.release.conf.json --target universal-apple-darwin"
- uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest'
env:
PREFIX: ${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
with:
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
path: "src-tauri/target/universal-apple-darwin/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/bundle/*/*"
- name: Run e2e tests (linux only)
if: ${{ matrix.os == 'ubuntu-latest' && github.event_name != 'release' && github.event_name != 'schedule' }}
run: |
cargo install tauri-driver --force
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri
- uses: actions/download-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
- name: Copy updated .json file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: "cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json"
- name: Build the app (release, updater test)
if: ${{ env.CUT_RELEASE_PR == 'true' }}
env:
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 }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: "yarn tauri build -c src-tauri/tauri.release.conf.json -b dmg --target universal-apple-darwin"
- name: Run e2e tests (windows only)
if: ${{ matrix.os == 'windows-latest' && github.event_name != 'release' && github.event_name != 'schedule' }}
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
path: "src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg"
name: updater-test
build-test-app-windows:
needs: [prepare-json-files]
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
- name: Copy updated .json files
if: github.event_name == 'schedule'
run: |
ls -l artifact
cp artifact/package.json package.json
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
- name: Update WebView2 on Windows
# Workaround needed to build the tauri windows app with matching edge version.
# From https://github.com/actions/runner-images/issues/9538
run: |
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
Start-Process -FilePath setup.exe -Verb RunAs -Wait
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Install aarch64 target
run: rustup target add aarch64-pc-windows-msvc
- name: Run build:wasm manually
shell: bash
env:
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
run: |
mkdir src/wasm-lib/pkg; cd src/wasm-lib
echo "building with ${{ env.MODE }}"
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
cd ../../
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
- name: Run vite build
run: yarn vite build --mode ${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
- name: Fix format
run: yarn fmt
- name: Prepare certificate and variables (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' }}
run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
shell: bash
- name: Setup certicate with SSM KSP (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' }}
run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn
smksp_registrar.exe list
smctl.exe keypair ls
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
shell: cmd
- name: Build the app (debug) for x86_64
if: ${{ env.BUILD_RELEASE == 'false' }}
run: "yarn run tauri build --debug"
- name: Build the app (release) and sign for x86_64
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: "yarn tauri build --config src-tauri\\tauri.release.conf.json"
- uses: actions/upload-artifact@v3
with:
path: "src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/bundle/*/*"
- name: Build the app (debug) for aarch64
if: ${{ env.BUILD_RELEASE == 'false' }}
run: yarn run tauri build --debug --target aarch64-pc-windows-msvc
- name: Build the app (release) and sign for aarch64
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: "yarn tauri build --config src-tauri\\tauri.release.conf.json --target aarch64-pc-windows-msvc"
- uses: actions/upload-artifact@v3
with:
path: "src-tauri/target/aarch64-pc-windows-msvc/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/bundle/*/*"
- name: Run e2e tests
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: |
cargo install tauri-driver --force
yarn wdio run wdio.conf.ts
env:
E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\Zoo Modeling App.exe"
E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\zoo-modeling-app.exe"
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_API_BASE_URL: ${{ env.BUILD_RELEASE == 'true' && 'https://api.zoo.dev' || 'https://api.dev.zoo.dev' }}
E2E_TAURI_ENABLED: true
@ -389,31 +386,41 @@ jobs:
- name: Copy updated .json file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
ls -l artifact
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
cat src-tauri/tauri.release.conf.json
run: "cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json"
- name: Build the app (release, updater test)
if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
- name: Build the app (release, updater test) for x86_64
if: ${{ env.CUT_RELEASE_PR == 'true' }}
env:
TAURI_CONF_ARGS: "-c ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
TAURI_BUNDLE_ARGS: "-b ${{ matrix.os == 'windows-latest' && 'msi' || 'dmg' }}"
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_BUNDLE_ARGS }} ${{ env.TAURI_ARGS_MACOS }}"
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: "yarn tauri build -c src-tauri\\tauri.release.conf.json -b msi"
- name: Build the app (release, updater test) for aarch64
if: ${{ env.CUT_RELEASE_PR == 'true' }}
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: "yarn tauri build -c src-tauri\\tauri.release.conf.json -b msi -t aarch64-pc-windows-msvc"
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
path: "${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg' || 'src-tauri/target/release/bundle/msi/*.msi' }}"
path: "src-tauri/target/release/bundle/msi/*.msi"
name: updater-test
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
path: "src-tauri/target/aarch64-pc-windows-msvc/release/bundle/msi/*.msi"
name: updater-test
publish-apps-release:
runs-on: ubuntu-22.04
permissions:
contents: write
runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps]
needs: [prepare-json-files, build-test-app-macos, build-test-app-windows]
env:
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
@ -429,7 +436,8 @@ jobs:
run: |
ls -l artifact/*/*oo*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
WINDOWS_X86_64_SIG=`cat artifact/msi/*x64*.msi.zip.sig`
WINDOWS_AARCH64_SIG=`cat artifact/msi/*arm64*.msi.zip.sig`
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
@ -437,8 +445,10 @@ jobs:
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \
--arg windows_x86_64_sig "$WINDOWS_X86_64_SIG" \
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \
--arg windows_aarch64_sig "$WINDOWS_AARCH64_SIG" \
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi.zip" \
'{
"version": $version,
"pub_date": $pub_date,
@ -453,8 +463,12 @@ jobs:
"url": $darwin_url
},
"windows-x86_64": {
"signature": $windows_sig,
"url": $windows_url
"signature": $windows_x86_64_sig,
"url": $windows_x86_64_url
},
"windows-aarch64": {
"signature": $windows_aarch64_sig,
"url": $windows_aarch64_url
}
}
}' > last_update.json
@ -468,7 +482,8 @@ jobs:
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \
--arg windows_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi" \
'{
"version": $version,
"pub_date": $pub_date,
@ -478,7 +493,10 @@ jobs:
"url": $darwin_url
},
"msi-x86_64": {
"url": $windows_url
"url": $windows_x86_64_url
},
"msi-aarch64": {
"url": $windows_aarch64_url
}
}
}' > last_download.json
@ -522,7 +540,7 @@ jobs:
announce_release:
needs: [publish-apps-release]
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
if: github.event_name == 'release'
steps:
- name: Check out code

78
.github/workflows/build-test-web.yml vendored Normal file
View File

@ -0,0 +1,78 @@
name: build-test-web
on:
pull_request:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
check-format:
runs-on: 'ubuntu-22.04'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn fmt-check
check-types:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn xstate:typegen
- run: yarn tsc
check-typos:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
- name: Install codespell
run: |
python -m pip install codespell
- name: Run codespell
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
build-test-web:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn simpleserver:ci
- run: yarn test:nowatch

View File

@ -7,7 +7,7 @@ on:
jobs:
create-release:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
permissions:
contents: write
pull-requests: read

View File

@ -345,7 +345,7 @@ $env:KITTYCAD_API_TOKEN="<YOUR_KITTYCAD_API_TOKEN>"
$env:VITE_KC_API_BASE_URL="https://api.dev.zoo.dev"
$env:E2E_TAURI_ENABLED="true"
$env:TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}'
$env:E2E_APPLICATION=".\src-tauri\target\debug\Zoo Modeling App.exe"
$env:E2E_APPLICATION=".\src-tauri\target\debug\zoo-modeling-app.exe"
Stop-Process -Name msedgedriver
yarn wdio run wdio.conf.ts
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -1,5 +1,5 @@
{
"name": "untitled-app",
"name": "zoo-modeling-app",
"version": "0.24.3",
"private": true,
"dependencies": {
@ -113,7 +113,7 @@
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1",
"@playwright/test": "^1.45.1",
"@tauri-apps/cli": "==2.0.0-beta.13",
"@tauri-apps/cli": "==2.0.0-beta.22",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@types/mocha": "^10.0.6",

1415
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,18 @@
[package]
name = "app"
name = "zoo-modeling-app"
version = "0.1.0"
description = "The Zoo Modeling App"
authors = ["Zoo Engineers <eng@zoo.dev>"]
license = ""
repository = "https://github.com/KittyCAD/modeling-app"
default-run = "app"
default-run = "zoo-modeling-app"
edition = "2021"
rust-version = "1.70"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2.0.0-beta.18", features = [] }
tauri-bundler = { git = "https://github.com/tauri-apps/tauri", rev = "1a88fc1a9b81bd09fc24cf0dfed95d20dd72a1bb" }
[dependencies]
anyhow = "1"

View File

@ -1,6 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"bundle": {
"createUpdaterArtifacts": "v1Compatible",
"windows": {
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
"digestAlgorithm": "sha256",

View File

@ -47,6 +47,7 @@ import {
PipeExpression,
Program,
ProgramMemory,
programMemoryInit,
recast,
SketchGroup,
ExtrudeGroup,
@ -129,7 +130,7 @@ export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels
export class SceneEntities {
engineCommandManager: EngineCommandManager
scene: Scene
sceneProgramMemory: ProgramMemory = ProgramMemory.empty()
sceneProgramMemory: ProgramMemory = { root: {}, return: null }
activeSegments: { [key: string]: Group } = {}
intersectionPlane: Mesh | null = null
axisGroup: Group | null = null
@ -549,9 +550,9 @@ export class SceneEntities {
const variableDeclarationName =
_node1.node?.declarations?.[0]?.id?.name || ''
const sg = kclManager.programMemory.get(
const sg = kclManager.programMemory.root[
variableDeclarationName
) as SketchGroup
] as SketchGroup
const lastSeg = sg.value.slice(-1)[0] || sg.start
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
@ -767,9 +768,9 @@ export class SceneEntities {
programMemoryOverride,
})
this.sceneProgramMemory = programMemory
const sketchGroup = programMemory.get(
const sketchGroup = programMemory.root[
variableDeclarationName
) as SketchGroup
] as SketchGroup
const sgPaths = sketchGroup.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -819,9 +820,9 @@ export class SceneEntities {
// Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory
const sketchGroup = programMemory.get(
const sketchGroup = programMemory.root[
variableDeclarationName
) as SketchGroup
] as SketchGroup
const sgPaths = sketchGroup.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -1080,9 +1081,9 @@ export class SceneEntities {
})
this.sceneProgramMemory = programMemory
const maybeSketchGroup = programMemory.get(variableDeclarationName)
const maybeSketchGroup = programMemory.root[variableDeclarationName]
let sketchGroup = undefined
if (maybeSketchGroup?.type === 'SketchGroup') {
if (maybeSketchGroup.type === 'SketchGroup') {
sketchGroup = maybeSketchGroup
} else if ((maybeSketchGroup as ExtrudeGroup).sketchGroup) {
sketchGroup = (maybeSketchGroup as ExtrudeGroup).sketchGroup
@ -1772,7 +1773,7 @@ function prepareTruncatedMemoryAndAst(
if (err(_node)) return _node
const variableDeclarationName = _node.node?.declarations?.[0]?.id?.name || ''
const lastSeg = (
programMemory.get(variableDeclarationName) as SketchGroup
programMemory.root[variableDeclarationName] as SketchGroup
).value.slice(-1)[0]
if (draftSegment) {
// truncatedAst needs to setup with another segment at the end
@ -1823,27 +1824,33 @@ function prepareTruncatedMemoryAndAst(
..._ast,
body: [JSON.parse(JSON.stringify(_ast.body[bodyIndex]))],
}
const programMemoryOverride = programMemoryInit()
if (err(programMemoryOverride)) return programMemoryOverride
// Grab all the TagDeclarators and TagIdentifiers from memory.
let start = _node.node.start
const programMemoryOverride = programMemory.filterVariables(true, (value) => {
for (const key in programMemory.root) {
const value = programMemory.root[key]
if (!('__meta' in value)) {
continue
}
if (
!('__meta' in value) ||
value.__meta === undefined ||
value.__meta.length === 0 ||
value.__meta[0].sourceRange === undefined
) {
return false
continue
}
if (value.__meta[0].sourceRange[0] >= start) {
// We only want things before our start point.
return false
continue
}
return value.type === 'TagIdentifier'
})
if (err(programMemoryOverride)) return programMemoryOverride
if (value.type === 'TagIdentifier') {
programMemoryOverride.root[key] = JSON.parse(JSON.stringify(value))
}
}
for (let i = 0; i < bodyIndex; i++) {
const node = _ast.body[i]
@ -1851,15 +1858,12 @@ function prepareTruncatedMemoryAndAst(
continue
}
const name = node.declarations[0].id.name
const memoryItem = programMemory.get(name)
// const memoryItem = kclManager.programMemory.root[name]
const memoryItem = programMemory.root[name]
if (!memoryItem) {
continue
}
const error = programMemoryOverride.set(
name,
JSON.parse(JSON.stringify(memoryItem))
)
if (err(error)) return error
programMemoryOverride.root[name] = JSON.parse(JSON.stringify(memoryItem))
}
return {
truncatedAst,
@ -1896,7 +1900,7 @@ export function sketchGroupFromPathToNode({
)
if (err(_varDec)) return _varDec
const varDec = _varDec.node
const result = programMemory.get(varDec?.id?.name || '')
const result = programMemory.root[varDec?.id?.name || '']
if (result?.type === 'ExtrudeGroup') {
return result.sketchGroup
}

View File

@ -1,5 +1,5 @@
import { useEffect, useState, useRef } from 'react'
import { parse, BinaryPart, Value, ProgramMemory } from '../lang/wasm'
import { parse, BinaryPart, Value } from '../lang/wasm'
import {
createIdentifier,
createLiteral,
@ -120,7 +120,8 @@ export function useCalc({
}, [])
useEffect(() => {
if (programMemory.has(newVariableName)) {
const allVarNames = Object.keys(programMemory.root)
if (allVarNames.includes(newVariableName)) {
setIsNewVariableNameUnique(false)
} else {
setIsNewVariableNameUnique(true)
@ -142,20 +143,17 @@ export function useCalc({
const code = `const __result__ = ${value}`
const ast = parse(code)
if (trap(ast)) return
const _programMem: ProgramMemory = ProgramMemory.empty()
for (const { key, value } of availableVarInfo.variables) {
const error = _programMem.set(key, {
type: 'UserVal',
value,
__meta: [],
})
if (trap(error)) return
}
const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
})
executeAst({
ast,
engineCommandManager,
useFakeExecutor: true,
programMemoryOverride: kclManager.programMemory.clone(),
programMemoryOverride: JSON.parse(
JSON.stringify(kclManager.programMemory)
),
}).then(({ programMemory }) => {
const resultDeclaration = ast.body.find(
(a) =>
@ -165,7 +163,7 @@ export function useCalc({
const init =
resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.get('__result__')?.value
const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init)
})

View File

@ -163,7 +163,7 @@ export function useCodeMirror(props: UseCodeMirror) {
effects: StateEffect.reconfigure.of(targetExtensions),
})
}
}, [targetExtensions, view, isFirstRender])
}, [targetExtensions])
return { view, setView, container, setContainer, state, setState }
}

View File

@ -1,6 +1,6 @@
import { processMemory } from './MemoryPane'
import { enginelessExecutor } from '../../../lib/testHelpers'
import { initPromise, parse, ProgramMemory } from '../../../lang/wasm'
import { initPromise, parse } from '../../../lang/wasm'
beforeAll(async () => {
await initPromise
@ -29,7 +29,10 @@ describe('processMemory', () => {
|> lineTo([2.15, 4.32], %)
// |> rx(90, %)`
const ast = parse(code)
const programMemory = await enginelessExecutor(ast, ProgramMemory.empty())
const programMemory = await enginelessExecutor(ast, {
root: {},
return: null,
})
const output = processMemory(programMemory)
expect(output.myVar).toEqual(5)
expect(output.otherVar).toEqual(3)

View File

@ -82,7 +82,8 @@ export const MemoryPane = () => {
export const processMemory = (programMemory: ProgramMemory) => {
const processedMemory: any = {}
for (const [key, val] of programMemory?.visibleEntries()) {
Object.keys(programMemory?.root || {}).forEach((key) => {
const val = programMemory.root[key]
if (typeof val.value !== 'function') {
if (val.type === 'SketchGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
@ -102,6 +103,6 @@ export const processMemory = (programMemory: ProgramMemory) => {
} else if (key !== 'log') {
processedMemory[key] = '__function__'
}
}
})
return processedMemory
}

View File

@ -179,8 +179,6 @@ export const Stream = () => {
videoElement: videoRef.current,
},
})
setIsLoading(false)
}, [mediaStream])
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {

View File

@ -14,7 +14,9 @@ import {
Program,
ProgramMemory,
recast,
SketchGroup,
SourceRange,
ExtrudeGroup,
} from 'lang/wasm'
import { getNodeFromPath } from './queryAst'
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
@ -31,7 +33,10 @@ export class KclManager {
},
digest: null,
}
private _programMemory: ProgramMemory = ProgramMemory.empty()
private _programMemory: ProgramMemory = {
root: {},
return: null,
}
private _logs: string[] = []
private _kclErrors: KCLError[] = []
private _isExecuting = false
@ -500,7 +505,10 @@ function defaultSelectionFilter(
programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager
) {
programMemory.hasSketchOrExtrudeGroup() &&
const firstSketchOrExtrudeGroup = Object.values(programMemory.root).find(
(node) => node.type === 'ExtrudeGroup' || node.type === 'SketchGroup'
) as SketchGroup | ExtrudeGroup
firstSketchOrExtrudeGroup &&
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),

View File

@ -16,7 +16,7 @@ const mySketch001 = startSketchOn('XY')
// |> rx(45, %)`
const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore
const sketch001 = programMemory?.get('mySketch001')
const sketch001 = programMemory?.root?.mySketch001
expect(sketch001).toEqual({
type: 'SketchGroup',
on: expect.any(Object),
@ -66,7 +66,7 @@ const mySketch001 = startSketchOn('XY')
|> extrude(2, %)`
const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore
const sketch001 = programMemory?.get('mySketch001')
const sketch001 = programMemory?.root?.mySketch001
expect(sketch001).toEqual({
type: 'ExtrudeGroup',
id: expect.any(String),
@ -146,7 +146,7 @@ const sk2 = startSketchOn('XY')
`
const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore
const geos = [programMemory?.get('theExtrude'), programMemory?.get('sk2')]
const geos = [programMemory?.root?.theExtrude, programMemory?.root?.sk2]
expect(geos).toEqual([
{
type: 'ExtrudeGroup',

View File

@ -12,25 +12,25 @@ describe('test executor', () => {
it('test assigning two variables, the second summing with the first', async () => {
const code = `const myVar = 5
const newVar = myVar + 1`
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(5)
expect(mem.get('newVar')?.value).toBe(6)
const { root } = await exe(code)
expect(root.myVar.value).toBe(5)
expect(root.newVar.value).toBe(6)
})
it('test assigning a var with a string', async () => {
const code = `const myVar = "a str"`
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe('a str')
const { root } = await exe(code)
expect(root.myVar.value).toBe('a str')
})
it('test assigning a var by cont concatenating two strings string execute', async () => {
const code = fs.readFileSync(
'./src/lang/testExamples/variableDeclaration.cado',
'utf-8'
)
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe('a str another str')
const { root } = await exe(code)
expect(root.myVar.value).toBe('a str another str')
})
it('fn funcN = () => {} execute', async () => {
const mem = await exe(
const { root } = await exe(
[
'fn funcN = (a, b) => {',
' return a + b',
@ -39,8 +39,8 @@ const newVar = myVar + 1`
'const magicNum = funcN(9, theVar)',
].join('\n')
)
expect(mem.get('theVar')?.value).toBe(60)
expect(mem.get('magicNum')?.value).toBe(69)
expect(root.theVar.value).toBe(60)
expect(root.magicNum.value).toBe(69)
})
it('sketch declaration', async () => {
let code = `const mySketch = startSketchOn('XY')
@ -50,9 +50,9 @@ const newVar = myVar + 1`
|> lineTo([5,-1], %, "rightPath")
// |> close(%)
`
const mem = await exe(code)
const { root } = await exe(code)
// geo is three js buffer geometry and is very bloated to have in tests
const minusGeo = mem.get('mySketch')?.value
const minusGeo = root.mySketch.value
expect(minusGeo).toEqual([
{
type: 'ToPoint',
@ -104,8 +104,8 @@ const newVar = myVar + 1`
'fn myFn = (a) => { return a + 1 }',
'const myVar = 5 + 1 |> myFn(%)',
].join('\n')
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(7)
const { root } = await exe(code)
expect(root.myVar.value).toBe(7)
})
// Enable rotations #152
@ -117,16 +117,16 @@ const newVar = myVar + 1`
// ' |> lineTo([1, 1], %)',
// 'const rotated = rx(90, mySk1)',
// ].join('\n')
// const mem = await exe(code)
// expect(mem.get('mySk1')?.value).toHaveLength(3)
// expect(mem.get('rotated')?.type).toBe('SketchGroup')
// const { root } = await exe(code)
// expect(root.mySk1.value).toHaveLength(3)
// expect(root?.rotated?.type).toBe('SketchGroup')
// if (
// mem.get('mySk1')?.type !== 'SketchGroup' ||
// mem.get('rotated')?.type !== 'SketchGroup'
// root?.mySk1?.type !== 'SketchGroup' ||
// root?.rotated?.type !== 'SketchGroup'
// )
// throw new Error('not a sketch group')
// expect(mem.get('mySk1')?.rotation).toEqual([0, 0, 0, 1])
// expect(mem.get('rotated')?.rotation.map((a) => a.toFixed(4))).toEqual([
// expect(root.mySk1.rotation).toEqual([0, 0, 0, 1])
// expect(root.rotated.rotation.map((a) => a.toFixed(4))).toEqual([
// '0.7071',
// '0.0000',
// '0.0000',
@ -144,8 +144,8 @@ const newVar = myVar + 1`
' |> lineTo([1,1], %)',
// ' |> rx(90, %)',
].join('\n')
const mem = await exe(code)
expect(mem.get('mySk1')).toEqual({
const { root } = await exe(code)
expect(root.mySk1).toEqual({
type: 'SketchGroup',
on: expect.any(Object),
start: {
@ -214,37 +214,36 @@ const newVar = myVar + 1`
const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join(
'\n'
)
const mem = await exe(code)
const { root } = await exe(code)
// TODO path to node is probably wrong here, zero indexes are not correct
expect(mem.get('three')).toEqual({
type: 'UserVal',
value: 3,
__meta: [
{
sourceRange: [14, 15],
},
],
expect(root).toEqual({
three: {
type: 'UserVal',
value: 3,
__meta: [
{
sourceRange: [14, 15],
},
],
},
yo: {
type: 'UserVal',
value: [1, '2', 3, 9],
__meta: [
{
sourceRange: [27, 49],
},
],
},
})
expect(mem.get('yo')).toEqual({
type: 'UserVal',
value: [1, '2', 3, 9],
__meta: [
{
sourceRange: [27, 49],
},
],
})
// Check that there are no other variables or environments.
expect(mem.numEnvironments()).toBe(1)
expect(mem.numVariables(0)).toBe(2)
})
it('execute object expression', async () => {
const code = [
'const three = 3',
"const yo = {aStr: 'str', anum: 2, identifier: three, binExp: 4 + 5}",
].join('\n')
const mem = await exe(code)
expect(mem.get('yo')).toEqual({
const { root } = await exe(code)
expect(root.yo).toEqual({
type: 'UserVal',
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
__meta: [
@ -258,8 +257,8 @@ const newVar = myVar + 1`
const code = ["const yo = {a: {b: '123'}}", "const myVar = yo.a['b']"].join(
'\n'
)
const mem = await exe(code)
expect(mem.get('myVar')).toEqual({
const { root } = await exe(code)
expect(root.myVar).toEqual({
type: 'UserVal',
value: '123',
__meta: [
@ -274,81 +273,81 @@ const newVar = myVar + 1`
describe('testing math operators', () => {
it('can sum', async () => {
const code = ['const myVar = 1 + 2'].join('\n')
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(3)
const { root } = await exe(code)
expect(root.myVar.value).toBe(3)
})
it('can subtract', async () => {
const code = ['const myVar = 1 - 2'].join('\n')
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(-1)
const { root } = await exe(code)
expect(root.myVar.value).toBe(-1)
})
it('can multiply', async () => {
const code = ['const myVar = 1 * 2'].join('\n')
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(2)
const { root } = await exe(code)
expect(root.myVar.value).toBe(2)
})
it('can divide', async () => {
const code = ['const myVar = 1 / 2'].join('\n')
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(0.5)
const { root } = await exe(code)
expect(root.myVar.value).toBe(0.5)
})
it('can modulus', async () => {
const code = ['const myVar = 5 % 2'].join('\n')
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(1)
const { root } = await exe(code)
expect(root.myVar.value).toBe(1)
})
it('can do multiple operations', async () => {
const code = ['const myVar = 1 + 2 * 3'].join('\n')
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(7)
const { root } = await exe(code)
expect(root.myVar.value).toBe(7)
})
it('big example with parans', async () => {
const code = ['const myVar = 1 + 2 * (3 - 4) / -5 + 6'].join('\n')
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(7.4)
const { root } = await exe(code)
expect(root.myVar.value).toBe(7.4)
})
it('with identifier', async () => {
const code = ['const yo = 6', 'const myVar = yo / 2'].join('\n')
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(3)
const { root } = await exe(code)
expect(root.myVar.value).toBe(3)
})
it('with lots of testing', async () => {
const code = ['const myVar = 2 * ((2 + 3 ) / 4 + 5)'].join('\n')
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(12.5)
const { root } = await exe(code)
expect(root.myVar.value).toBe(12.5)
})
it('with callExpression at start', async () => {
const code = 'const myVar = min(4, 100) + 2'
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(6)
const { root } = await exe(code)
expect(root.myVar.value).toBe(6)
})
it('with callExpression at end', async () => {
const code = 'const myVar = 2 + min(4, 100)'
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(6)
const { root } = await exe(code)
expect(root.myVar.value).toBe(6)
})
it('with nested callExpression', async () => {
const code = 'const myVar = 2 + min(100, legLen(5, 3))'
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(6)
const { root } = await exe(code)
expect(root.myVar.value).toBe(6)
})
it('with unaryExpression', async () => {
const code = 'const myVar = -min(100, 3)'
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(-3)
const { root } = await exe(code)
expect(root.myVar.value).toBe(-3)
})
it('with unaryExpression in callExpression', async () => {
const code = 'const myVar = min(-legLen(5, 4), 5)'
const code2 = 'const myVar = min(5 , -legLen(5, 4))'
const mem = await exe(code)
const mem2 = await exe(code2)
expect(mem.get('myVar')?.value).toBe(-3)
expect(mem.get('myVar')?.value).toBe(mem2.get('myVar')?.value)
const { root } = await exe(code)
const { root: root2 } = await exe(code2)
expect(root.myVar.value).toBe(-3)
expect(root.myVar.value).toBe(root2.myVar.value)
})
it('with unaryExpression in ArrayExpression', async () => {
const code = 'const myVar = [1,-legLen(5, 4)]'
const mem = await exe(code)
expect(mem.get('myVar')?.value).toEqual([1, -3])
const { root } = await exe(code)
expect(root.myVar.value).toEqual([1, -3])
})
it('with unaryExpression in ArrayExpression in CallExpression, checking nothing funny happens when used in a sketch', async () => {
const code = [
@ -356,8 +355,8 @@ describe('testing math operators', () => {
' |> startProfileAt([0, 0], %)',
'|> line([-2.21, -legLen(5, min(3, 999))], %)',
].join('\n')
const mem = await exe(code)
const sketch = mem.get('part001')
const { root } = await exe(code)
const sketch = root.part001
// result of `-legLen(5, min(3, 999))` should be -4
const yVal = (sketch as SketchGroup).value?.[0]?.to?.[1]
expect(yVal).toBe(-4)
@ -374,8 +373,8 @@ describe('testing math operators', () => {
`], %)`,
``,
].join('\n')
const mem = await exe(code)
const sketch = mem.get('part001')
const { root } = await exe(code)
const sketch = root.part001
// expect -legLen(segLen('seg01', %), myVar) to equal -4 setting the y value back to 0
expect((sketch as SketchGroup).value?.[1]?.from).toEqual([3, 4])
expect((sketch as SketchGroup).value?.[1]?.to).toEqual([6, 0])
@ -383,18 +382,18 @@ describe('testing math operators', () => {
`-legLen(segLen('seg01', %), myVar)`,
`legLen(segLen('seg01', %), myVar)`
)
const removedUnaryExpMem = await exe(removedUnaryExp)
const removedUnaryExpMemSketch = removedUnaryExpMem.get('part001')
const { root: removedUnaryExpRoot } = await exe(removedUnaryExp)
const removedUnaryExpRootSketch = removedUnaryExpRoot.part001
// without the minus sign, the y value should be 8
expect((removedUnaryExpMemSketch as SketchGroup).value?.[1]?.to).toEqual([
expect((removedUnaryExpRootSketch as SketchGroup).value?.[1]?.to).toEqual([
6, 8,
])
})
it('with nested callExpression and binaryExpression', async () => {
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'
const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(5)
const { root } = await exe(code)
expect(root.myVar.value).toBe(5)
})
})
@ -422,7 +421,7 @@ const theExtrude = startSketchOn('XY')
async function exe(
code: string,
programMemory: ProgramMemory = ProgramMemory.empty()
programMemory: ProgramMemory = { root: {}, return: null }
) {
const ast = parse(code)

View File

@ -79,14 +79,20 @@ export async function executeAst({
return {
errors: [e],
logs: [],
programMemory: ProgramMemory.empty(),
programMemory: {
root: {},
return: null,
},
}
} else {
console.log(e)
return {
logs: [e],
errors: [],
programMemory: ProgramMemory.empty(),
programMemory: {
root: {},
return: null,
},
}
}
}

View File

@ -983,7 +983,7 @@ export async function deleteFromSelection(
if (err(parent)) {
return
}
const sketchToPreserve = programMemory.get(sketchName) as SketchGroup
const sketchToPreserve = programMemory.root[sketchName] as SketchGroup
console.log('sketchName', sketchName)
// Can't kick off multiple requests at once as getFaceDetails
// is three engine calls in one and they conflict

View File

@ -130,14 +130,8 @@ function moreNodePathFromSourceRange(
const isInRange = _node.start <= start && _node.end >= end
if (
(_node.type === 'Identifier' ||
_node.type === 'Literal' ||
_node.type === 'TagDeclarator') &&
isInRange
) {
if ((_node.type === 'Identifier' || _node.type === 'Literal') && isInRange)
return path
}
if (_node.type === 'CallExpression' && isInRange) {
const { callee, arguments: args } = _node
@ -283,15 +277,6 @@ function moreNodePathFromSourceRange(
}
}
}
return path
}
if (_node.type === 'ReturnStatement' && isInRange) {
const { argument } = _node
if (argument.start <= start && argument.end >= end) {
path.push(['argument', 'ReturnStatement'])
return moreNodePathFromSourceRange(argument, sourceRange, path)
}
return path
}
if (_node.type === 'MemberExpression' && isInRange) {
const { object, property } = _node
@ -474,8 +459,8 @@ export function findAllPreviousVariablesPath(
bodyItems?.forEach?.((item) => {
if (item.type !== 'VariableDeclaration' || item.end > startRange) return
const varName = item.declarations[0].id.name
const varValue = programMemory?.get(varName)
if (!varValue || typeof varValue?.value !== type) return
const varValue = programMemory?.root[varName]
if (typeof varValue?.value !== type) return
variables.push({
key: varName,
value: varValue.value,
@ -655,7 +640,7 @@ export function isLinesParallelAndConstrained(
if (err(_varDec)) return _varDec
const varDec = _varDec.node
const varName = (varDec as VariableDeclaration)?.declarations[0]?.id?.name
const path = programMemory?.get(varName) as SketchGroup
const path = programMemory?.root[varName] as SketchGroup
const _primarySegment = getSketchSegmentFromSourceRange(
path,
primaryLine.range
@ -702,7 +687,7 @@ export function isLinesParallelAndConstrained(
constraintType === 'angle' || constraintLevel === 'full'
// get the previous segment
const prevSegment = (programMemory.get(varName) as SketchGroup).value[
const prevSegment = (programMemory.root[varName] as SketchGroup).value[
secondaryIndex - 1
]
const prevSourceRange = prevSegment.__geoMeta.sourceRange
@ -772,7 +757,7 @@ export function hasExtrudeSketchGroup({
const varDec = varDecMeta.node
if (varDec.type !== 'VariableDeclaration') return false
const varName = varDec.declarations[0].id.name
const varValue = programMemory?.get(varName)
const varValue = programMemory?.root[varName]
return varValue?.type === 'ExtrudeGroup' || varValue?.type === 'SketchGroup'
}

View File

@ -1009,8 +1009,8 @@ export const angledLineOfXLength: SketchLineHelper = {
const { node: varDec } = nodeMeta2
const variableName = varDec.id.name
const sketch = previousProgramMemory?.get(variableName)
if (!sketch || sketch.type !== 'SketchGroup') {
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'SketchGroup') {
return new Error('not a SketchGroup')
}
const angle = createLiteral(roundOff(getAngle(from, to), 0))
@ -1105,8 +1105,8 @@ export const angledLineOfYLength: SketchLineHelper = {
if (err(nodeMeta2)) return nodeMeta2
const { node: varDec } = nodeMeta2
const variableName = varDec.id.name
const sketch = previousProgramMemory?.get(variableName)
if (!sketch || sketch.type !== 'SketchGroup') {
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'SketchGroup') {
return new Error('not a SketchGroup')
}
@ -1443,7 +1443,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
const { node: varDec } = nodeMeta2
const varName = varDec.declarations[0].id.name
const sketchGroup = previousProgramMemory.get(varName) as SketchGroup
const sketchGroup = previousProgramMemory.root[varName] as SketchGroup
const intersectPath = sketchGroup.value.find(
({ tag }: Path) => tag && tag.value === intersectTagName
)

View File

@ -363,7 +363,7 @@ const part001 = startSketchOn('XY')
const programMemory = await enginelessExecutor(parse(code))
const index = code.indexOf('// normal-segment') - 7
const _segment = getSketchSegmentFromSourceRange(
programMemory.get('part001') as SketchGroup,
programMemory.root['part001'] as SketchGroup,
[index, index]
)
if (err(_segment)) throw _segment
@ -379,7 +379,7 @@ const part001 = startSketchOn('XY')
const programMemory = await enginelessExecutor(parse(code))
const index = code.indexOf('// segment-in-start') - 7
const _segment = getSketchSegmentFromSourceRange(
programMemory.get('part001') as SketchGroup,
programMemory.root['part001'] as SketchGroup,
[index, index]
)
if (err(_segment)) throw _segment

View File

@ -1636,8 +1636,8 @@ export function transformAstSketchLines({
})
const varName = varDec.node.id.name
let sketchGroup = programMemory.get(varName)
if (sketchGroup?.type === 'ExtrudeGroup') {
let sketchGroup = programMemory.root?.[varName]
if (sketchGroup.type === 'ExtrudeGroup') {
sketchGroup = sketchGroup.sketchGroup
}
if (!sketchGroup || sketchGroup.type !== 'SketchGroup')

View File

@ -17,9 +17,9 @@ describe('testing angledLineThatIntersects', () => {
offset: ${offset},
}, %, "yo2")
const intersect = segEndX('yo2', part001)`
const mem = await enginelessExecutor(parse(code('-1')))
expect(mem.get('intersect')?.value).toBe(1 + Math.sqrt(2))
const noOffset = await enginelessExecutor(parse(code('0')))
expect(noOffset.get('intersect')?.value).toBeCloseTo(1)
const { root } = await enginelessExecutor(parse(code('-1')))
expect(root.intersect.value).toBe(1 + Math.sqrt(2))
const { root: noOffset } = await enginelessExecutor(parse(code('0')))
expect(noOffset.intersect.value).toBeCloseTo(1)
})
})

View File

@ -143,200 +143,14 @@ interface Memory {
[key: string]: MemoryItem
}
type EnvironmentRef = number
const ROOT_ENVIRONMENT_REF: EnvironmentRef = 0
interface Environment {
bindings: Memory
parent: EnvironmentRef | null
}
function emptyEnvironment(): Environment {
return { bindings: {}, parent: null }
}
interface RawProgramMemory {
environments: Environment[]
currentEnv: EnvironmentRef
export interface ProgramMemory {
root: Memory
return: ProgramReturn | null
}
/**
* This duplicates logic in Rust. The hope is to keep ProgramMemory internals
* isolated from the rest of the TypeScript code so that we can move it to Rust
* in the future.
*/
export class ProgramMemory {
private environments: Environment[]
private currentEnv: EnvironmentRef
private return: ProgramReturn | null
/**
* Empty memory doesn't include prelude definitions.
*/
static empty(): ProgramMemory {
return new ProgramMemory()
}
static fromRaw(raw: RawProgramMemory): ProgramMemory {
return new ProgramMemory(raw.environments, raw.currentEnv, raw.return)
}
constructor(
environments: Environment[] = [emptyEnvironment()],
currentEnv: EnvironmentRef = ROOT_ENVIRONMENT_REF,
returnVal: ProgramReturn | null = null
) {
this.environments = environments
this.currentEnv = currentEnv
this.return = returnVal
}
/**
* Returns a deep copy.
*/
clone(): ProgramMemory {
return ProgramMemory.fromRaw(JSON.parse(JSON.stringify(this.toRaw())))
}
has(name: string): boolean {
let envRef = this.currentEnv
while (true) {
const env = this.environments[envRef]
if (env.bindings.hasOwnProperty(name)) {
return true
}
if (!env.parent) {
break
}
envRef = env.parent
}
return false
}
get(name: string): MemoryItem | null {
let envRef = this.currentEnv
while (true) {
const env = this.environments[envRef]
if (env.bindings.hasOwnProperty(name)) {
return env.bindings[name]
}
if (!env.parent) {
break
}
envRef = env.parent
}
return null
}
set(name: string, value: MemoryItem): Error | null {
if (this.environments.length === 0) {
return new Error('No environment to set memory in')
}
const env = this.environments[this.currentEnv]
env.bindings[name] = value
return null
}
/**
* Returns a new ProgramMemory with only `MemoryItem`s that pass the
* predicate. Values are deep copied.
*
* Note: Return value of the returned ProgramMemory is always null.
*/
filterVariables(
keepPrelude: boolean,
predicate: (value: MemoryItem) => boolean
): ProgramMemory | Error {
const environments: Environment[] = []
for (const [i, env] of this.environments.entries()) {
let bindings: Memory
if (i === ROOT_ENVIRONMENT_REF && keepPrelude) {
// Get prelude definitions. Create these first so that they're always
// first in iteration order.
const memoryOrError = programMemoryInit()
if (err(memoryOrError)) return memoryOrError
bindings = memoryOrError.environments[0].bindings
} else {
bindings = emptyEnvironment().bindings
}
for (const [name, value] of Object.entries(env.bindings)) {
// Check the predicate.
if (!predicate(value)) {
continue
}
// Deep copy.
bindings[name] = JSON.parse(JSON.stringify(value))
}
environments.push({ bindings, parent: env.parent })
}
return new ProgramMemory(environments, this.currentEnv, null)
}
numEnvironments(): number {
return this.environments.length
}
numVariables(envRef: EnvironmentRef): number {
return Object.keys(this.environments[envRef]).length
}
/**
* Returns all variable entries in memory that are visible, in a flat
* structure. If variables are shadowed, they're not visible, and therefore,
* not included.
*
* This should only be used to display in the MemoryPane UI.
*/
visibleEntries(): Map<string, MemoryItem> {
const map = new Map<string, MemoryItem>()
let envRef = this.currentEnv
while (true) {
const env = this.environments[envRef]
for (const [name, value] of Object.entries(env.bindings)) {
// Don't include shadowed variables.
if (!map.has(name)) {
map.set(name, value)
}
}
if (!env.parent) {
break
}
envRef = env.parent
}
return map
}
/**
* Returns true if any visible variables are a SketchGroup or ExtrudeGroup.
*/
hasSketchOrExtrudeGroup(): boolean {
for (const node of this.visibleEntries().values()) {
if (node.type === 'ExtrudeGroup' || node.type === 'SketchGroup') {
return true
}
}
return false
}
/**
* Return the representation that can be serialized to JSON. This should only
* be used within this module.
*/
toRaw(): RawProgramMemory {
return {
environments: this.environments,
currentEnv: this.currentEnv,
return: this.return,
}
}
}
export const executor = async (
node: Program,
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
programMemory: ProgramMemory | Error = { root: {}, return: null },
engineCommandManager: EngineCommandManager,
isMock: boolean = false
): Promise<ProgramMemory> => {
@ -357,7 +171,7 @@ export const executor = async (
export const _executor = async (
node: Program,
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
programMemory: ProgramMemory | Error = { root: {}, return: null },
engineCommandManager: EngineCommandManager,
isMock: boolean
): Promise<ProgramMemory> => {
@ -372,15 +186,15 @@ export const _executor = async (
baseUnit =
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
}
const memory: RawProgramMemory = await execute_wasm(
const memory: ProgramMemory = await execute_wasm(
JSON.stringify(node),
JSON.stringify(programMemory.toRaw()),
JSON.stringify(programMemory),
baseUnit,
engineCommandManager,
fileSystemManager,
isMock
)
return ProgramMemory.fromRaw(memory)
return memory
} catch (e: any) {
console.log(e)
const parsed: RustKclError = JSON.parse(e.toString())
@ -515,17 +329,10 @@ export function getTangentialArcToInfo({
}
}
/**
* Returns new ProgramMemory with prelude definitions.
*/
export function programMemoryInit(): ProgramMemory | Error {
try {
const memory: RawProgramMemory = program_memory_init()
return new ProgramMemory(
memory.environments,
memory.currentEnv,
memory.return
)
const memory: ProgramMemory = program_memory_init()
return memory
} catch (e: any) {
console.log(e)
const parsed: RustKclError = JSON.parse(e.toString())

View File

@ -75,7 +75,7 @@ class MockEngineCommandManager {
export async function enginelessExecutor(
ast: Program | Error,
pm: ProgramMemory | Error = ProgramMemory.empty()
pm: ProgramMemory | Error = { root: {}, return: null }
): Promise<ProgramMemory> {
if (err(ast)) return Promise.reject(ast)
if (err(pm)) return Promise.reject(pm)
@ -93,7 +93,7 @@ export async function enginelessExecutor(
export async function executor(
ast: Program,
pm: ProgramMemory = ProgramMemory.empty()
pm: ProgramMemory = { root: {}, return: null }
): Promise<ProgramMemory> {
const engineCommandManager = new EngineCommandManager()
engineCommandManager.start({

View File

@ -3,7 +3,7 @@ import { kclManager, engineCommandManager } from 'lib/singletons'
import { useKclContext } from 'lang/KclProvider'
import { findUniqueName } from 'lang/modifyAst'
import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
import { ProgramMemory, Value, parse } from 'lang/wasm'
import { Value, parse } from 'lang/wasm'
import { useEffect, useRef, useState } from 'react'
import { executeAst } from 'lang/langHelpers'
import { err, trap } from 'lib/trap'
@ -60,8 +60,9 @@ export function useCalculateKclExpression({
}, [])
useEffect(() => {
const allVarNames = Object.keys(programMemory.root)
if (
programMemory.has(newVariableName) ||
allVarNames.includes(newVariableName) ||
newVariableName === '' ||
!isValidVariableName(newVariableName)
) {
@ -88,20 +89,17 @@ export function useCalculateKclExpression({
if (err(ast)) return
if (trap(ast, { suppress: true })) return
const _programMem: ProgramMemory = ProgramMemory.empty()
for (const { key, value } of availableVarInfo.variables) {
const error = _programMem.set(key, {
type: 'UserVal',
value,
__meta: [],
})
if (trap(error, { suppress: true })) return
}
const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
})
const { programMemory } = await executeAst({
ast,
engineCommandManager,
useFakeExecutor: true,
programMemoryOverride: kclManager.programMemory.clone(),
programMemoryOverride: JSON.parse(
JSON.stringify(kclManager.programMemory)
),
})
const resultDeclaration = ast.body.find(
(a) =>
@ -111,7 +109,7 @@ export function useCalculateKclExpression({
const init =
resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.get('__result__')?.value
const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init)
}

View File

@ -1139,8 +1139,8 @@ export const modelingMachine = createMachine(
)
if (err(varDecNode)) return
const sketchVar = varDecNode.node.declarations[0].id.name
const sketchGroup = kclManager.programMemory.get(sketchVar)
if (sketchGroup?.type !== 'SketchGroup') return
const sketchGroup = kclManager.programMemory.root[sketchVar]
if (sketchGroup.type !== 'SketchGroup') return
const idArtifact = engineCommandManager.artifactMap[sketchGroup.id]
if (idArtifact.commandType !== 'start_path') return
const extrusionArtifactId = (idArtifact as any)?.extrusions?.[0]

View File

@ -1388,7 +1388,7 @@ impl CallExpression {
}
FunctionKind::UserDefined => {
let func = memory.get(&fn_name, self.into())?;
let result = func.call_fn(fn_args, ctx.clone()).await.map_err(|e| {
let result = func.call_fn(fn_args, memory.clone(), ctx.clone()).await.map_err(|e| {
// Add the call expression to the source ranges.
e.add_source_ranges(vec![self.into()])
})?;
@ -2880,30 +2880,6 @@ impl BinaryExpression {
pipe_info: &PipeInfo,
ctx: &ExecutorContext,
) -> Result<MemoryItem, KclError> {
// First check if we are doing short-circuiting logical operator.
if self.operator == BinaryOperator::LogicalOr {
let left_json_value = self.left.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
let left = json_to_bool(&left_json_value);
if left {
// Short-circuit.
return Ok(MemoryItem::UserVal(UserVal {
value: serde_json::Value::Bool(left),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let right_json_value = self.right.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
let right = json_to_bool(&right_json_value);
return Ok(MemoryItem::UserVal(UserVal {
value: serde_json::Value::Bool(right),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let left_json_value = self.left.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
let right_json_value = self.right.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
@ -2933,9 +2909,6 @@ impl BinaryExpression {
BinaryOperator::Div => (left / right).into(),
BinaryOperator::Mod => (left % right).into(),
BinaryOperator::Pow => (left.powf(right)).into(),
BinaryOperator::LogicalOr => {
unreachable!("LogicalOr should have been handled above")
}
};
Ok(MemoryItem::UserVal(UserVal {
@ -2977,27 +2950,6 @@ pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
}
}
pub fn json_to_bool(j: &serde_json::Value) -> bool {
match j {
JValue::Null => false,
JValue::Bool(b) => *b,
JValue::Number(n) => {
if let Some(n) = n.as_u64() {
n != 0
} else if let Some(n) = n.as_i64() {
n != 0
} else if let Some(x) = n.as_f64() {
x != 0.0 && !x.is_nan()
} else {
false
}
}
JValue::String(s) => !s.is_empty(),
JValue::Array(a) => !a.is_empty(),
JValue::Object(_) => false,
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
@ -3028,10 +2980,6 @@ pub enum BinaryOperator {
#[serde(rename = "^")]
#[display("^")]
Pow,
/// Logical OR.
#[serde(rename = "||")]
#[display("||")]
LogicalOr,
}
/// Mathematical associativity.
@ -3060,7 +3008,6 @@ impl BinaryOperator {
BinaryOperator::Div => *b"div",
BinaryOperator::Mod => *b"mod",
BinaryOperator::Pow => *b"pow",
BinaryOperator::LogicalOr => *b"lor",
}
}
@ -3071,7 +3018,6 @@ impl BinaryOperator {
BinaryOperator::Add | BinaryOperator::Sub => 11,
BinaryOperator::Mul | BinaryOperator::Div | BinaryOperator::Mod => 12,
BinaryOperator::Pow => 6,
BinaryOperator::LogicalOr => 3,
}
}
@ -3079,7 +3025,7 @@ impl BinaryOperator {
/// Taken from <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table>
pub fn associativity(&self) -> Associativity {
match self {
Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod | Self::LogicalOr => Associativity::Left,
Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod => Associativity::Left,
Self::Pow => Associativity::Right,
}
}
@ -3143,21 +3089,6 @@ impl UnaryExpression {
pipe_info: &PipeInfo,
ctx: &ExecutorContext,
) -> Result<MemoryItem, KclError> {
if self.operator == UnaryOperator::Not {
let value = self
.argument
.get_result(memory, pipe_info, ctx)
.await?
.get_json_value()?;
let negated = !json_to_bool(&value);
return Ok(MemoryItem::UserVal(UserVal {
value: serde_json::Value::Bool(negated),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let num = parse_json_number_as_f64(
&self
.argument

View File

@ -23,8 +23,7 @@ use crate::{
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ProgramMemory {
pub environments: Vec<Environment>,
pub current_env: EnvironmentRef,
pub root: HashMap<String, MemoryItem>,
#[serde(rename = "return")]
pub return_: Option<ProgramReturn>,
}
@ -32,105 +31,7 @@ pub struct ProgramMemory {
impl ProgramMemory {
pub fn new() -> Self {
Self {
environments: vec![Environment::root()],
current_env: EnvironmentRef::root(),
return_: None,
}
}
pub fn new_env_for_call(&mut self, parent: EnvironmentRef) -> EnvironmentRef {
let new_env_ref = EnvironmentRef(self.environments.len());
let new_env = Environment::new(parent);
self.environments.push(new_env);
new_env_ref
}
/// Add to the program memory in the current scope.
pub fn add(&mut self, key: &str, value: MemoryItem, source_range: SourceRange) -> Result<(), KclError> {
if self.environments[self.current_env.index()].contains_key(key) {
return Err(KclError::ValueAlreadyDefined(KclErrorDetails {
message: format!("Cannot redefine `{}`", key),
source_ranges: vec![source_range],
}));
}
self.environments[self.current_env.index()].insert(key.to_string(), value);
Ok(())
}
/// Get a value from the program memory.
/// Return Err if not found.
pub fn get(&self, var: &str, source_range: SourceRange) -> Result<&MemoryItem, KclError> {
let mut env_ref = self.current_env;
loop {
let env = &self.environments[env_ref.index()];
if let Some(item) = env.bindings.get(var) {
return Ok(item);
}
if let Some(parent) = env.parent {
env_ref = parent;
} else {
break;
}
}
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("memory item key `{}` is not defined", var),
source_ranges: vec![source_range],
}))
}
/// Find all extrude groups in the memory that are on a specific sketch group id.
/// This does not look inside closures. But as long as we do not allow
/// mutation of variables in KCL, closure memory should be a subset of this.
pub fn find_extrude_groups_on_sketch_group(&self, sketch_group_id: uuid::Uuid) -> Vec<Box<ExtrudeGroup>> {
self.environments
.iter()
.flat_map(|env| {
env.bindings
.values()
.filter_map(|item| match item {
MemoryItem::ExtrudeGroup(eg) if eg.sketch_group.id == sketch_group_id => Some(eg.clone()),
_ => None,
})
.collect::<Vec<_>>()
})
.collect()
}
}
impl Default for ProgramMemory {
fn default() -> Self {
Self::new()
}
}
/// An index pointing to an environment.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
pub struct EnvironmentRef(usize);
impl EnvironmentRef {
pub fn root() -> Self {
Self(0)
}
pub fn index(&self) -> usize {
self.0
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
pub struct Environment {
bindings: HashMap<String, MemoryItem>,
parent: Option<EnvironmentRef>,
}
impl Environment {
pub fn root() -> Self {
Self {
// Prelude
bindings: HashMap::from([
root: HashMap::from([
(
"ZERO".to_string(),
MemoryItem::UserVal(UserVal {
@ -160,19 +61,28 @@ impl Environment {
}),
),
]),
parent: None,
return_: None,
}
}
pub fn new(parent: EnvironmentRef) -> Self {
Self {
bindings: HashMap::new(),
parent: Some(parent),
/// Add to the program memory.
pub fn add(&mut self, key: &str, value: MemoryItem, source_range: SourceRange) -> Result<(), KclError> {
if self.root.contains_key(key) {
return Err(KclError::ValueAlreadyDefined(KclErrorDetails {
message: format!("Cannot redefine `{}`", key),
source_ranges: vec![source_range],
}));
}
self.root.insert(key.to_string(), value);
Ok(())
}
/// Get a value from the program memory.
/// Return Err if not found.
pub fn get(&self, key: &str, source_range: SourceRange) -> Result<&MemoryItem, KclError> {
self.bindings.get(key).ok_or_else(|| {
self.root.get(key).ok_or_else(|| {
KclError::UndefinedValue(KclErrorDetails {
message: format!("memory item key `{}` is not defined", key),
source_ranges: vec![source_range],
@ -180,12 +90,21 @@ impl Environment {
})
}
pub fn insert(&mut self, key: String, value: MemoryItem) {
self.bindings.insert(key, value);
/// Find all extrude groups in the memory that are on a specific sketch group id.
pub fn find_extrude_groups_on_sketch_group(&self, sketch_group_id: uuid::Uuid) -> Vec<Box<ExtrudeGroup>> {
self.root
.values()
.filter_map(|item| match item {
MemoryItem::ExtrudeGroup(eg) if eg.sketch_group.id == sketch_group_id => Some(eg.clone()),
_ => None,
})
.collect()
}
}
pub fn contains_key(&self, key: &str) -> bool {
self.bindings.contains_key(key)
impl Default for ProgramMemory {
fn default() -> Self {
Self::new()
}
}
@ -242,7 +161,6 @@ pub enum MemoryItem {
#[serde(skip)]
func: Option<MemoryFunction>,
expression: Box<FunctionExpression>,
memory: Box<ProgramMemory>,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
@ -702,7 +620,7 @@ impl MemoryItem {
.map(Some)
}
pub fn as_user_val(&self) -> Option<&UserVal> {
fn as_user_val(&self) -> Option<&UserVal> {
if let MemoryItem::UserVal(x) = self {
Some(x)
} else {
@ -724,21 +642,27 @@ impl MemoryItem {
}
/// If this value is of type function, return it.
pub fn get_function(&self) -> Option<FnAsArg<'_>> {
pub fn get_function(&self, source_ranges: Vec<SourceRange>) -> Result<FnAsArg<'_>, KclError> {
let MemoryItem::Function {
func,
expression,
memory,
meta: _,
} = &self
else {
return None;
return Err(KclError::Semantic(KclErrorDetails {
message: "not an in-memory function".to_string(),
source_ranges,
}));
};
let func = func.as_ref()?;
Some(FnAsArg {
let func = func.as_ref().ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Not an in-memory function: {:?}", expression),
source_ranges,
})
})?;
Ok(FnAsArg {
func,
expr: expression.to_owned(),
memory: memory.to_owned(),
})
}
@ -812,15 +736,10 @@ impl MemoryItem {
pub async fn call_fn(
&self,
args: Vec<MemoryItem>,
memory: ProgramMemory,
ctx: ExecutorContext,
) -> Result<Option<ProgramReturn>, KclError> {
let MemoryItem::Function {
func,
expression,
memory: closure_memory,
meta,
} = &self
else {
let MemoryItem::Function { func, expression, meta } = &self else {
return Err(KclError::Semantic(KclErrorDetails {
message: "not a in memory function".to_string(),
source_ranges: vec![],
@ -832,14 +751,7 @@ impl MemoryItem {
source_ranges: vec![],
}));
};
func(
args,
closure_memory.as_ref().clone(),
expression.clone(),
meta.clone(),
ctx,
)
.await
func(args, memory, expression.clone(), meta.clone(), ctx).await
}
}
@ -1648,13 +1560,16 @@ impl ExecutorContext {
memory.return_ = result.return_;
}
FunctionKind::UserDefined => {
// TODO: Why do we change the source range to
// the call expression instead of keeping the
// range of the callee?
let func = memory.get(&fn_name, call_expr.into())?;
let result = func.call_fn(args.clone(), self.clone()).await?;
if let Some(func) = memory.clone().root.get(&fn_name) {
let result = func.call_fn(args.clone(), memory.clone(), self.clone()).await?;
memory.return_ = result;
memory.return_ = result;
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("No such name {} defined", fn_name),
source_ranges: vec![call_expr.into()],
}));
}
}
}
}
@ -1765,15 +1680,7 @@ impl ExecutorContext {
_metadata: Vec<Metadata>,
ctx: ExecutorContext| {
Box::pin(async move {
// Create a new environment to execute the function
// body in so that local variables shadow variables
// in the parent scope. The new environment's
// parent should be the environment of the closure.
let mut body_memory = memory.clone();
let closure_env = memory.current_env;
let body_env = body_memory.new_env_for_call(closure_env);
body_memory.current_env = body_env;
let mut fn_memory = assign_args_to_params(&function_expression, args, body_memory)?;
let mut fn_memory = assign_args_to_params(&function_expression, args, memory.clone())?;
let result = ctx
.inner_execute(&function_expression.body, &mut fn_memory, BodyType::Block)
@ -1783,14 +1690,10 @@ impl ExecutorContext {
})
},
);
// Cloning memory here is crucial for semantics so that we close
// over variables. Variables defined lexically later shouldn't
// be available to the function body.
MemoryItem::Function {
expression: function_expression.clone(),
meta: vec![metadata.to_owned()],
func: Some(mem_func),
memory: Box::new(memory.clone()),
}
}
Value::CallExpression(call_expression) => call_expression.execute(memory, pipe_info, self).await?,
@ -1893,8 +1796,7 @@ fn assign_args_to_params(
return Err(err_wrong_number_args);
}
// Add the arguments to the memory. A new call frame should have already
// been created.
// Add the arguments to the memory.
for (index, param) in function_expression.params.iter().enumerate() {
if let Some(arg) = args.get(index) {
// Argument was provided.
@ -1960,19 +1862,11 @@ const newVar = myVar + 1"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(5),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
memory.root.get("myVar").unwrap().get_json_value().unwrap()
);
assert_eq!(
serde_json::json!(6.0),
memory
.get("newVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
memory.root.get("newVar").unwrap().get_json_value().unwrap()
);
}
@ -1997,21 +1891,13 @@ const intersect = segEndX('yo2', part001)"#,
let memory = parse_execute(&ast_fn("-1")).await.unwrap();
assert_eq!(
serde_json::json!(1.0 + 2.0f64.sqrt()),
memory
.get("intersect", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
memory.root.get("intersect").unwrap().get_json_value().unwrap()
);
let memory = parse_execute(&ast_fn("0")).await.unwrap();
assert_eq!(
serde_json::json!(1.0000000000000002),
memory
.get("intersect", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
memory.root.get("intersect").unwrap().get_json_value().unwrap()
);
}
@ -2329,252 +2215,13 @@ const thisBox = box([[0,0], 6, 10, 3])
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_function_cannot_access_future_definitions() {
let ast = r#"
fn returnX = () => {
// x shouldn't be defined yet.
return x
}
const x = 5
const answer = returnX()"#;
let result = parse_execute(ast).await;
let err = result.unwrap_err().downcast::<KclError>().unwrap();
assert_eq!(
err,
KclError::UndefinedValue(KclErrorDetails {
message: "memory item key `x` is not defined".to_owned(),
source_ranges: vec![SourceRange([64, 65]), SourceRange([97, 106])],
}),
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_pattern_transform_function_cannot_access_future_definitions() {
let ast = r#"
fn transform = (replicaId) => {
// x shouldn't be defined yet.
let scale = x
return {
translate: [0, 0, replicaId * 10],
scale: [scale, 1, 0],
}
}
fn layer = () => {
return startSketchOn("XY")
|> circle([0, 0], 1, %, 'tag1')
|> extrude(10, %)
}
const x = 5
// The 10 layers are replicas of each other, with a transform applied to each.
let shape = layer() |> patternTransform(10, transform, %)
"#;
let result = parse_execute(ast).await;
let err = result.unwrap_err().downcast::<KclError>().unwrap();
assert_eq!(
err,
KclError::UndefinedValue(KclErrorDetails {
message: "memory item key `x` is not defined".to_owned(),
source_ranges: vec![SourceRange([80, 81])],
}),
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_function_with_parameter_redefined_outside() {
let ast = r#"
fn myIdentity = (x) => {
return x
}
const x = 33
const two = myIdentity(2)"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(2),
memory
.get("two", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(33),
memory
.get("x", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_function_referencing_variable_in_parent_scope() {
let ast = r#"
const x = 22
const y = 3
fn add = (x) => {
return x + y
}
const answer = add(2)"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(5.0),
memory
.get("answer", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(22),
memory
.get("x", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_function_redefining_variable_in_parent_scope() {
let ast = r#"
const x = 1
fn foo = () => {
const x = 2
return x
}
const answer = foo()"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(2),
memory
.get("answer", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(1),
memory
.get("x", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_pattern_transform_function_redefining_variable_in_parent_scope() {
let ast = r#"
const scale = 100
fn transform = (replicaId) => {
// Redefine same variable as in parent scope.
const scale = 2
return {
translate: [0, 0, replicaId * 10],
scale: [scale, 1, 0],
}
}
fn layer = () => {
return startSketchOn("XY")
|> circle([0, 0], 1, %, 'tag1')
|> extrude(10, %)
}
// The 10 layers are replicas of each other, with a transform applied to each.
let shape = layer() |> patternTransform(10, transform, %)"#;
let memory = parse_execute(ast).await.unwrap();
// TODO: Assert that scale 2 was used.
assert_eq!(
serde_json::json!(100),
memory
.get("scale", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_ycombinator_is_even() {
let ast = r#"
// Heavily inspired by: https://raganwald.com/2018/09/10/why-y.html
fn why = (f) => {
fn inner = (maker) => {
fn inner2 = (x) => {
return f(maker(maker), x)
}
return inner2
}
return inner(
(maker) => {
fn inner2 = (x) => {
return f(maker(maker), x)
}
return inner2
}
)
}
fn innerIsEven = (self, n) => {
return !n || !self(n - 1)
}
const isEven = why(innerIsEven)
const two = isEven(2)
const three = isEven(3)
"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(true),
memory
.get("two", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(false),
memory
.get("three", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(5.0),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
memory.root.get("myVar").unwrap().get_json_value().unwrap()
);
}
@ -2584,11 +2231,7 @@ const three = isEven(3)
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(7.4),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
memory.root.get("myVar").unwrap().get_json_value().unwrap()
);
}
@ -2598,11 +2241,7 @@ const three = isEven(3)
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(1.0),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
memory.root.get("myVar").unwrap().get_json_value().unwrap()
);
}
@ -2612,11 +2251,7 @@ const three = isEven(3)
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(std::f64::consts::TAU),
memory
.get("myVar", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
memory.root.get("myVar").unwrap().get_json_value().unwrap()
);
}
@ -2626,11 +2261,7 @@ const three = isEven(3)
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(7.4),
memory
.get("thing", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
memory.root.get("thing").unwrap().get_json_value().unwrap()
);
}
@ -2760,9 +2391,7 @@ const bracket = startSketchOn('XY')
fn additional_program_memory(items: &[(String, MemoryItem)]) -> ProgramMemory {
let mut program_memory = ProgramMemory::new();
for (name, item) in items {
program_memory
.add(name.as_str(), item.clone(), SourceRange::default())
.unwrap();
program_memory.root.insert(name.to_string(), item.clone());
}
program_memory
}

View File

@ -299,7 +299,6 @@ fn binary_operator(i: TokenSlice) -> PResult<BinaryOperator> {
"*" => BinaryOperator::Mul,
"%" => BinaryOperator::Mod,
"^" => BinaryOperator::Pow,
"||" => BinaryOperator::LogicalOr,
_ => {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
@ -1137,11 +1136,11 @@ fn unary_expression(i: TokenSlice) -> PResult<UnaryExpression> {
let (operator, op_token) = any
.try_map(|token: Token| match token.token_type {
TokenType::Operator if token.value == "-" => Ok((UnaryOperator::Neg, token)),
// TODO: negation. Original parser doesn't support `not` yet.
TokenType::Operator => Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{EXPECTED} but found {} which is an operator, but not a unary one (unary operators apply to just a single operand, your operator applies to two or more operands)", token.value.as_str(),),
})),
TokenType::Bang => Ok((UnaryOperator::Not, token)),
other => Err(KclError::Syntax(KclErrorDetails { source_ranges: token.as_source_ranges(), message: format!("{EXPECTED} but found {} which is {}", token.value.as_str(), other,) })),
})
.context(expected("a unary expression, e.g. -x or -3"))

View File

@ -79,7 +79,7 @@ impl From<ParseError<&[Token], ContextError>> for KclError {
// See https://github.com/KittyCAD/modeling-app/issues/784
KclError::Syntax(KclErrorDetails {
source_ranges: bad_token.as_source_ranges(),
message: format!("Unexpected token: {}", bad_token.value),
message: "Unexpected token".to_string(),
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,7 @@ use crate::{
ast::types::FunctionExpression,
docs::StdLibFn,
errors::KclError,
executor::{MemoryItem, ProgramMemory, SketchGroup, SketchSurface},
executor::{MemoryItem, SketchGroup, SketchSurface},
std::kcl_stdlib::KclStdLibFn,
};
pub use args::Args;
@ -281,7 +281,6 @@ pub enum Primitive {
pub struct FnAsArg<'a> {
pub func: &'a crate::executor::MemoryFunction,
pub expr: Box<FunctionExpression>,
pub memory: Box<ProgramMemory>,
}
#[cfg(test)]

View File

@ -87,7 +87,7 @@ pub async fn pattern_transform(args: Args) -> Result<MemoryItem, KclError> {
fn_expr: transform.expr,
meta: vec![args.source_range.into()],
ctx: args.ctx.clone(),
memory: *transform.memory,
memory: args.current_program_memory.clone(),
},
extr,
&args,

View File

@ -90,7 +90,7 @@ fn word(i: &mut Located<&str>) -> PResult<Token> {
fn operator(i: &mut Located<&str>) -> PResult<Token> {
let (value, range) = alt((
">=", "<=", "==", "=>", "!= ", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "||", "|", "^",
">=", "<=", "==", "=>", "!= ", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "|", "^",
))
.with_span()
.parse_next(i)?;

View File

@ -1308,7 +1308,7 @@ async fn serial_test_stdlib_kcl_error_right_code_path() {
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([157, 175])], message: "Expected an argument at index 2" }"#,
r#"type: KclErrorDetails { source_ranges: [SourceRange([157, 175])], message: "Expected a SketchGroup or SketchSurface as the third argument, found `[UserVal(UserVal { value: Array [Number(2), Number(2)], meta: [Metadata { source_range: SourceRange([164, 170]) }] }), UserVal(UserVal { value: Number(0.5), meta: [Metadata { source_range: SourceRange([172, 174]) }] })]`" }"#
);
}
@ -1406,7 +1406,7 @@ const part = rectShape([0, 0], 20, 20)
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([891, 940])], message: "Argument at index 0 was supposed to be type [f64; 2] but wasn't" }"#,
r#"type: KclErrorDetails { source_ranges: [SourceRange([891, 940])], message: "Expected a [number, number] as the first argument, found `[UserVal(UserVal { value: String(\"XY\"), meta: [Metadata { source_range: SourceRange([898, 902]) }] }), UserVal(UserVal { value: Array [Number(-6.0), Number(6)], meta: [Metadata { source_range: SourceRange([904, 927]) }] }), UserVal(UserVal { value: Number(1), meta: [Metadata { source_range: SourceRange([760, 761]) }] })]`" }"#
);
}
@ -1784,31 +1784,31 @@ const part002 = startSketchOn(part001, 'end')
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_plumbus_fillets() {
let code = r#"fn make_circle = (ext, face, pos, radius) => {
let code = r#"fn make_circle = (ext, face, tag ,pos, radius) => {
const sg = startSketchOn(ext, face)
|> startProfileAt([pos[0] + radius, pos[1]], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: radius
}, %, $arc1)
}, %, tag)
|> close(%)
return sg
}
fn pentagon = (len) => {
fn pentagon = (len, taga, tagb, tagc) => {
const sg = startSketchOn('XY')
|> startProfileAt([-len / 2, -len / 2], %)
|> angledLine({ angle: 0, length: len }, %, $a)
|> angledLine({ angle: 0, length: len }, %,taga)
|> angledLine({
angle: segAng(a, %) + 180 - 108,
length: len
}, %, $b)
}, %, tagb)
|> angledLine({
angle: segAng(b, %) + 180 - 108,
length: len
}, %, $c)
}, %,tagc)
|> angledLine({
angle: segAng(c, %) + 180 - 108,
length: len
@ -1821,23 +1821,21 @@ fn pentagon = (len) => {
return sg
}
const p = pentagon(32)
const p = pentagon(32, $a, $b, $c)
|> extrude(10, %)
const circle0 = make_circle(p, p.sketchGroup.tags.a, [0, 0], 2.5)
const plumbus0 = circle0
const plumbus0 = make_circle(p,a, $arc_a, [0, 0], 2.5)
|> extrude(10, %)
|> fillet({
radius: 0.5,
tags: [circle0.tags.arc1, getOppositeEdge(circle0.tags.arc1, %)]
tags: [arc_a, getOppositeEdge(arc_a, %)]
}, %)
const circle1 = make_circle(p, p.sketchGroup.tags.b, [0, 0], 2.5)
const plumbus1 = circle1
const plumbus1 = make_circle(p, b,$arc_b, [0, 0], 2.5)
|> extrude(10, %)
|> fillet({
radius: 0.5,
tags: [circle1.tags.arc1, getOppositeEdge(circle1.tags.arc1, %)]
tags: [arc_b, getOppositeEdge(arc_b, %)]
}, %)
"#;

View File

@ -39,7 +39,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
// We need to get the sketch ID.
// Get the sketch group ID from memory.
let MemoryItem::SketchGroup(sketch_group) = memory.get(name, SourceRange::default()).unwrap() else {
let MemoryItem::SketchGroup(sketch_group) = memory.root.get(name).unwrap() else {
anyhow::bail!("part001 not found in memory: {:?}", memory);
};
let sketch_id = sketch_group.id;

108
yarn.lock
View File

@ -1922,71 +1922,71 @@
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.14.tgz#8c1c65c07559cd29c5103a99e0abe5331cc2246f"
integrity sha512-YLYgHqdwWswr4Y70+hRzaLD6kLIUgHhE3shLXNquPiTaQ9+cX3Q2dB0AFfqsua6NXYFNe7LfkmMzaqEzqv3yQg==
"@tauri-apps/cli-darwin-arm64@2.0.0-beta.13":
version "2.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.13.tgz#4926b310f5c39f967753c1c6b9aa20916011ebb6"
integrity sha512-/ibwIj1n2TQSXazGr79K4sfiZ85JndGXjMVN5QD9M8AkhpqgiSM+QT+qfIb+Y8p/RY9v1w1h3+zKMJXjhIppbA==
"@tauri-apps/cli-darwin-arm64@2.0.0-beta.22":
version "2.0.0-beta.22"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.22.tgz#5e41c880a4b324bf5c1345fec7259c6b99be2caf"
integrity sha512-Ofhythvg1Ks2IM87WUYNtgFzm21aU1Zn+8QP81lJy9Y7ZGMxP8FYfqeHz6GIWKI+CYf6I77HA8LHkT9pyE5PYg==
"@tauri-apps/cli-darwin-x64@2.0.0-beta.13":
version "2.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-beta.13.tgz#50fee7410ef12e705aa446be7f640b7da504c0e3"
integrity sha512-DNqvRzlrH0ZEo+MxdbJIFOYGPCI7iVXzPxSRU+WFz9aa388fZSVEw9jWer5WaAR5FBgp3bDjrkjPuejSb2A8fw==
"@tauri-apps/cli-darwin-x64@2.0.0-beta.22":
version "2.0.0-beta.22"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-beta.22.tgz#029fc107abaa9ede26f5fa3f0949fbf8eaf3667c"
integrity sha512-/lWIixo7WgmMUqcxlPT7Ojlkl6qbVlNDwUZ+9DtTpoWnaaBxv/YpSe1k62vDWEC7l0apFY+Fz7cRONN2wglFyQ==
"@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.13":
version "2.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-beta.13.tgz#209af191283ec4730fed8fe8e5299a91710e4b38"
integrity sha512-DACLzD8PqgURFBDTnxGODBw/8AP1M5etMrc73dCYs2d4aingc2fVxGYeIQBA0SgijznoCk+pcOmiRsNKO6gemw==
"@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.22":
version "2.0.0-beta.22"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-beta.22.tgz#5913e0a3c062e22974f0351f5e10c9fd4f66d33c"
integrity sha512-9nJCSStoxu4BKaKVJhu/uBJ8IsIofwAdsX0TWFxqo0obaZbeQSEpPhVsCy+uk3u/28dF+qyUtMCYawO2Uljnag==
"@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.13":
version "2.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-beta.13.tgz#01064bcc7cad8db596e7b54382f7b8e7a96d60e9"
integrity sha512-qm141KNMD6ZjbtAntEZYqiEbiAD0Y6CQnfzmARM9OAPkHD2vk0rnGWSa87N8lnAA27LVAnKj+nTtt77dRLlaVA==
"@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.22":
version "2.0.0-beta.22"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-beta.22.tgz#86c2107e12c2f2b1289d74a29ebe810df7bb22aa"
integrity sha512-TF9q9zHFBx9LaG2fJJC+BcpIokOmX1UIniBapndvx3dJmdDiK4F6w2QYKDkrBQVzDzcIducmdo2zNBv17O9tFQ==
"@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.13":
version "2.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-beta.13.tgz#1974a209612fd2bbd2b66dc13406f495e5e38dbd"
integrity sha512-AnB+FaqnKfGszStoj7NFZyxMV3Dz4jJcTcCE+EUYJ8Tctah35EJS/39ykskXjXonhxzg8Zr7joXRUVgGFk/yVA==
"@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.22":
version "2.0.0-beta.22"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-beta.22.tgz#8a87be68e3ffc9115052bc8d015a9515c7870969"
integrity sha512-ak/RdmaV7sATQmNOxlpHVlbKlrdquH7WH8nOv82X+iK+1HgAOGGqLqBUMzzhkGqo9SHQ9zJ6A2yOo7Z6TJXMmQ==
"@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.13":
version "2.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-beta.13.tgz#55fbbb3fe12c1c6fe1e4cae6dce055dcb23a522b"
integrity sha512-do+H48Sq/CJPRCSj7aK4j+QXi5OLbqmVt3YUB7H/krH4PFobveIhm2UpEwTjdEWO2tFTCttj07GD/OYxDhzD/g==
"@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.22":
version "2.0.0-beta.22"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-beta.22.tgz#108aaa4a939de0ea7567974c627b8061b88be091"
integrity sha512-9t+jQeMqBdXz51ikTh1PQFG/gs9PBzXmtMcIzUxE0juvH/ynjw0Vf+yZbNmwqVS9g7cj8XiBXoc6/N41SZE2cA==
"@tauri-apps/cli-linux-x64-musl@2.0.0-beta.13":
version "2.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-beta.13.tgz#67b0f6859072ec297b39dfb5bdf76fc897cfe6f4"
integrity sha512-txkn8CAb8/n6vOHvuXhUBKBJFAip6dF11qqK1lcpsgpNdv1UbvpZYYbjEd8y4jWyjN7OEoIseTtzFzXdezycDw==
"@tauri-apps/cli-linux-x64-musl@2.0.0-beta.22":
version "2.0.0-beta.22"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-beta.22.tgz#2ea7740c86dc2fc83ed5dfd3f887b0ab76545be4"
integrity sha512-PemcztfHG3HAuuo7HcnhfDrtN9NT7kueyNg8ipxJNPMa+s4K7kfieViyEiMW5pTr2F5WG/UuBSNcuwY+DVCcPA==
"@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.13":
version "2.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-beta.13.tgz#96de65e90b29545ac3825ed51c1fd22648a40299"
integrity sha512-SKa+qiZQ0+JXTHYtZKJw6RuUoolI/GU7E7pTHfkhYpGFO8UXLpTABkQ0KbN0RK0Bw/MOFFVqnAN2AoXLgPUDEA==
"@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.22":
version "2.0.0-beta.22"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-beta.22.tgz#c378ff68b99aa1afa17a583e4e7a0b16339282b7"
integrity sha512-EgKoG/jGEtTzhOp7ISjMdQsfd8IOG/5yZhO9Z4L/u7oB9mprKAJohYs24+ZxJtq2bOz4f/ZIysZ19nbkpxUzrg==
"@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.13":
version "2.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-beta.13.tgz#ff3fe14c68abed27810d727a6d7e78c7f172785c"
integrity sha512-4i9MK2mxNVF2Y1Wp6r/73Xhpevaz1sXD1DezfCDC8Fdszxo2IhkIZ0AYF5/M+TnSLyJk2u5TtFCnbaOt5e4gCg==
"@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.22":
version "2.0.0-beta.22"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-beta.22.tgz#88a81f2c6dbf62ea18252a542d45eb4190b4ed6e"
integrity sha512-67OrM2m4FB3KujPbjd/i+9lqcLDO3/ixqL1GMc3BoHhcjF+7QY08OxqWeitdsP/8ihnMIIdir2xEjNUKc6Zelw==
"@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.13":
version "2.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-beta.13.tgz#adac497f4cb289f147678ceced59102ae4d2696a"
integrity sha512-aQRwG/dc9zScIzCst646uyprppxc1Gx4jFJUw4yAEikO32SOS+90c8NFEj6H3HtZBmhzfI3JDxrGJl7ORAOCCQ==
"@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.22":
version "2.0.0-beta.22"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-beta.22.tgz#eb156270431382b64f8d95f0e86a72a6311a3b6f"
integrity sha512-BsO5xMUxliTZTImXnOC73sKT2U9VUeqR8AtklSObBcAg5LaZKpYOdF2pZzU6rIMAZwzROTAT1hYsr4r/nx2UZg==
"@tauri-apps/cli@==2.0.0-beta.13":
version "2.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.0.0-beta.13.tgz#59f410c680cce0707675c78a745bf5c6faa1a6a3"
integrity sha512-Kp0zSvrhXrOQL+8evRMJufnDYDinWXaBb1Un8x4cptrM0GAKjYddV4vjNsXvEyjlXv0S+SWJD0OUNHQyMDUlAg==
"@tauri-apps/cli@==2.0.0-beta.22":
version "2.0.0-beta.22"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.0.0-beta.22.tgz#a8c58568db903271b767ecec689bdf7632fbc8cf"
integrity sha512-OAuiDdSRKxNmr/dseQKKMoZZxIhQ6aAxmXJctGYJxCnkd62tQ8xeq87roVXGNS5Qkuv7WpySAyR0ntiMjvNLUA==
optionalDependencies:
"@tauri-apps/cli-darwin-arm64" "2.0.0-beta.13"
"@tauri-apps/cli-darwin-x64" "2.0.0-beta.13"
"@tauri-apps/cli-linux-arm-gnueabihf" "2.0.0-beta.13"
"@tauri-apps/cli-linux-arm64-gnu" "2.0.0-beta.13"
"@tauri-apps/cli-linux-arm64-musl" "2.0.0-beta.13"
"@tauri-apps/cli-linux-x64-gnu" "2.0.0-beta.13"
"@tauri-apps/cli-linux-x64-musl" "2.0.0-beta.13"
"@tauri-apps/cli-win32-arm64-msvc" "2.0.0-beta.13"
"@tauri-apps/cli-win32-ia32-msvc" "2.0.0-beta.13"
"@tauri-apps/cli-win32-x64-msvc" "2.0.0-beta.13"
"@tauri-apps/cli-darwin-arm64" "2.0.0-beta.22"
"@tauri-apps/cli-darwin-x64" "2.0.0-beta.22"
"@tauri-apps/cli-linux-arm-gnueabihf" "2.0.0-beta.22"
"@tauri-apps/cli-linux-arm64-gnu" "2.0.0-beta.22"
"@tauri-apps/cli-linux-arm64-musl" "2.0.0-beta.22"
"@tauri-apps/cli-linux-x64-gnu" "2.0.0-beta.22"
"@tauri-apps/cli-linux-x64-musl" "2.0.0-beta.22"
"@tauri-apps/cli-win32-arm64-msvc" "2.0.0-beta.22"
"@tauri-apps/cli-win32-ia32-msvc" "2.0.0-beta.22"
"@tauri-apps/cli-win32-x64-msvc" "2.0.0-beta.22"
"@tauri-apps/plugin-dialog@^2.0.0-beta.6":
version "2.0.0-beta.6"