diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d591c0e68..0ad4388f13 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -256,6 +256,15 @@ jobs: ;; esac + # sccache on PRs: per-TU content-hash cache via GHA backend. Master clean. + - if: github.event_name == 'pull_request' + uses: mozilla-actions/sccache-action@v0.0.10 + - if: github.event_name == 'pull_request' + run: | + echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV + echo "CMAKE_C_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV + echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV + - name: "Build: Daslang" run: | set -eux diff --git a/.github/workflows/extended_checks.yml b/.github/workflows/extended_checks.yml index d6e8d4cc4d..da81a5a5f1 100644 --- a/.github/workflows/extended_checks.yml +++ b/.github/workflows/extended_checks.yml @@ -124,6 +124,15 @@ jobs: ./vcpkg/vcpkg install openssl:x64-windows --binarycaching echo "VCPKG_ROOT=$(pwd)/vcpkg" >> $GITHUB_ENV + # sccache on PRs: per-TU content-hash cache via GHA backend. Master clean. + - if: github.event_name == 'pull_request' + uses: mozilla-actions/sccache-action@v0.0.10 + - if: github.event_name == 'pull_request' + run: | + echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV + echo "CMAKE_C_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV + echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV + - name: "Build: Daslang (Release)" run: | set -eux diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 0e61cd85ca..2700e6e3b9 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -73,23 +73,22 @@ jobs: working_directory: build/latex continue_on_error: true + - name: Setup Emscripten + uses: emscripten-core/setup-emsdk@v11 + with: + # Pinned: 5.0.7's clang crashes on utils/dasFormatter/ds_parser.cpp + # in diagnostic snippet rendering. 5.0.3 known-good. + version: '5.0.3' + - name: "Build WASM (Emscripten) for /playground/ + /files/wasm/" run: | set -eux cd web - # step0 only clones the emsdk wrapper — the toolchain itself - # (emsdk/upstream/emscripten/) lands via the install+activate pair - # that web/step1_emsdk_activate_linux.sh does locally. Without these, - # cmake's -DCMAKE_TOOLCHAIN_FILE points at a path that doesn't - # exist yet and the WASM build silently fails under continue-on-error. - bash step0_emsdk_install.sh - ./emsdk/emsdk install latest - ./emsdk/emsdk activate latest - bash -c "source emsdk/emsdk_env.sh && \ - mkdir -p build && cd build && \ - cmake -DCMAKE_BUILD_TYPE=Release -G Ninja \ - -DCMAKE_TOOLCHAIN_FILE=../emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake ../ && \ - ninja" + mkdir -p build && cd build + emcmake cmake -DCMAKE_BUILD_TYPE=Release -G Ninja .. + # daslang_static.wasm playground + stage_site_playground (vendored UI + # + js/wasm) + stage_site_playground_wasm (cross-compiled examples). + ninja daslang_static stage_site_playground stage_site_playground_wasm continue-on-error: true - name: "Fetch dasProfile benchmark JSON" diff --git a/.github/workflows/playground-e2e.yml b/.github/workflows/playground-e2e.yml index a7e65b4667..4af998397c 100644 --- a/.github/workflows/playground-e2e.yml +++ b/.github/workflows/playground-e2e.yml @@ -8,13 +8,13 @@ on: pull_request: paths: - 'site/**' - - 'web/ui/**' + - 'web/examples/ui/**' - '.github/workflows/playground-e2e.yml' push: branches-ignore: [master] paths: - 'site/**' - - 'web/ui/**' + - 'web/examples/ui/**' - '.github/workflows/playground-e2e.yml' workflow_dispatch: @@ -48,9 +48,9 @@ jobs: cp site/downloads.html _site/ || true cp -r site/files _site/files - # Playground: vendor web/ui IDE (no WASM artifacts) - cp -r web/ui/src/* _site/playground/ - cp -r web/ui/samples _site/playground/samples 2>/dev/null || true + # Playground: vendor web/examples/ui IDE (no WASM artifacts) + cp -r web/examples/ui/src/* _site/playground/ + cp -r web/examples/ui/samples _site/playground/samples 2>/dev/null || true cp site/playground/index.html _site/playground/index.html cp site/playground/forge-skin.css _site/playground/ # Forge playground scripts (init shim + tabs + share + splitter). diff --git a/.github/workflows/wasm_build.yml b/.github/workflows/wasm_build.yml index 8c641516b1..be73df76d9 100644 --- a/.github/workflows/wasm_build.yml +++ b/.github/workflows/wasm_build.yml @@ -108,6 +108,17 @@ jobs: ./emsdk/emsdk_env.bat ;; *) + # macOS stock bison is 2.3; install Homebrew bison 3.x and shadow + # the system one on PATH so any FIND_FLEX_AND_BISON pickup is sane. + if [[ "$(uname -m)" == "arm64" ]]; then + brew install bison + echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.bash_profile + export PATH="/opt/homebrew/opt/bison/bin:$PATH" + else + brew install bison + echo 'export PATH="/usr/local/opt/bison/bin:$PATH"' >> ~/.bash_profile + export PATH="/usr/local/opt/bison/bin:$PATH" + fi ./emsdk/emsdk install latest ./emsdk/emsdk activate latest source "/Users/runner/work/daScript/daScript/emsdk/emsdk_env.sh" @@ -117,7 +128,6 @@ jobs: run: | set -eux cd web - cp ../CMakeXxdImpl.txt . rm -r -f cmake_temp mkdir cmake_temp cd cmake_temp @@ -126,5 +136,58 @@ jobs: - name: "Test: hello world via Node.js" run: | + set -eux + # Use emsdk-bundled Node (deterministic version that supports the + # --experimental-wasm-exnref flag). System Node may be older or use + # a different spelling for the EH proposal flag. + source ./emsdk/emsdk_env.sh + cd web + # Modern wasm EH proposal (try_table / exnref) is gated in Node 22. + # Default-on in Node 24+. Force-enable for forward compat with current LTS. + "$EMSDK_NODE" --experimental-wasm-exnref test/dastest_wasm.js ../ ./output + + ########################################################### + wasm_cross: + ########################################################### + # Cross-compile utility mains to wasm32 via dasLLVM and execute under + # wasmtime. Smoke-tests the runtime-linked cross-compile pipeline + # end-to-end (see modules/dasLLVM/README.md "Cross-compilation"). + needs: pre_job + if: needs.pre_job.outputs.should_skip != 'true' + runs-on: ubuntu-latest-fat + steps: + - name: "SCM Checkout" + uses: actions/checkout@v4 + + - uses: lukka/get-cmake@latest + + - name: Setup Emscripten + uses: emscripten-core/setup-emsdk@v11 + with: + # 5.0.7 ships clang commit 7b58716d that crashes in + # TextDiagnostic::emitSnippetAndCaret on utils/dasFormatter/ds_parser.cpp + # (std::length_error during diagnostic snippet rendering). 5.0.3 works. + version: '5.0.3' + + - name: Setup Wasmtime + uses: bytecodealliance/actions/wasmtime/setup@v1 + with: + version: "44.0.1" + + - name: "Install: graphics dev libs" + run: | + set -eux + sudo apt-get update -y + # OpenGL + GLFW headers needed by dasGlfw (enabled by default in host + # cmake). Same set as extended_checks.yml. + sudo apt-get install --no-install-recommends -y \ + mesa-common-dev libglu1-mesa-dev libgl1-mesa-dev libglfw3-dev \ + libx11-dev libxrandr-dev libxcursor-dev libxinerama-dev libxi-dev + + - name: "Build + run cross-compiled wasm examples" + run: | + set -eux cd web - node test/dastest_wasm.js ../ ./output + mkdir -p cmake_temp && cd cmake_temp + emcmake cmake -DCMAKE_BUILD_TYPE=Release -G Ninja .. + ninja run_wasm_examples diff --git a/CLAUDE.md b/CLAUDE.md index abb1390b67..a71f48851b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -263,6 +263,7 @@ Full migration table (when reading older docs that say `var inscope` or `<-` for | `unsafe(x + y)` / `unsafe { let d = x + y }` where nothing inside requires unsafe | drop the wrap | STYLE024: redundant `unsafe` — flagged when no descendant matches a known inherently-unsafe shape (reinterpret/upcast cast, `delete`, `addr`, table-index, variant-write, ExprCallFunc with `unsafeOperation`). Macro-generated subtrees (`genFlags.generated == true`) skipped per design | | `unsafe { stmt1; stmt2; stmt_needing_unsafe }` (only ONE stmt actually needs unsafe) | `stmt1; stmt2; unsafe()` | STYLE025: narrow block-form unsafe to expression-form on the single unsafe-needing statement. Silent when ≥2 statements need unsafe (block is justified) | | `unsafe { ...; unsafe { ... }; ... }` (nested `unsafe { }` block) | drop the inner wrap | STYLE026: outer `unsafe` already covers the whole inner scope, so the inner block is pure noise. Closure / lambda / generator bodies are NOT nested for this rule — they execute in a separate context where the outer wrap does not propagate | +| `for (s in A) { B \|> push(s) }` / `push_clone(s)` (iter-var only) | `B \|> push_from(A)` / `push_clone_from(A)` | PERF022: the bulk overload in builtin.das reserves combined capacity up front. Single name `push`/`push_clone` is overloaded between single-element and bulk (ambiguous when destination is `array`); the `_from` suffix names the bulk intent. Source must be `array` or C-array — range/iterator sources are not flagged. `emplace` is out of scope (const iter-var can't be moved) | For path/filename ops use `fio` helpers (`base_name`/`dir_name`/`path_join`/etc.) — see `skills/filesystem.md`. Never hand-roll `rfind("/")` / slice — misses Windows separators. diff --git a/benchmarks/sql/M4_DECS_EXPANSION.md b/benchmarks/sql/M4_DECS_EXPANSION.md index 74cb25f861..f38e485295 100644 --- a/benchmarks/sql/M4_DECS_EXPANSION.md +++ b/benchmarks/sql/M4_DECS_EXPANSION.md @@ -337,3 +337,21 @@ Two new planners `plan_decs_order_family` (mirrors `plan_order_family`) and `pla All 6 rows improve 1.5-2.7× (avg ~2.1×). m4 lands within 2× of m3f on most rows — the remaining gap is the Wave 4 multi-component `get_ro` overhead (sort dominates the wall-clock so the gap doesn't fully close even when the splice fires). `bare_order_where` is the tightest squeeze (1.5×) because sort over 100K rows is the bottleneck; once the m4 buffer build matches m3f (~117 ns), the rest is pure sort time. **Coverage:** bare `_order_by` + to_array, bare `_order_by_descending` + to_array, `_order_by` + take, `_order_by_descending` + take, `_order_by` + first, `_order_by_descending` + first, `_order_by` + first_or_default (nonempty + empty), `_where + _order_by + take`, `_where + _order_by + first`; `.reverse() + to_array`, `.reverse() + take(N)` (in-range, beyond-length, and zero — last uses runtime-var to defeat const-fold of PERF017 false-positive on splice-emitted `0 < length(buf)`), `.reverse() + count` (no-buffer counter loop), `_where + .reverse() + first_or_default` (empty + nonempty), `_where + .reverse() + to_array`, `_where + _select + .reverse() + to_array` (projection through reverse). AST shape gates for: order+take (top_n_by emit + no to_sequence + 1 for_each_archetype), order+first (min_by emit + panic-on-empty guard + no top_n_by), reverse+to_array (1 reverse_inplace), reverse+count (no decs_buf, no reverse_inplace). +20 tests (100 → 120 in file). + +## Update — Slice 5c distinct/distinct_by on decs (2026-05-21, plan_decs_distinct) + +New `plan_decs_distinct` planner mirrors `plan_distinct` — streaming dedup via `var inscope seen : table` hoisted above `for_each_archetype` so the seen-table persists across archetypes. Inserted in the cascade BEFORE `plan_distinct` (same rule as 5d/5e — decs bridge match is stricter). Two emit shapes: + +- **Buffer-required** (`to_array` default, optionally `take(N)`): per-element splice computes the key, `key_exists` check, on miss does `taken++ → seen|>insert → buf|>push_clone`. When `take(N)` is present, outer iteration switches to `for_each_archetype_find` and the take-cap `return true` propagates across archetypes — true streaming early-exit. Take-limit bound to a `let` at outer scope so a side-effecting `take(arg)` evaluates exactly once. +- **Buffer-elided** (`count`/`long_count` → `length(seen)`/`int64(length(seen))`; `sum` folds inline at fresh-key site via `acc += `): no buffer allocation, only the seen-table is materialized. count + take both present cascades to tier 2 (matches array-side). + +Side-effecting `_select(proj)` upstream binds once per element to a fresh `decs_pv` local — key + buf push (or sum fold) share the bind, matching array-side single-eval per source element. `distinct_by(key)` wraps the key block in `invoke(, )` and the seen-table's value type is derived via `_::unique_key(invoke(, default))` so the table key type tracks the key function's return type. + +| benchmark | shape | m1 sql | m3 | m3f (array splice) | m4 (eager bridge, was) | m4 (Slice 5c, now) | m4 win | +|---|---|---:|---:|---:|---:|---:|---:| +| distinct_count | `_select(_.brand).distinct().to_array()` | 41 | 44 | 15 | 97 | **28** | 3.5× | +| distinct_take | `_select(_.brand).distinct().take(3).to_array()` | 0 | 30 | 0 | 34 | **0** | matches m3f | + +`distinct_count` lands at 28 ns/op vs m3f's 15 — the ~13 ns gap is the Wave 4 multi-component `get_ro` floor (3 components participate in the inner for-loop even when chain only reads `brand`). `distinct_take` collapses to 0 ns/op — early-exit at the 3rd distinct brand visits only ~3 source elements regardless of N=100K, same as the array-side splice. + +**Coverage:** `_select(_.brand).distinct()` + to_array / count / long_count / sum / take(N) / take(0) / take(N>num_distinct), `_where(_.id<8)._select(_.brand).distinct()`, `_distinct_by(_.brand)` + to_array / count / take(N), empty-decs distinct yields empty, side-effecting take(N) arg evaluates exactly once at invoke entry. AST shape gates for: distinct+count (no to_sequence, no decs_buf, single key_exists, plain for_each_archetype), distinct+take (for_each_archetype_find + decs_buf + decs_seen + decs_taken counters), distinct_by+to_array (unique_key wrapping the key invocation), distinct+sum (no decs_buf, decs_acc declared+folded). +17 tests (122 → 139 in file). diff --git a/daslib/algorithm.das b/daslib/algorithm.das index f632f1b381..8c9f162b2d 100644 --- a/daslib/algorithm.das +++ b/daslib/algorithm.das @@ -54,13 +54,9 @@ def reverse(var a : array) { def combine(a, b : array) { //! Returns a new array containing elements from a followed by b. var c : array - reserve(c, length(a) + length(b)) - for (t in a) { - c |> push_clone(t) - } - for (t in b) { - c |> push_clone(t) - } + c |> reserve(long_length(a) + long_length(b)) + c |> push_clone_from(a) + c |> push_clone_from(b) return <- c } diff --git a/daslib/builtin.das b/daslib/builtin.das index fcc5c03c27..1d8d8ec4d4 100644 --- a/daslib/builtin.das +++ b/daslib/builtin.das @@ -193,6 +193,7 @@ def push(var Arr : array; var value : numT -# ==const) { def push(var Arr : array; varr : array ==const) { static_if (typeinfo can_copy(type)) { static_if (typeinfo can_clone_from_const(varr[0])) { + Arr |> reserve(long_length(Arr) + long_length(varr)) for (t in varr) { Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] = t } @@ -207,6 +208,7 @@ def push(var Arr : array; varr : array ==const) { [unused_argument(Arr, value)] def push(var Arr : array; var varr : array ==const) { static_if (typeinfo can_copy(type)) { + Arr |> reserve(long_length(Arr) + long_length(varr)) for (t in varr) { Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] = t } @@ -218,6 +220,7 @@ def push(var Arr : array; var varr : array ==const) { [unused_argument(Arr, value)] def push(var Arr : array; varr : numT[] -#) { static_if (typeinfo can_copy(type)) { + Arr |> reserve(long_length(Arr) + int64(length(varr))) for (t in varr) { Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] = t } @@ -287,6 +290,7 @@ def emplace(var Arr : array; var value : numT -#&) { [unused_argument(Arr, value)] def emplace(var Arr : array; var value : numT[] -#) { static_if (typeinfo can_move(value)) { + Arr |> reserve(long_length(Arr) + int64(length(value))) for (t in value) { unsafe { Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] <- t @@ -368,8 +372,9 @@ def push_clone(var Arr : array; var value : numT ==const | #) { def push_clone(var Arr : array; varr : numT[] ==const) { static_if (typeinfo can_clone(type)) { static_if (typeinfo can_clone_from_const(varr[0])) { + Arr |> reserve(long_length(Arr) + int64(length(varr))) for (t in varr) { - Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] := t + Arr[__builtin_array_push_back_zero(Arr, typeinfo sizeof(Arr[0]))] := t } } else { concept_assert(false, "can't push-clone value, which can't be cloned from const") @@ -382,8 +387,9 @@ def push_clone(var Arr : array; varr : numT[] ==const) { [unused_argument(Arr, value)] def push_clone(var Arr : array; var varr : numT[] ==const) { static_if (typeinfo can_clone(type)) { + Arr |> reserve(long_length(Arr) + int64(length(varr))) for (t in varr) { - Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] := t + Arr[__builtin_array_push_back_zero(Arr, typeinfo sizeof(Arr[0]))] := t } } else { concept_assert(false, "can't push_clone value, which can't be cloned") @@ -396,7 +402,7 @@ def push_clone(var Arr : array; varr : numT[] ==const) { static_if (typeinfo sizeof(Arr[0]) == typeinfo sizeof(varr)) { if (typeinfo can_clone_from_const(varr)) { for (t in varr) { - Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] := t + Arr[__builtin_array_push_back_zero(Arr, typeinfo sizeof(Arr[0]))] := t } } else { concept_assert(false, "can't push-clone value, which can't be cloned from const") @@ -413,7 +419,7 @@ def push_clone(var Arr : array; varr : numT[] ==const) { def push_clone(var Arr : array; var varr : numT[]) { static_if (typeinfo can_copy(type)) { static_if (typeinfo sizeof(Arr[0]) == typeinfo sizeof(varr)) { - Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] := varr + Arr[__builtin_array_push_back_zero(Arr, typeinfo sizeof(Arr[0]))] := varr } else { concept_assert(false, "can't push_clone array of different size") } @@ -436,6 +442,148 @@ def push_clone(var A : auto(CT) -# -const; b : auto(TT) | #) { } } +//! Append every element of source array `varr` to `Arr` via copy. +//! Reserves `length(Arr) + length(varr)` capacity up front, so a bulk +//! append touches the allocator once instead of N times. +[unused_argument(Arr, varr)] +def push_from(var Arr : array; varr : array ==const) { + static_if (typeinfo can_copy(type)) { + static_if (typeinfo can_clone_from_const(varr[0])) { + Arr |> reserve(long_length(Arr) + long_length(varr)) + for (t in varr) { + Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] = t + } + } else { + concept_assert(false, "can't push value, which can't be cloned from const") + } + } else { + concept_assert(false, "can't push value, which can't be copied") + } +} + +//! Append every element of source array `varr` to `Arr` via copy +//! (mutable source overload — still copies, does not consume). +[unused_argument(Arr, varr)] +def push_from(var Arr : array; var varr : array ==const) { + static_if (typeinfo can_copy(type)) { + Arr |> reserve(long_length(Arr) + long_length(varr)) + for (t in varr) { + Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] = t + } + } else { + concept_assert(false, "can't push value, which can't be copied") + } +} + +//! Append every element of fixed-size C-array `varr` to `Arr` via copy. +//! Reserves the combined length up front. +[unused_argument(Arr, varr)] +def push_from(var Arr : array; varr : numT[] -#) { + static_if (typeinfo can_copy(type)) { + Arr |> reserve(long_length(Arr) + int64(length(varr))) + for (t in varr) { + Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] = t + } + } else { + concept_assert(false, "can't push value, which can't be copied") + } +} + +//! Append every element of source array `varr` to `Arr` via clone (`:=`). +//! Reserves the combined length up front. Each slot is zero-initialized +//! before clone (matches single-element ``push_clone`` semantics). +[unused_argument(Arr, varr)] +def push_clone_from(var Arr : array; varr : array ==const) { + static_if (typeinfo can_clone(type)) { + static_if (typeinfo can_clone_from_const(varr[0])) { + Arr |> reserve(long_length(Arr) + long_length(varr)) + for (t in varr) { + Arr[__builtin_array_push_back_zero(Arr, typeinfo sizeof(Arr[0]))] := t + } + } else { + concept_assert(false, "can't push-clone value, which can't be cloned from const") + } + } else { + concept_assert(false, "can't push_clone value, which can't be cloned") + } +} + +//! Append every element of source array `varr` to `Arr` via clone (`:=`), +//! mutable source overload. +[unused_argument(Arr, varr)] +def push_clone_from(var Arr : array; var varr : array ==const) { + static_if (typeinfo can_clone(type)) { + Arr |> reserve(long_length(Arr) + long_length(varr)) + for (t in varr) { + Arr[__builtin_array_push_back_zero(Arr, typeinfo sizeof(Arr[0]))] := t + } + } else { + concept_assert(false, "can't push_clone value, which can't be cloned") + } +} + +//! Append every element of fixed-size C-array `varr` to `Arr` via clone. +[unused_argument(Arr, varr)] +def push_clone_from(var Arr : array; varr : numT[] ==const) { + static_if (typeinfo can_clone(type)) { + static_if (typeinfo can_clone_from_const(varr[0])) { + Arr |> reserve(long_length(Arr) + int64(length(varr))) + for (t in varr) { + Arr[__builtin_array_push_back_zero(Arr, typeinfo sizeof(Arr[0]))] := t + } + } else { + concept_assert(false, "can't push-clone value, which can't be cloned from const") + } + } else { + concept_assert(false, "can't push_clone value, which can't be cloned") + } +} + +//! Append every element of fixed-size C-array `varr` to `Arr` via clone, +//! mutable source overload. +[unused_argument(Arr, varr)] +def push_clone_from(var Arr : array; var varr : numT[] ==const) { + static_if (typeinfo can_clone(type)) { + Arr |> reserve(long_length(Arr) + int64(length(varr))) + for (t in varr) { + Arr[__builtin_array_push_back_zero(Arr, typeinfo sizeof(Arr[0]))] := t + } + } else { + concept_assert(false, "can't push_clone value, which can't be cloned") + } +} + +//! Move every element of source array `varr` into `Arr` (`<-`). +//! Source is consumed. +[unused_argument(Arr, varr)] +def emplace_from(var Arr : array; var varr : array ==const) { + static_if (typeinfo can_move(type)) { + Arr |> reserve(long_length(Arr) + long_length(varr)) + for (t in varr) { + unsafe { + Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] <- t + } + } + } else { + concept_assert(false, "can't emplace value, which can't be moved") + } +} + +//! Move every element of fixed-size C-array `varr` into `Arr`. +[unused_argument(Arr, varr)] +def emplace_from(var Arr : array; var varr : numT[] -#) { + static_if (typeinfo can_move(type)) { + Arr |> reserve(long_length(Arr) + int64(length(varr))) + for (t in varr) { + unsafe { + Arr[__builtin_array_push_back(Arr, typeinfo sizeof(Arr[0]))] <- t + } + } + } else { + concept_assert(false, "can't emplace value, which can't be moved") + } +} + def back(var a : array -const ==const) : TT& { let l = length(a) if (l == 0) { diff --git a/daslib/daspkg.das b/daslib/daspkg.das index 47140d470a..cc5a91e1ee 100644 --- a/daslib/daspkg.das +++ b/daslib/daspkg.das @@ -55,10 +55,7 @@ def package_tag(tag : string) { } def package_tags(tags : array) { - _pkg_meta.tags |> reserve(length(_pkg_meta.tags) + length(tags)) - for (t in tags) { - _pkg_meta.tags |> push_clone(t) - } + _pkg_meta.tags |> push_clone_from(tags) } def package_min_sdk(version : string) { diff --git a/daslib/fio.das b/daslib/fio.das index 1b76a06055..1efd3acbd5 100644 --- a/daslib/fio.das +++ b/daslib/fio.das @@ -414,10 +414,7 @@ def expand_glob(pattern : string; var result : array) { } } sort(matches) - result |> reserve(length(result) + length(matches)) - for (m in matches) { - result |> push(m) - } + result |> push_from(matches) } // Join `dir_part` and `fname` with `/`, but skip the inserted separator when `dir_part` diff --git a/daslib/linq.das b/daslib/linq.das index e47989b446..bb7a432702 100644 --- a/daslib/linq.das +++ b/daslib/linq.das @@ -467,10 +467,7 @@ def top_n_by(arr : array; n : int; key) : array { var buf : array if (n <= 0 || empty(arr)) return <- buf let take_count = min(n, length(arr)) - buf |> reserve(length(arr)) - for (it in arr) { - buf |> push_clone(it) - } + buf |> push_clone_from(arr) sort_boost::partial_sort(buf, take_count, $(v1, v2) => _::less(key(v1), key(v2))) buf |> resize(take_count) return <- buf @@ -524,10 +521,7 @@ def top_n_by_descending(arr : array; n : int; key) : array if (n <= 0 || empty(arr)) return <- buf let take_count = min(n, length(arr)) - buf |> reserve(length(arr)) - for (it in arr) { - buf |> push_clone(it) - } + buf |> push_clone_from(arr) sort_boost::partial_sort(buf, take_count, $(v1, v2) => _::less(key(v2), key(v1))) buf |> resize(take_count) return <- buf @@ -583,10 +577,7 @@ def top_n_by_with_cmp(arr : array; n : int; cmp : block<(v1 : TT -&, v var buf : array if (n <= 0 || empty(arr)) return <- buf let take_count = min(n, length(arr)) - buf |> reserve(length(arr)) - for (it in arr) { - buf |> push_clone(it) - } + buf |> push_clone_from(arr) sort_boost::partial_sort(buf, take_count, cmp) buf |> resize(take_count) return <- buf diff --git a/daslib/linq_fold.das b/daslib/linq_fold.das index c12ee7edf7..a849827b39 100644 --- a/daslib/linq_fold.das +++ b/daslib/linq_fold.das @@ -487,13 +487,9 @@ def private prepend_binds(var stmts : array; var intermediateBinds // Splice the chain's let-bindings (one per upstream `select`) ahead of the per-element if (empty(intermediateBinds)) return var prefixed : array - prefixed |> reserve(length(intermediateBinds) + length(stmts)) - for (b in intermediateBinds) { - prefixed |> push(b) - } - for (s in stmts) { - prefixed |> push(s) - } + prefixed |> reserve(long_length(intermediateBinds) + long_length(stmts)) + prefixed |> push_from(intermediateBinds) + prefixed |> push_from(stmts) swap(stmts, prefixed) } @@ -522,10 +518,8 @@ def private prepend_precond(var body : Expression?; var preCondStmts : array - stmts |> reserve(length(preCondStmts) + 1) - for (s in preCondStmts) { - stmts |> push(s) - } + stmts |> reserve(long_length(preCondStmts) + 1_l) + stmts |> push_from(preCondStmts) stmts |> push(body) return stmts_to_expr(stmts) } @@ -600,9 +594,7 @@ def private wrap_with_ranges(var stmts : array; var skipExpr, takeE $i(takeCountName) ++ } } - for (s in stmts) { - prefixed |> push(s) - } + prefixed |> push_from(stmts) swap(stmts, prefixed) } @@ -809,9 +801,7 @@ def private emit_accumulator_lane( var bodyStmts : array bodyStmts |> reserve(length(preludeStmts) + 4) append_ranges_prelude(bodyStmts, skipExpr, takeExpr, skipWhileCond, skipName, takeCountName, skippingName) - for (s in preludeStmts) { - bodyStmts |> push(s) - } + bodyStmts |> push_from(preludeStmts) // For-loop emission: single-source uses one iter var (itName); 2-source (zip) uses literal `itA, itB` parallel iter vars (qmacro for-loop iter-var position doesn't accept $i(...) splice). Caller (plan_zip) threads `let it = (itA, itB)` via preCondStmts so itName resolves inside the loop body. if (length(srcNames) == 1) { bodyStmts |> push <| qmacro_expr() { @@ -1169,9 +1159,7 @@ def private emit_early_exit_lane( var bodyStmts : array bodyStmts |> reserve(length(preludeStmts) + length(tailStmts) + 3) append_ranges_prelude(bodyStmts, skipExpr, takeExpr, skipWhileCond, skipName, takeCountName, skippingName) - for (s in preludeStmts) { - bodyStmts |> push(s) - } + bodyStmts |> push_from(preludeStmts) // For-loop emission: 1-source uses itName; 2-source (zip) uses literal `itA, itB` (qmacro for-iter-var position doesn't accept $i splice). Caller (plan_zip) threads `let it = (itA, itB)` via preCondStmts. if (length(srcNames) == 1) { bodyStmts |> push <| qmacro_expr() { @@ -1186,9 +1174,7 @@ def private emit_early_exit_lane( } } } - for (s in tailStmts) { - bodyStmts |> push(s) - } + bodyStmts |> push_from(tailStmts) return finalize_lane_emission(topExprs, srcNames, bodyStmts, at) } @@ -1915,7 +1901,9 @@ def private plan_distinct(var expr : Expression?) : Expression? { var terminatorName : string = "" { let lastName = calls.back()._1.name - if (lastName == "count" || lastName == "sum" || lastName == "long_count") { + // 1-arg form only — `count(src, predicate)` / `long_count(src, predicate)` would silently drop the predicate (splice emits `length(seen)`). Bail to tier-2 which correctly evaluates the predicate. + if ((lastName == "count" || lastName == "sum" || lastName == "long_count") + && length(calls.back()._0.arguments) == 1) { terminatorName = lastName calls |> pop } @@ -3607,12 +3595,8 @@ def private emit_decs_accumulator(bridge : DecsBridgeShape?; var rangeStmts <- decs_range_prelude(rangeInfo, skipName, takeCountName, takeLimitName, skippingName) var bodyStmts : array bodyStmts |> reserve(length(preludeStmts) + length(rangeStmts) + length(tailStmts) + 1) - for (s in preludeStmts) { - bodyStmts |> push(s) - } - for (s in rangeStmts) { - bodyStmts |> push(s) - } + bodyStmts |> push_from(preludeStmts) + bodyStmts |> push_from(rangeStmts) if (rangeInfo.takeExpr != null || rangeInfo.takeWhileCond != null) { // Slice 5b: take_while also produces inner `return true` on its first-false (terminates iteration globally), so it needs the bool-lambda archetype-find dispatch too. bodyStmts |> push <| qmacro_expr() { @@ -3628,9 +3612,7 @@ def private emit_decs_accumulator(bridge : DecsBridgeShape?; }) } } - for (s in tailStmts) { - bodyStmts |> push(s) - } + bodyStmts |> push_from(tailStmts) var retType : TypeDeclPtr if (opName == "long_count") { retType = new TypeDecl(baseType = Type.tInt64) @@ -3806,12 +3788,8 @@ def private emit_decs_early_exit(bridge : DecsBridgeShape?; // Combine prelude + invocation + tail into ONE bodyStmts list — multiple $b splices in the same qmacro fragment isolate variable scope. var bodyStmts : array bodyStmts |> reserve(length(preludeStmts) + length(rangeStmts) + length(tailStmts) + 1) - for (s in preludeStmts) { - bodyStmts |> push(s) - } - for (s in rangeStmts) { - bodyStmts |> push(s) - } + bodyStmts |> push_from(preludeStmts) + bodyStmts |> push_from(rangeStmts) // When takeExpr OR takeWhileCond is present, any/all/contains route their answer through explicit foundName state (set above) — the find result is unreliable because the iteration-stop signal and "real match" both produce inner `return true`. In that case, treat the find call as iteration control only and emit the return via tailStmts (which already contains `return foundName` / `return !foundName`). let useExplicitState = ((rangeInfo.takeExpr != null || rangeInfo.takeWhileCond != null) && (opName == "any" || opName == "all" || opName == "contains")) if (useExplicitState) { @@ -3821,9 +3799,7 @@ def private emit_decs_early_exit(bridge : DecsBridgeShape?; return false }) } - for (s in tailStmts) { - bodyStmts |> push(s) - } + bodyStmts |> push_from(tailStmts) } elif (opName == "any" || opName == "contains") { bodyStmts |> push <| qmacro_expr() { return for_each_archetype_find($e(reqExpr), $e(erqExpr), $($i(archName) : Archetype) : bool { @@ -3846,9 +3822,7 @@ def private emit_decs_early_exit(bridge : DecsBridgeShape?; return false }) } - for (s in tailStmts) { - bodyStmts |> push(s) - } + bodyStmts |> push_from(tailStmts) } var emission : Expression? if (opName == "first" || opName == "first_or_default") { @@ -3899,9 +3873,7 @@ def private emit_decs_to_array(bridge : DecsBridgeShape?; bodyStmts |> push <| qmacro_expr() { var $i(bufName) : array<$t(elemType)> } - for (s in rangeStmts) { - bodyStmts |> push(s) - } + bodyStmts |> push_from(rangeStmts) if (rangeInfo.takeExpr != null || rangeInfo.takeWhileCond != null) { // Slice 5b: take_while also produces inner `return true` on its first-false (terminates iteration globally), so it needs the bool-lambda archetype-find dispatch too. bodyStmts |> push <| qmacro_expr() { @@ -3989,9 +3961,7 @@ def private emit_decs_min_max_by(bridge : DecsBridgeShape?; bodyStmts |> push <| qmacro_expr() { var $i(bestElemName) : $t(elemType) } - for (s in rangeStmts) { - bodyStmts |> push(s) - } + bodyStmts |> push_from(rangeStmts) if (rangeInfo.takeExpr != null || rangeInfo.takeWhileCond != null) { // Slice 5b: take_while also produces inner `return true` on its first-false (terminates iteration globally), so it needs the bool-lambda archetype-find dispatch too. bodyStmts |> push <| qmacro_expr() { @@ -4483,6 +4453,244 @@ def private plan_decs_reverse(var expr : Expression?) : Expression? { return emission } +// ── decs distinct splice (Slice 5c — streaming dedup table hoisted above for_each_archetype) ─────── + +[macro_function] +def private plan_decs_distinct(var expr : Expression?) : Expression? { + // Decs-bridge entry mirroring plan_distinct. Recognizes [where_*][select?] |> distinct[_by(key)] [|> take(N)]? [|> {count, long_count, sum, to_array}]?. Two emit shapes: buffer-required (to_array, optionally with take(N)) and buffer-free (count/long_count return length(seen); sum folds inline at fresh-key site). Take(N) flips outer iteration to for_each_archetype_find so the take-cap can stop across archetypes; counter / seen-table hoist above for_each_archetype so state persists. + var (top, calls) = flatten_linq(expr) + var bridge = extract_decs_bridge(top) + if (bridge == null || empty(calls) || expr._type == null) return null + var terminatorName : string = "" + { + let lastName = calls.back()._1.name + if ((lastName == "count" || lastName == "long_count" || lastName == "sum") + && length(calls.back()._0.arguments) == 1) { + // 1-arg form only — `count(src, predicate)` / `long_count(src, predicate)` would silently drop the predicate (sum has no selector overload today, but kept under the same guard for symmetry with plan_distinct). Bail to tier-2 which correctly evaluates the predicate. + terminatorName = lastName + calls |> pop + } + } + if (empty(calls)) return null + let at = calls[0]._0.at + let tupName = "`decs_tup`{at.line}`{at.column}" + let bufName = "`decs_buf`{at.line}`{at.column}" + let seenName = "`decs_seen`{at.line}`{at.column}" + let keyName = "`decs_k`{at.line}`{at.column}" + let takenName = "`decs_taken`{at.line}`{at.column}" + let takeLimName = "`decs_takeLim`{at.line}`{at.column}" + let accName = "`decs_acc`{at.line}`{at.column}" + let pvName = "`decs_pv`{at.line}`{at.column}" + var whereCond : Expression? + var projection : Expression? + var hasDistinct = false + var seenSelect = false + var isDistinctBy = false + var distinctKeyBlock : Expression? + var takeExpr : Expression? + for (i in 0 .. length(calls)) { + var cll & = unsafe(calls[i]) + let name = cll._1.name + if (name == "where_") { + if (hasDistinct || seenSelect) return null + var pred = fold_linq_cond(cll._0.arguments[1], tupName) + if (pred == null) return null + whereCond = merge_where_cond(whereCond, pred) + } elif (name == "select") { + if (hasDistinct || seenSelect) return null + seenSelect = true + projection = fold_linq_cond(cll._0.arguments[1], tupName) + if (projection == null) return null + } elif (name == "distinct") { + if (hasDistinct) return null + hasDistinct = true + } elif (name == "distinct_by") { + if (hasDistinct) return null + hasDistinct = true + isDistinctBy = true + distinctKeyBlock = clone_expression(cll._0.arguments[1]) + } elif (name == "take") { + if (!hasDistinct || takeExpr != null) return null + var arg = cll._0.arguments[1] + if (arg == null || arg._type == null || arg._type.baseType != Type.tInt) return null + takeExpr = clone_expression(arg) + } else { + return null + } + } + // take(N) only composes with the implicit-to_array terminator. count/sum + take would defeat the streaming early-exit (matches array-side bail at plan_distinct). + if (!hasDistinct || (takeExpr != null && terminatorName != "")) return null + var elemType = strip_const_ref(clone_type(projection != null ? projection._type : bridge.elementType)) + let isCountTerminator = terminatorName == "count" || terminatorName == "long_count" + let isSumTerminator = terminatorName == "sum" + let needBuffer = !isCountTerminator && !isSumTerminator + // Prelude: seen-table + (buffer | accumulator | take counter). All hoisted at invoke scope so state spans archetypes. + var preludeStmts : array + if (isDistinctBy) { + var keyTypeBlock = clone_expression(distinctKeyBlock) + preludeStmts |> push <| qmacro_expr() { + var inscope $i(seenName) : table)))> + } + } else { + preludeStmts |> push <| qmacro_expr() { + var inscope $i(seenName) : table))> + } + } + if (needBuffer) { + preludeStmts |> push <| qmacro_expr() { + var $i(bufName) : array<$t(elemType)> + } + } elif (isSumTerminator) { + preludeStmts |> push <| qmacro_expr() { + var $i(accName) : $t(elemType) = default<$t(elemType)> + } + } + if (takeExpr != null) { + // Bind take(N) limit once at outer scope so a side-effecting arg fires once (matches plan_distinct). + var takeBind = clone_expression(takeExpr) + preludeStmts |> push <| qmacro_expr() { + let $i(takeLimName) = $e(takeBind) + } + preludeStmts |> push <| qmacro_expr() { + var $i(takenName) = 0 + } + } + // Bind side-effecting projection once per element; key + buffer/acc share the bind (matches array-side single-eval per source elem). + let bindProjection = projection != null && has_sideeffects(projection) + var pushExpr : Expression? + if (projection != null) { + if (bindProjection) { + pushExpr = qmacro_expr() { + $i(pvName) + } + } else { + pushExpr = clone_expression(projection) + } + } else { + pushExpr = qmacro_expr() { + $i(tupName) + } + } + var keyExpr : Expression? + if (isDistinctBy) { + var keyBlk = clone_expression(distinctKeyBlock) + var keyArg = clone_expression(pushExpr) + keyExpr = qmacro_expr() { + invoke($e(keyBlk), $e(keyArg)) + } + } else { + keyExpr = clone_expression(pushExpr) + } + // Per-fresh-key consume: only inside the fresh-key branch. take++ first (matches plan_distinct). + var consumeStmts : array + if (takeExpr != null) { + consumeStmts |> push <| qmacro_expr() { + $i(takenName) ++ + } + } + consumeStmts |> push <| qmacro_expr() { + $i(seenName) |> insert($i(keyName)) + } + if (needBuffer) { + var pushClone = clone_expression(pushExpr) + consumeStmts |> push <| qmacro_expr() { + $i(bufName) |> push_clone($e(pushClone)) + } + } elif (isSumTerminator) { + var sumExpr = clone_expression(pushExpr) + consumeStmts |> push <| qmacro_expr() { + $i(accName) += $e(sumExpr) + } + } + var consumeBody = stmts_to_expr(consumeStmts) + var ifNew : Expression? = qmacro_expr() { + if (!$i(seenName) |> key_exists($i(keyName))) { + $e(consumeBody) + } + } + var perMatchStmts : array // nolint:STYLE012 — array-literal-of-qmacro_expr is parse-fragile + if (bindProjection) { + var pvBind = clone_expression(projection) + perMatchStmts |> push <| qmacro_expr() { + let $i(pvName) = $e(pvBind) + } + } + perMatchStmts |> push <| qmacro_expr() { + let $i(keyName) = _::unique_key($e(keyExpr)) + } + perMatchStmts |> push(ifNew) + var perElement = stmts_to_expr(perMatchStmts) + perElement = wrap_with_condition(perElement, whereCond) + // Top-of-inner-for take-cap guard: `return true` exits both the inner for AND the archetype-find lambda, propagating the stop across archetypes. + if (takeExpr != null) { + perElement = qmacro_expr() { + { + if ($i(takenName) >= $i(takeLimName)) return true + $e(perElement) + } + } + } + var tupBind = build_decs_tup_bind(bridge, tupName, at) + var forExprNode = build_decs_inner_for(bridge, tupBind, perElement, at) + var reqExpr = clone_expression(bridge.reqHashExpr) + var erqExpr = clone_expression(bridge.erqExpr) + let archName = bridge.archName + var bodyStmts : array + bodyStmts |> reserve(length(preludeStmts) + 2) + bodyStmts |> push_from(preludeStmts) + if (takeExpr != null) { + bodyStmts |> push <| qmacro_expr() { + for_each_archetype_find($e(reqExpr), $e(erqExpr), $($i(archName) : Archetype) : bool { + $e(forExprNode) + return false + }) + } + } else { + bodyStmts |> push <| qmacro_expr() { + for_each_archetype($e(reqExpr), $e(erqExpr), $($i(archName) : Archetype) { + $e(forExprNode) + }) + } + } + var emission : Expression? + if (terminatorName == "count") { + bodyStmts |> push <| qmacro_expr() { + return length($i(seenName)) + } + emission = qmacro(invoke($() : int { + $b(bodyStmts) + })) + } elif (terminatorName == "long_count") { + bodyStmts |> push <| qmacro_expr() { + return int64(length($i(seenName))) + } + emission = qmacro(invoke($() : int64 { + $b(bodyStmts) + })) + } elif (isSumTerminator) { + bodyStmts |> push <| qmacro_expr() { + return $i(accName) + } + emission = qmacro(invoke($() : $t(elemType) { + $b(bodyStmts) + })) + } else { + bodyStmts |> push <| qmacro_expr() { + return <- $i(bufName) + } + emission = qmacro(invoke($() : array<$t(elemType)> { + $b(bodyStmts) + })) + } + emission.force_at(at) + emission.force_generated(true) + let needIterWrap = expr._type.isIterator + if (needIterWrap && needBuffer) { + emission = qmacro($e(emission).to_sequence_move()) + } + return emission +} + [macro_function] def private plan_zip(var expr : Expression?) : Expression? { // Phase 2 Z1/Z2/Z3 + accumulator/early-exit: 2-ary lockstep zip splice. Supports bare zip (array/iterator), no-pred count/long_count + accumulator (sum/min/max/average) + early-exit (first/first_or_default/any/all/contains) terminators, and fused where_/select/take/skip/take_while/skip_while chain ops between zip and terminator. Result-selector form (3-arg zip) and chained selects bail to tier-2 cascade. @@ -4811,6 +5019,8 @@ class private LinqFold : AstCallMacro { if (res != null) return res res = plan_reverse(call.arguments[0]) if (res != null) return res + res = plan_decs_distinct(call.arguments[0]) + if (res != null) return res res = plan_distinct(call.arguments[0]) if (res != null) return res res = plan_decs_group_by(call.arguments[0]) diff --git a/daslib/lint.das b/daslib/lint.das index 700474840c..5fc331b6b5 100644 --- a/daslib/lint.das +++ b/daslib/lint.das @@ -452,10 +452,7 @@ def public paranoid_collect(prog : ProgramPtr; var errors : array; visit(prog, astVisitor.astVisitorAdapter) } let count = length(astVisitor.errors) - errors |> reserve(count) - for (e in astVisitor.errors) { - errors |> push(e) - } + errors |> push_from(astVisitor.errors) unsafe { delete astVisitor } diff --git a/daslib/perf_lint.das b/daslib/perf_lint.das index d715259bd6..3c2a50b9e3 100644 --- a/daslib/perf_lint.das +++ b/daslib/perf_lint.das @@ -34,6 +34,7 @@ module perf_lint shared private //! PERF019 — int(T.a) | int(T.b) on bitfield-or-enum-with-operator-| — collapse to int(T.a | T.b) //! PERF020 — T(x) where x is already T (workhorse type) — drop the redundant cast //! PERF021 — cond ? T(a) : T(b) on workhorse cast T — hoist to T(cond ? a : b) +//! PERF022 — for (s in A) { B |> push(s) } / push_clone — use 'B |> push_from(A)' (bulk reserve+copy) require daslib/ast_boost require strings @@ -65,6 +66,25 @@ struct Perf018State { matched : bool } +struct Perf022State { + //! PERF022 per-for-loop candidate. Detects 'for (s in A) { B |> push(s) }' + //! (push and push_clone; all three pipe/dot/plain-call shapes compiler-fold + //! to the same ExprCall). Pushed in preVisitExprFor in lockstep with PERF018. + //! `matched` is stamped by preVisitExprForBody when body is a single + //! ExprCall of the right shape against the frame's iter_var. We warn at the + //! for-loop's `at` (looked up via the parallel `perf022_for_exprs` stack) + //! rather than the inner push's `at` — the inner push location dedups + //! against PERF006 which also fires at that exact line:col. + //! `callee_id`: 1 = push, 2 = push_clone (kept trivial so the struct can live + //! in an array). String name is rebuilt at warn time. + @do_not_delete iter_var : Variable? + callee_id : int + sources_seen : int + vars_seen : int + disqualified : bool + matched : bool +} + class PerfLintVisitor : AstVisitor { compile_time_errors : bool // variable + loop tracking @@ -108,6 +128,11 @@ class PerfLintVisitor : AstVisitor { @do_not_delete perf018_for_exprs : array in_qualifying_idx : int = 0 at_qualifying_pushes : array + // PERF022: per-for-loop bulk-push candidates; same lockstep with for-loop + // nesting. `array` requires T trivially-storable, so the owning ExprFor + // pointer lives in a parallel `perf022_for_exprs` array. + perf022_stack : array + @do_not_delete perf022_for_exprs : array warning_count : int = 0 // collection mode — when true, warnings are appended to `warnings` instead of to_log collect_warnings : bool = false @@ -556,6 +581,7 @@ class PerfLintVisitor : AstVisitor { current_for_known_length = true loop_has_early_exit |> push(false) self->perf018_push_sentinel(expr) + self->perf022_push_sentinel(expr) } } @@ -576,6 +602,7 @@ class PerfLintVisitor : AstVisitor { } if (in_closure == 0) { self->perf018_record_source(src) + self->perf022_record_source(src) } } @@ -585,6 +612,7 @@ class PerfLintVisitor : AstVisitor { } if (in_closure == 0) { self->perf018_finalize_on_body() + self->perf022_inspect_body(expr.body) } } @@ -592,12 +620,14 @@ class PerfLintVisitor : AstVisitor { if (in_closure == 0) { var_stack |> push(VarStackEntry(v = v, depth = loop_depth - 1, is_iter = true)) self->perf018_record_var(v) + self->perf022_record_var(v) } } def override visitExprFor(var expr : ExprFor?) : ExpressionPtr { if (in_closure == 0) { self->perf018_pop_and_check() + self->perf022_pop_and_check() loop_depth-- if (current_for_known_length) { known_length_loop_depth-- @@ -793,6 +823,122 @@ class PerfLintVisitor : AstVisitor { } } + // --- PERF022: for (s in A) { B |> push(s) } — recommend bulk *_from --- + + def perf022_push_sentinel(expr : ExprFor?) : void { + //! Always pushed in preVisitExprFor so the stack stays lock-step with + //! for-loop nesting. preVisitExprForBody may flip `matched`; if a + //! frame survives to perf022_pop_and_check matched-and-undisqualified, + //! we fire at the for-loop's `at` (not the push's, to avoid colliding + //! with PERF006 under the same-location dedup in perf_warning). + var state : Perf022State + perf022_stack |> push(state) + unsafe { + perf022_for_exprs |> push(reinterpret(expr)) + } + } + + def perf022_record_source(src : Expression?) : void { + if (empty(perf022_stack)) return + let top_idx = length(perf022_stack) - 1 + var state = perf022_stack[top_idx] + state.sources_seen++ + if (state.sources_seen > 1) { + state.disqualified = true + } + // PERF022 only suggests push_from / push_clone_from, which take array + // or C-array sources. Disqualify range/string/iterator/generator sources + // — there is no bulk overload to migrate to in those cases. + if (src == null || src._type == null) { + state.disqualified = true + } else { + let bt = src._type.baseType + let is_array_src = bt == Type.tArray || !empty(src._type.dim) + if (!is_array_src) { + state.disqualified = true + } + } + perf022_stack[top_idx] = state + } + + def perf022_record_var(var v : Variable?) : void { + if (empty(perf022_stack) || v == null) return + let top_idx = length(perf022_stack) - 1 + var state = perf022_stack[top_idx] + state.vars_seen++ + if (state.vars_seen > 1) { + state.disqualified = true + perf022_stack[top_idx] = state + return + } + state.iter_var = v + perf022_stack[top_idx] = state + } + + def perf022_inspect_body(body : Expression?) : void { + //! Called from preVisitExprForBody. Flips `matched` iff the body is + //! a single-statement ExprBlock containing exactly one ExprCall to + //! push/push_clone with arg[1] = the frame's iter_var. Compiler folds + //! 'B |> push(s)', 'B.push(s)', and 'push(B, s)' to the same shape, + //! so this single check covers all three call forms. + if (empty(perf022_stack)) return + let top_idx = length(perf022_stack) - 1 + var state = perf022_stack[top_idx] + if (state.disqualified || state.iter_var == null + || state.sources_seen != 1 || state.vars_seen != 1 + || body == null || !(body is ExprBlock)) return + var blk = body as ExprBlock + if (length(blk.list) != 1) return + var stmt = blk.list[0] + if (stmt == null || !(stmt is ExprCall) || (stmt as ExprCall).func == null + || length((stmt as ExprCall).arguments) != 2) return + var call = stmt as ExprCall + // Destination must be `array` — the `*_from` overloads are array-only. + // Generic `auto` parameters (e.g. `[expect_any_vector] def clone(var args; + // ...)` in builtin.das) where the destination accepts dasvector etc. must + // be skipped — no push_from overload exists for them. + let dest = call.arguments[0] + if (dest == null || dest._type == null || dest._type.baseType != Type.tArray) return + var callee_id = 0 + if (call.func.fromGeneric != null) { + if (call.func.fromGeneric.name == "push") { + callee_id = 1 + } elif (call.func.fromGeneric.name == "push_clone") { + callee_id = 2 + } + } else { + if (call.func.name == "push") { + callee_id = 1 + } elif (call.func.name == "push_clone") { + callee_id = 2 + } + } + if (callee_id == 0) return + var arg1 = call.arguments[1] + if (arg1 is ExprRef2Value) { + arg1 = (arg1 as ExprRef2Value.subexpr) + } + if (arg1 == null || !(arg1 is ExprVar)) return + var ev = arg1 as ExprVar + if (ev.variable != state.iter_var) return + state.matched = true + state.callee_id = callee_id + perf022_stack[top_idx] = state + } + + def perf022_pop_and_check() : void { + if (empty(perf022_stack)) return + let top_idx = length(perf022_stack) - 1 + let state = perf022_stack[top_idx] + let owning = perf022_for_exprs[top_idx] + perf022_stack |> pop() + perf022_for_exprs |> pop() + if (state.matched && !state.disqualified && owning != null) { + let name = state.callee_id == 1 ? "push" : "push_clone" + self->perf_warning("PERF022: for-loop body 'B |> {name}(x)' pushes one element per iteration; use 'B |> {name}_from(A)' for a bulk reserve+copy", owning.at) + } + } + def override preVisitExprAt(var expr : ExprAt?) : void { if (in_closure == 0 && expr != null) { self->perf018_on_at(expr) @@ -1525,10 +1671,7 @@ def public perf_lint_collect(prog : ProgramPtr; var warnings : array; visit(prog, astVisitorAdapter) } let count = astVisitor.warning_count - warnings |> reserve(length(astVisitor.warnings)) - for (w in astVisitor.warnings) { - warnings |> push(w) - } + warnings |> push_from(astVisitor.warnings) unsafe { delete astVisitor; } return count } diff --git a/daslib/regex.das b/daslib/regex.das index 39348893be..48c8dd83fd 100644 --- a/daslib/regex.das +++ b/daslib/regex.das @@ -605,10 +605,7 @@ def private mk_union(var left, right : ReNode?) : ReNode? { if (left.op == ReOp.Union) { left.at.y = right.at.y if (right.op == ReOp.Union) {// union(union(x),union(y)) = union(xy) - left.all |> reserve(length(left.all) + length(right.all)) - for (x in right.all) { - push(left.all, x) - } + push_from(left.all, right.all) unsafe { delete right } diff --git a/daslib/style_lint.das b/daslib/style_lint.das index 15f7af9da6..c104decaf8 100644 --- a/daslib/style_lint.das +++ b/daslib/style_lint.das @@ -1622,10 +1622,7 @@ def public style_lint_collect(prog : ProgramPtr; var warnings : array; panic("style_lint internal: unsafe_stack not balanced after walk (size={length(astVisitor.unsafe_stack)})") } let count = astVisitor.warning_count - warnings |> reserve(length(astVisitor.warnings)) - for (w in astVisitor.warnings) { - warnings |> push(w) - } + warnings |> push_from(astVisitor.warnings) unsafe { delete astVisitor; } return count } diff --git a/doc/reflections/das2rst.das b/doc/reflections/das2rst.das index 46ec13f00d..5725f75d6c 100644 --- a/doc/reflections/das2rst.das +++ b/doc/reflections/das2rst.das @@ -150,9 +150,9 @@ def document_module_builtin(root : string) { hide_group(group_by_regex("Internal pointer arithmetics", mod, %regex~i_das_%%)), hide_group(group_by_regex("Internal clone infrastructure", mod, %regex~clone%%)), hide_group(group_by_regex("Internal finalize infrastructure", mod, %regex~finalize%%)), - group_by_regex("Containers", mod, %regex~(capacity|clear|length|resize|resize_no_init|reserve|each|emplace|erase|find| + group_by_regex("Containers", mod, %regex~(capacity|clear|length|resize|resize_no_init|reserve|each|emplace|emplace_from|erase|find| find_for_edit|find_if_exists|find_index|find_index_if|has_value|key_exists|keys|values|get_key|lock|each_enum|each_ref| -find_for_edit_if_exists|lock_forever|next|nothing|pop|push|push_clone|back|sort|to_array|to_table|to_array_move| +find_for_edit_if_exists|lock_forever|next|nothing|pop|push|push_from|push_clone|push_clone_from|back|sort|to_array|to_table|to_array_move| to_table_move|empty|subarray|insert|move_to_ref|copy_to_local|move_to_local|get|remove_value|erase_if|resize_and_init| get_value|insert_clone|emplace_new|insert_default|emplace_default|get_with_default|modify)$%%), group_by_regex("Character set groups", mod, %regex~(is_alpha|is_number|is_white_space|is_char_in_set)$%%), diff --git a/doc/source/reference/language/lint.rst b/doc/source/reference/language/lint.rst index 3128b7203a..cf53e7ab8b 100644 --- a/doc/source/reference/language/lint.rst +++ b/doc/source/reference/language/lint.rst @@ -829,6 +829,54 @@ User-named struct / enum / bitfield constructors (``MyEnum(x)``, ``Foo(v=x)``) and multi-argument vector constructors (``float2(x, y)``) do not match the workhorse cast set and are intentionally out of scope. +PERF022 — for-loop pushing one element per iteration +===================================================== + +A loop body that consists of exactly one ``push(s)`` or ``push_clone(s)`` +of the for-loop iteration variable into a destination array is the +element-at-a-time form of an array concatenation. The bulk overloads +``push_from`` / ``push_clone_from`` (in ``daslib/builtin.das``) reserve the +combined capacity up front and skip the per-iteration capacity check. + +The rule fires for the iteration-variable shape only — a transform, +if-guard, multi-statement body, or multi-source ``for`` does not match, +because those have no direct bulk equivalent. + +Compiler folds ``B |> push(s)``, ``B.push(s)``, and ``push(B, s)`` to the +same call shape, so all three forms are detected by the same rule. + +.. code-block:: das + + // Bad + for (s in src) { // PERF022 + dst |> push(s) + } + + // Good + dst |> push_from(src) + +The bulk forms expect the destination to be ``array`` and the source to +be ``array`` or a fixed-size C-array ``T[N]``. Range, string, iterator, +and generator sources do not have a bulk overload and are left unflagged. + +The same recommendation applies to ``push_clone``: + +.. code-block:: das + + // Bad + for (s in src) { // PERF022 + dst |> push_clone(s) + } + + // Good + dst |> push_clone_from(src) + +``emplace`` is not in the rule's scope: a for-loop iteration variable is a +const reference, but ``emplace`` requires a mutable reference, so the +hand-rolled shape ``for (s in src) { dst |> emplace(s) }`` does not +compile. The ``emplace_from`` bulk overload still exists in +``daslib/builtin.das`` for direct calls with a mutable source array. + .. _style_lint: ----------- diff --git a/doc/source/reference/tutorials/06_arrays.rst b/doc/source/reference/tutorials/06_arrays.rst index 96be5160a6..50f6de1352 100644 --- a/doc/source/reference/tutorials/06_arrays.rst +++ b/doc/source/reference/tutorials/06_arrays.rst @@ -52,6 +52,27 @@ Operations resize(buf, 5) // resize to 5 elements (zero-filled) clear(buf) // remove all elements +Bulk append +=========== + +To append every element of one array to another, prefer the bulk +``push_from`` / ``push_clone_from`` / ``emplace_from`` overloads over a +hand-rolled element-at-a-time ``for`` loop. The bulk forms reserve the +combined capacity up front, so the allocator is touched once instead of +N times:: + + var prefix <- [1, 2, 3] + var suffix <- [4, 5] + var combined : array + combined |> push_from(prefix) + combined |> push_from(suffix) + // combined: [1, 2, 3, 4, 5] + +The ``PERF022`` lint rule flags the element-at-a-time shape and +recommends the bulk form. ``push_clone_from`` clones each element (deep +copy semantics) and ``emplace_from`` moves each element (consuming the +source). + Iteration ========= diff --git a/doc/source/stdlib/handmade/function-builtin-emplace_from-0xc49d62384d1e9e92.rst b/doc/source/stdlib/handmade/function-builtin-emplace_from-0xc49d62384d1e9e92.rst new file mode 100644 index 0000000000..02d3e59dc6 --- /dev/null +++ b/doc/source/stdlib/handmade/function-builtin-emplace_from-0xc49d62384d1e9e92.rst @@ -0,0 +1 @@ +Bulk-moves every element of source ``varr`` (an array or fixed-size C-array) into ``Arr`` via ``<-``. Reserves the combined capacity up front; the source is consumed. diff --git a/doc/source/stdlib/handmade/function-builtin-push_clone_from-0x4f26fb65c0960a56.rst b/doc/source/stdlib/handmade/function-builtin-push_clone_from-0x4f26fb65c0960a56.rst new file mode 100644 index 0000000000..9f14719ae4 --- /dev/null +++ b/doc/source/stdlib/handmade/function-builtin-push_clone_from-0x4f26fb65c0960a56.rst @@ -0,0 +1 @@ +Bulk-appends every element of source ``varr`` (an array or fixed-size C-array) to ``Arr`` via clone (``:=``). Reserves the combined capacity up front; one allocator touch instead of N. diff --git a/doc/source/stdlib/handmade/function-builtin-push_from-0xaacdbe8b8e9e40ff.rst b/doc/source/stdlib/handmade/function-builtin-push_from-0xaacdbe8b8e9e40ff.rst new file mode 100644 index 0000000000..b3f423575f --- /dev/null +++ b/doc/source/stdlib/handmade/function-builtin-push_from-0xaacdbe8b8e9e40ff.rst @@ -0,0 +1 @@ +Bulk-appends every element of source ``varr`` (an array or fixed-size C-array) to ``Arr`` via copy. Reserves the combined capacity up front, so an O(N) bulk append touches the allocator once instead of N times. diff --git a/modules/dasClangBind/bind/bind_llvm.das b/modules/dasClangBind/bind/bind_llvm.das index 7ece135b48..de9ceaea47 100644 --- a/modules/dasClangBind/bind/bind_llvm.das +++ b/modules/dasClangBind/bind/bind_llvm.das @@ -17,7 +17,7 @@ let private per_target_init_suffixes <- array( // prebuilt LLVM.dll (linux x86_64, linux arm64, mac arm64, windows x86_64). // Anything else is filtered out at regen time to keep the binding loadable. let private universal_targets <- array( - "X86", "AArch64", "ARM" + "X86", "AArch64", "ARM", "WebAssembly" ) def private skip_per_target_initializer(function_name : string) : bool { @@ -113,44 +113,25 @@ class LlvmGen : CppGenBind { return true } def override skip_file(fname : string) : bool { - if (!any <| [iterator for(h in llvm_c_headers); fname |> ends_with(h)]) { - return true - } - return false + return !any <| [iterator for(h in llvm_c_headers); fname |> ends_with(h)] } def override skip_alias(var c : CXCursor) { let aliasf = clang_getCursorLocationFileName(c) - if (!any <| [iterator for(h in llvm_c_headers); aliasf |> ends_with(h)]) { - return true - } - return false + return !any <| [iterator for(h in llvm_c_headers); aliasf |> ends_with(h)] } def override skip_enum(ns_en, en : string) { - if (ns_en == "std::byte" || ns_en |> starts_with("(unnamed enum")) { - return true - } - return false + return ns_en == "std::byte" || ns_en |> starts_with("(unnamed enum") } def override skip_struct(sn : string) { - if (sn |> starts_with("__crt")) { - return true - } - return false + return sn |> starts_with("__crt") } def override skip_function(var c : CXCursor) : bool { let function_name = string(clang_getCursorSpelling(c)) if (function_name == "LLVMConstGEP2" || function_name == "LLVMConstInBoundsGEP2" - || function_name == "LLVMOrcObjectLayerAddObjectFileWithRT" || function_name == "LLVMInitializeCore") { - return true - } + || function_name == "LLVMOrcObjectLayerAddObjectFileWithRT" || function_name == "LLVMInitializeCore") return true let funf = clang_getCursorLocationFileName(c) - if (!any <| [iterator for(h in llvm_c_headers); funf |> ends_with(h)]) { - return true - } - if (skip_anyFunction(c, false)) { - return true - } - return false + if (!any <| [iterator for(h in llvm_c_headers); funf |> ends_with(h)]) return true + return skip_anyFunction(c, false) } } @@ -159,22 +140,14 @@ class LlvmGenDinamic : DasGenBind { init_args(pfn, pfp, args) } def override skip_const(name : string) : bool { - if (name == "true" || name == "false") { - return true - } - return false + return name == "true" || name == "false" } def override skip_alias(var c : CXCursor) { let aliasf = clang_getCursorLocationFileName(c) let typedef_name = string(clang_getCursorSpelling(c)) // Let's skip ssize_t, we never use it. - if (typedef_name == "ssize_t") { - return true - } - if (!any <| [iterator for(h in llvm_c_headers); aliasf |> ends_with(h)]) { - return true - } - return false + if (typedef_name == "ssize_t") return true + return !any <| [iterator for(h in llvm_c_headers); aliasf |> ends_with(h)] } def override skip_function(var c : CXCursor) : bool { let function_name = string(clang_getCursorSpelling(c)) @@ -189,24 +162,14 @@ class LlvmGenDinamic : DasGenBind { // only needs the host target, so skipping every per-target initializer // outside the X86/AArch64/ARM set keeps the binding loadable everywhere. skip_per_target_initializer(function_name) - ) { - return true - } + ) return true let funf = clang_getCursorLocationFileName(c) - if (!any <| [iterator for(h in llvm_c_headers); funf |> ends_with(h)]) { - return true - } - if (skip_anyFunction(c, false)) { - return true - } - return false + if (!any <| [iterator for(h in llvm_c_headers); funf |> ends_with(h)]) return true + return skip_anyFunction(c, false) } def override skip_file(fname : string) : bool { - if (!any <| [iterator for(h in llvm_c_headers); fname |> ends_with(h)]) { - return true - } - return false + return !any <| [iterator for(h in llvm_c_headers); fname |> ends_with(h)] } def override output_dasbind(function_name : string) { fprint(func_decl_file, "[extern(cdecl, name=\"{function_name}\", library=\"LLVM.dll\")]\n") @@ -221,7 +184,7 @@ def main { print_help(get_command_info(type), "bind_llvm") return } - var cfg <- r |> move_unwrap + let cfg <- r |> move_unwrap if (cfg.help) { print_help(get_command_info(type), "bind_llvm") return @@ -245,14 +208,14 @@ def main { if (is_dynamic) { // generate let output_name = "{cfg.output}/llvm_func.das" - fopen(output_name, "wb") <| $(fdf) { + fopen(output_name, "wb") $(fdf) { fprint(fdf, "require dasbind public\n") fprint(fdf, "options gen2\n") fprint(fdf, "module llvm_func shared\n") fprint(fdf, "require llvm/bindings/llvm_struct\n") fprint(fdf, "require llvm/bindings/llvm_enum\n") let output_const_name = "{cfg.output}/llvm_const.das" - fopen(output_const_name, "wb") <| $(dcf) { + fopen(output_const_name, "wb") $(dcf) { var glbind = new LlvmGenDinamic(pfn, pfp, args) var scf = glbind.openFile("{cfg.output}/llvm_struct.das") var ecf = glbind.openFile("{cfg.output}/llvm_enum.das") diff --git a/modules/dasLLVM/README.md b/modules/dasLLVM/README.md index 1b0ac2fb4d..8641d1af35 100644 --- a/modules/dasLLVM/README.md +++ b/modules/dasLLVM/README.md @@ -78,3 +78,110 @@ cached DLL for instant execution. ### DLL location - By default, the `dll` is stored in `.jitted_scripts/`. - This can be changed using `jit_output_path`. + +## Cross-compilation (WebAssembly) +The JIT pipeline can emit a non-host target instead of running on the host. +The supported cross-target is `wasm32-unknown-emscripten` (the default when no +explicit triple is passed). + +### How it works +`-exe` mode usually emits a host object via LLVM and links it with `clang`/`clang-cl`. +When a cross-compile target is selected, dasLLVM instead: +1. Initializes the WebAssembly LLVM target (lazy — no JIT-startup overhead when unused). +2. Builds a `TargetMachine` for the requested triple and pins the module's data + layout / triple so codegen sizes pointers as 32-bit. +3. Emits a `wasm32` object file via `LLVMTargetMachineEmitToFile`. +4. Links via `emcc [] -sSTANDALONE_WASM --no-entry`. + The runtime archive is included only when the module references runtime + symbols (i.e. has external decls). Pure programs link without it. + +The runtime archive is auto-located at +`/web/output/lib/liblibDaScript_runtime.a` and is produced by the +existing emscripten build (`web/CMakeLists.txt`). If a program references +runtime symbols but the archive is missing, the link still proceeds and emits +a warning — the resulting `.wasm` will fail at load time on unresolved +imports for every runtime symbol that is actually used. + +### Linker tools +The wasm link always goes through `emcc`. dasLLVM resolves it in this order: +1. Explicit override (set `cop.jit_path_to_linker` programmatically). +2. `/bin/emcc[.bat]` — bundled next to `daslang`. +3. `emcc` on `PATH` (typically provided by an activated emsdk). + +### Building libDaScript_runtime for wasm +The runtime archive needed by the emcc path is a side-effect of the +emscripten build documented in `web/README.md`. One-time setup (assumes +`emcc` is on `PATH` — see `web/README.md` for install options, e.g. +`sudo apt install emscripten` on Ubuntu): + +```sh +emcmake cmake -S web -B web/cmake_temp -G Ninja -DCMAKE_BUILD_TYPE=Release && cmake --build web/cmake_temp --target libDaScript_runtime +``` + +This drops `liblibDaScript_runtime.a` (wasm32) into `web/output/lib/`. +Re-run only when runtime sources change. + +#### Custom runtime archive path +By default dasLLVM auto-locates the archive at +`/web/output/lib/liblibDaScript_runtime.a`. To use a different +location pass `--jit-runtime-lib=` after `--`: + +```sh +./bin/daslang -exe -output add add.das -- \ + --jit-target=wasm32-unknown-emscripten \ + --jit-runtime-lib=/opt/daslang/wasm/liblibDaScript_runtime.a +``` + +Equivalent script-level option: `options jit_runtime_lib = "..."`. CLI wins +when both are set. + +### Example +Write `add.das`: +``` +options gen2 +def add(a, b : int) : int { return a + b; } +[export] def main() : int { return add(2, 3); } +``` +Pick a triple via either the script or the CLI (only one is required; both +are optional — CLI wins when both present): + +**Script-level option:** +``` +options jit_target = "wasm32-unknown-emscripten" +``` +then: +```sh +./bin/daslang -exe -output add add.das +``` + +**CLI flag** (no edit to the script): +```sh +./bin/daslang -exe -output add add.das -- --jit-target=wasm32-unknown-emscripten +``` +`-exe` selects executable JIT mode; the triple pins codegen to wasm32 and +`emcc` links `add.wasm` next to `-output`. Script-side flags live after `--`. + +### Running the produced `.wasm` +For pure-arithmetic programs (no runtime linked): +``` +node -e 'WebAssembly.instantiate(require("fs").readFileSync("add.wasm"),{env:new Proxy({},{get:()=>()=>0})}).then(r=>console.log(r.instance.exports.main()))' +``` +Prints `5`. +The `Proxy` supplies a `()=>0` stub for every wasm import — needed because +`--allow-undefined` leaves daslang-runtime symbols as imports. + +For programs that pull in `libDaScript_runtime` (auto-detected, see *How it +works*), the output is `-sSTANDALONE_WASM` and the only imports are wasi +syscalls. Run under any wasi-capable host: +``` +wasmtime add.wasm +# or with Node ≥ 20: +node --experimental-wasi-unstable-preview1 -e \ + 'const {WASI}=require("node:wasi");const fs=require("fs");\ + const w=new WASI({version:"preview1"});\ + WebAssembly.instantiate(fs.readFileSync("add.wasm"),w.getImportObject())\ + .then(r=>w.start(r.instance))' +``` + +Threading is unsupported: the runtime archive is built without `-sUSE_PTHREADS=1`, +so jobque/channels/`LockBox` paths trap at runtime even though they link. diff --git a/modules/dasLLVM/bindings/llvm_func.das b/modules/dasLLVM/bindings/llvm_func.das index c92dad42fc..84cc15e70a 100644 --- a/modules/dasLLVM/bindings/llvm_func.das +++ b/modules/dasLLVM/bindings/llvm_func.das @@ -1807,36 +1807,48 @@ def LLVMCreateStringError(ErrMsg : string implicit) : LLVMOpaqueError? {} def LLVMInitializeAArch64TargetInfo() : void {} [extern(cdecl, name="LLVMInitializeARMTargetInfo", library="LLVM.dll")] def LLVMInitializeARMTargetInfo() : void {} +[extern(cdecl, name="LLVMInitializeWebAssemblyTargetInfo", library="LLVM.dll")] +def LLVMInitializeWebAssemblyTargetInfo() : void {} [extern(cdecl, name="LLVMInitializeX86TargetInfo", library="LLVM.dll")] def LLVMInitializeX86TargetInfo() : void {} [extern(cdecl, name="LLVMInitializeAArch64Target", library="LLVM.dll")] def LLVMInitializeAArch64Target() : void {} [extern(cdecl, name="LLVMInitializeARMTarget", library="LLVM.dll")] def LLVMInitializeARMTarget() : void {} +[extern(cdecl, name="LLVMInitializeWebAssemblyTarget", library="LLVM.dll")] +def LLVMInitializeWebAssemblyTarget() : void {} [extern(cdecl, name="LLVMInitializeX86Target", library="LLVM.dll")] def LLVMInitializeX86Target() : void {} [extern(cdecl, name="LLVMInitializeAArch64TargetMC", library="LLVM.dll")] def LLVMInitializeAArch64TargetMC() : void {} [extern(cdecl, name="LLVMInitializeARMTargetMC", library="LLVM.dll")] def LLVMInitializeARMTargetMC() : void {} +[extern(cdecl, name="LLVMInitializeWebAssemblyTargetMC", library="LLVM.dll")] +def LLVMInitializeWebAssemblyTargetMC() : void {} [extern(cdecl, name="LLVMInitializeX86TargetMC", library="LLVM.dll")] def LLVMInitializeX86TargetMC() : void {} [extern(cdecl, name="LLVMInitializeAArch64AsmPrinter", library="LLVM.dll")] def LLVMInitializeAArch64AsmPrinter() : void {} [extern(cdecl, name="LLVMInitializeARMAsmPrinter", library="LLVM.dll")] def LLVMInitializeARMAsmPrinter() : void {} +[extern(cdecl, name="LLVMInitializeWebAssemblyAsmPrinter", library="LLVM.dll")] +def LLVMInitializeWebAssemblyAsmPrinter() : void {} [extern(cdecl, name="LLVMInitializeX86AsmPrinter", library="LLVM.dll")] def LLVMInitializeX86AsmPrinter() : void {} [extern(cdecl, name="LLVMInitializeAArch64AsmParser", library="LLVM.dll")] def LLVMInitializeAArch64AsmParser() : void {} [extern(cdecl, name="LLVMInitializeARMAsmParser", library="LLVM.dll")] def LLVMInitializeARMAsmParser() : void {} +[extern(cdecl, name="LLVMInitializeWebAssemblyAsmParser", library="LLVM.dll")] +def LLVMInitializeWebAssemblyAsmParser() : void {} [extern(cdecl, name="LLVMInitializeX86AsmParser", library="LLVM.dll")] def LLVMInitializeX86AsmParser() : void {} [extern(cdecl, name="LLVMInitializeAArch64Disassembler", library="LLVM.dll")] def LLVMInitializeAArch64Disassembler() : void {} [extern(cdecl, name="LLVMInitializeARMDisassembler", library="LLVM.dll")] def LLVMInitializeARMDisassembler() : void {} +[extern(cdecl, name="LLVMInitializeWebAssemblyDisassembler", library="LLVM.dll")] +def LLVMInitializeWebAssemblyDisassembler() : void {} [extern(cdecl, name="LLVMInitializeX86Disassembler", library="LLVM.dll")] def LLVMInitializeX86Disassembler() : void {} [extern(cdecl, name="LLVMGetModuleDataLayout", library="LLVM.dll")] diff --git a/modules/dasLLVM/daslib/llvm_exe.das b/modules/dasLLVM/daslib/llvm_exe.das index ba076153e4..0f06f0d71a 100644 --- a/modules/dasLLVM/daslib/llvm_exe.das +++ b/modules/dasLLVM/daslib/llvm_exe.das @@ -190,7 +190,7 @@ class CollectExternVisitor : AstVisitor { standalone_ctx, types.ConstI64(fn.index |> uint64), get_string_constant_ptr(builder, string(fn.name)), - get_string_constant_ptr(builder, string(fn |> get_mangled_name())), + get_string_constant_ptr(builder, fn |> get_mangled_name()), types.ConstI64(fn.getMangledNameHash), types.ConstI32(fn.totalStackSize |> uint64), fn_ptr_cast, @@ -677,9 +677,9 @@ def private write_release_deps(prog : Program?) { var by_das : table var by_pkg : table for_each_registered_dynamic_module() $(path, mod_name, das_name) { - let abs = string(path) + let abs = path let rel = compute_modules_relative_suffix(abs) - let entry = SharedModuleEntry(rel = rel, abs = abs, das_name = string(das_name)) + let entry = SharedModuleEntry(rel = rel, abs = abs, das_name = das_name) by_das[entry.das_name] = entry let pkg = dylib_package_name(rel) if (!empty(pkg)) { @@ -734,7 +734,8 @@ def private write_release_deps(prog : Program?) { // all compiled functions, runs init scripts, then calls the program entry point. def public inject_main(program_context : Context?; ctx : LLVMContextRef; prog : Program?; entry_point : string; mod : LLVMOpaqueModule?; - var types : PrimitiveTypes?, var uids : UidNodes?; strict : bool) : tuple { + var types : PrimitiveTypes?, var uids : UidNodes?; strict : bool; + target_triple : string = "") : tuple { let builder = LLVMCreateBuilder() defer() { @@ -770,6 +771,7 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; var start_fn_name = "" var no_return = true + var bool_return = false for (fn in funcs) { if (fn.name == entry_point && fn.arguments.empty()) { assume fnmna = uids.get_dll_fn_name(fn).impl() @@ -779,6 +781,10 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; } elif (fn.result.baseType == Type.tInt) { start_fn_name = fnmna no_return = false + } elif (fn.result.baseType == Type.tBool) { + start_fn_name = fnmna + no_return = false + bool_return = true } } } @@ -793,7 +799,10 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; types.LLVMVoidPtrType() // argv ) ) - let main_fn = LLVMAddFunctionWithType(mod, "main", main_fn_type) + // wasm32-emscripten: emcc's libstandalonewasm already defines `main`, + // emit `__main_argc_argv` instead and let crt1 chain through. + let main_sym = (target_triple |> starts_with("wasm32-")) ? "__main_argc_argv" : "main" + let main_fn = LLVMAddFunctionWithType(mod, main_sym, main_fn_type) let entry = LLVMAppendBasicBlockInContext(ctx, main_fn, "entry") LLVMPositionBuilderAtEnd(builder, entry) @@ -1032,6 +1041,11 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; LLVMBuildCall2(builder, shutdown_fn_type, shutdown_fn, array(), "") if (no_return) { LLVMBuildRet(builder, types.ConstI32(uint64(0))) + } elif (bool_return) { + // bool main → exit code: true → 0 (success), false → 1 (failure). + let exit_code = LLVMBuildSelect(builder, main_ret, + types.ConstI32(uint64(0)), types.ConstI32(uint64(1)), "main_exit") + LLVMBuildRet(builder, exit_code) } else { LLVMBuildRet(builder, main_ret) } diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das index 53da917a21..5ca163f69d 100644 --- a/modules/dasLLVM/daslib/llvm_jit.das +++ b/modules/dasLLVM/daslib/llvm_jit.das @@ -46,6 +46,11 @@ struct AnnotationVarInfoEntry { } var annotation_varinfo_list : array +// (parent_global, resolvent_str_global) pairs for tHandle typeinfo slot 0. +// Static initializer leaves slot 0 null; runtime ctor writes the tagged pointer +// because wasm32 forbids widening ptrtoint inside .data initializers. +var handle_typeinfo_pending : array> + [macro_function] def private get_expr_ptr(expr : ExpressionPtr) { return expr @@ -233,6 +238,51 @@ def generate_annotations_ctor_dtor(types : PrimitiveTypes; var entries : array>; jit_mode : bool) { + // Slot 0 (annotation_or_name) of every tHandle TypeInfo holds + // ((TypeAnnotation*)(intptr_t(annName)|1)) + // wasm32 forbids widening ptrtoint inside .data initializers (the cast + // must run post-relocation), so the tagged pointer is materialized at + // runtime by this constructor function. globalopt would otherwise fold + // the ctor-only store back into the static initializer — the parent + // global is marked externally_initialized to block that. + var ctor_builder = LLVMCreateBuilderInContext(g_ctx) + defer() { + LLVMDisposeBuilder(ctor_builder) + } + let functionType = LLVMFunctionType(types.t_void, null, 0u, 0) + let constructor = LLVMAddFunctionWithType(g_mod, "handle_typeinfo_constructor", functionType) + if (!jit_mode) { + set_private_linkage(constructor) + } else { + set_public_linkage(constructor) + } + let entry_ctor = LLVMAppendBasicBlockInContext(g_ctx, constructor, "entry_ctor") + LLVMPositionBuilderAtEnd(ctor_builder, entry_ctor) + + let void_ptr = types.LLVMVoidPtrType() + let void_ptr_ptr = LLVMPointerType(void_ptr, 0u) + let one = LLVMConstInt(types.t_int64, 1 |> uint64(), 0) + for ((parent_global, resolvent_str) in entries) { + // tagged = (i8*)(ptrtoint(resolvent) + 1) + // Mark parent as externally_initialized: globalopt would otherwise + // fold ctor-only stores back into the static initializer, which + // re-introduces the ptrtoint ConstantExpr that wasm32 cannot lower + // in .data. The attribute is LLVM's intended signal for + // "runtime-initialized by external code, don't fold". + LLVMSetExternallyInitialized(parent_global, 1) + let nameInt = LLVMBuildPtrToInt(ctor_builder, resolvent_str, types.t_int64, "nameInt") + let taggedInt = LLVMBuildAdd(ctor_builder, nameInt, one, "taggedInt") + let taggedPtr = LLVMBuildIntToPtr(ctor_builder, taggedInt, void_ptr, "taggedPtr") + // slot 0 is at offset 0 — alias parent_global as i8** and store. + let slot0 = LLVMBuildBitCast(ctor_builder, parent_global, void_ptr_ptr, "slot0") + LLVMBuildStore(ctor_builder, taggedPtr, slot0) + } + LLVMBuildRetVoid(ctor_builder) + return (constructor, unsafe(reinterpret(null))) +} + [macro_function] def get_function_addr(var ctx : Context?; func : Function?) { let mangled_name = get_mangled_name(func) @@ -303,6 +353,9 @@ class public LlvmJitVisitor : AstVisitor { option_no_range_check : bool = false option_no_alias : bool = false option_no_capture : bool = false + // True once any ExprCall to a builtIn function has been emitted, i.e. this + // function pulls in libDaScript_runtime. + has_externals : bool = false flags : LlvmJitFlags emit_prologue : bool = false own_di : bool = false @@ -1655,6 +1708,9 @@ class public LlvmJitVisitor : AstVisitor { } def override visitExprCall(expr : ExprCall?) : ExpressionPtr { + if (expr.func != null && expr.func.flags.builtIn) { + has_externals = true + } make_call(expr, expr.doesNotNeedSp) return expr } @@ -3857,6 +3913,7 @@ class public LlvmJitVisitor : AstVisitor { set_globalvar_linkage(varinfo) let vi = unsafe(ei[i]) varinfo |> LLVMSetInitializer <| get_value_for_varinfo(vi) + queue_handle_typeinfo_if_needed(unsafe(reinterpret vi), varinfo) array_init |> push <| unsafe(reinterpret(varinfo)) if (vi.annotation_arguments != null) { let ann_ty = get_llvm_type_for_annotation_arg_pod() @@ -3975,6 +4032,17 @@ class public LlvmJitVisitor : AstVisitor { return global } + // For tHandle: emits resolvent string global + queues (parent, resolvent) + // for runtime slot-0 store. Parent's slot 0 stays null in the static + // initializer — wasm32 forbids widening ptrtoint inside .data initializers. + def queue_handle_typeinfo_if_needed(info : TypeInfo?; parent_global : LLVMValueRef) { + if (info == null || info.basicType != Type.tHandle) return + let resolvent = "~{info.annotation._module.name}::{info.annotation.name}" + var llvm_resolvent = g_builder |> get_string_constant_ptr(resolvent) + LLVMSetAlignment(llvm_resolvent, 2u) + handle_typeinfo_pending |> push((parent_global, llvm_resolvent)) + } + def rec_create_type_info_global(info : TypeInfo?) { var initValues = array() initValues |> resize(6) @@ -3982,18 +4050,8 @@ class public LlvmJitVisitor : AstVisitor { initValues[i] = LLVMConstPointerNull(types.LLVMVoidPtrType()) } if (info.basicType == Type.tHandle) { - let resolvent = "~{info.annotation._module.name}::{info.annotation.name}" - var llvm_resolvent = g_builder |> get_string_constant_ptr(resolvent) - LLVMSetAlignment(llvm_resolvent, 2u) - // now do this: - // info->annotation_or_name = ((TypeAnnotation*)(intptr_t(annName)|1)); - let one = LLVMConstInt(types.t_int64, 1 |> uint64(), 0) - let ptrToInt = LLVMBuildPtrToInt(g_builder, llvm_resolvent, types.t_int64, "ptrToInt") - // Or constexpr are forbidden. Since we have alignment we can use add. - let modifiedPtr = LLVMBuildAdd(g_builder, ptrToInt, one, "modifiedPtr") - let intToPtr = LLVMBuildIntToPtr(g_builder, modifiedPtr, types.LLVMVoidPtrType(), "intToPtr") - - initValues[0] = intToPtr + // slot 0 stays null — parent-site queue_handle_typeinfo_if_needed + // arranged the runtime store. } elif (info.basicType == Type.tStructure) { var t = create_struct_global_pointer(info.structType) initValues[0] = t @@ -4052,6 +4110,7 @@ class public LlvmJitVisitor : AstVisitor { typeinfo_cache[ti] = type_info_global + queue_handle_typeinfo_if_needed(ti, type_info_global) let init_values <- get_typeinfo_fields(ti) var type_info_init = LLVMGetUndef(type_info_type) for (i in urange(init_values |> length())) { @@ -6149,7 +6208,6 @@ def private get_llvm_function_type(func : FunctionPtr; types : PrimitiveTypes?) [macro_function] def private add_ctor_dtor_function(ctor_dtor : array>; types : PrimitiveTypes?) { - let total_sz = ctor_dtor |> length() |> uint let priority = 0 |> uint64() let dataPointerType = LLVMPointerType(types.t_int8, 0u) let funcType = LLVMPointerType(LLVMFunctionType(types.t_void, null, 0u, 0), 0u) @@ -6171,33 +6229,42 @@ def private add_ctor_dtor_function(ctor_dtor : array reserve(n_entries) dtor_entries |> reserve(n_entries) - var i = 0 + // Skip null fn entries — caller passed null to opt out of ctor or dtor. + // ctor / dtor counted independently; each may be null without affecting the other. + var ci = 0 + var di = 0 for ((ctor, dtor) in ctor_dtor) { - ctorFields |> push(types.ConstI32(priority)) - var c := ctor - ctorFields |> push(c) - ctorFields |> push(LLVMConstPointerNull(dataPointerType)) - ctor_entries |> push(LLVMConstNamedStruct(ctorStructType, unsafe(addr(ctorFields[i])), 3u)) - - dtorFields |> push(types.ConstI32(priority)) - var d := dtor - dtorFields |> push(d) - dtorFields |> push(LLVMConstPointerNull(dataPointerType)) - dtor_entries |> push(LLVMConstNamedStruct(dtorStructType, unsafe(addr(dtorFields[i])), 3u)) - i = i + 3 + if (ctor != null) { + ctorFields |> push(types.ConstI32(priority)) + var c := ctor + ctorFields |> push(c) + ctorFields |> push(LLVMConstPointerNull(dataPointerType)) + ctor_entries |> push(LLVMConstNamedStruct(ctorStructType, unsafe(addr(ctorFields[ci])), 3u)) + ci += 3 + } + if (dtor != null) { + dtorFields |> push(types.ConstI32(priority)) + var d := dtor + dtorFields |> push(d) + dtorFields |> push(LLVMConstPointerNull(dataPointerType)) + dtor_entries |> push(LLVMConstNamedStruct(dtorStructType, unsafe(addr(dtorFields[di])), 3u)) + di += 3 + } } - var ctorStruct = LLVMConstNamedStruct(ctorStructType, array_data_ptr(ctorFields), 3u) - var dtorStruct = LLVMConstNamedStruct(dtorStructType, array_data_ptr(dtorFields), 3u) - - let ctorArray = LLVMAddGlobal(g_mod, LLVMArrayType(ctorStructType, total_sz), "llvm.global_ctors") - let dtorArray = LLVMAddGlobal(g_mod, LLVMArrayType(dtorStructType, total_sz), "llvm.global_dtors") + let ctor_sz = ctor_entries |> length() |> uint + let dtor_sz = dtor_entries |> length() |> uint - LLVMSetLinkage(ctorArray, LLVMLinkage.LLVMAppendingLinkage) - LLVMSetLinkage(dtorArray, LLVMLinkage.LLVMAppendingLinkage) - - LLVMSetInitializer(ctorArray, LLVMConstArray(ctorStructType, array_data_ptr(ctor_entries), total_sz)) - LLVMSetInitializer(dtorArray, LLVMConstArray(dtorStructType, array_data_ptr(dtor_entries), total_sz)) + if (ctor_sz != 0u) { + let ctorArray = LLVMAddGlobal(g_mod, LLVMArrayType(ctorStructType, ctor_sz), "llvm.global_ctors") + LLVMSetLinkage(ctorArray, LLVMLinkage.LLVMAppendingLinkage) + LLVMSetInitializer(ctorArray, LLVMConstArray(ctorStructType, array_data_ptr(ctor_entries), ctor_sz)) + } + if (dtor_sz != 0u) { + let dtorArray = LLVMAddGlobal(g_mod, LLVMArrayType(dtorStructType, dtor_sz), "llvm.global_dtors") + LLVMSetLinkage(dtorArray, LLVMLinkage.LLVMAppendingLinkage) + LLVMSetInitializer(dtorArray, LLVMConstArray(dtorStructType, array_data_ptr(dtor_entries), dtor_sz)) + } } // We need to overwrite address-like dependencies in .dll before start. @@ -6447,7 +6514,7 @@ def public get_dll_missing(fns : array; disabled : array add_ctor_dtor_function(types) active_filenames = table() annotation_varinfo_list |> clear() + handle_typeinfo_pending |> clear() } [macro_function] @@ -6730,5 +6800,6 @@ class public DisableJitVisitor : AstVisitor { def init_llvm_jit_module_options() { if (is_compiling_macros_in_module("llvm_jit")) { this_module() |> add_module_option("jit_vec3_ldu", Type.tBool) + this_module() |> add_module_option("jit_target", Type.tString) } } diff --git a/modules/dasLLVM/daslib/llvm_jit_common.das b/modules/dasLLVM/daslib/llvm_jit_common.das index 24a784dfd1..013e765276 100644 --- a/modules/dasLLVM/daslib/llvm_jit_common.das +++ b/modules/dasLLVM/daslib/llvm_jit_common.das @@ -19,6 +19,8 @@ require daslib/strings require daslib/strings_boost require daslib/defer require daslib/enum_trait +require daslib/fio +require daslib/option require jit let public LLVM_DEBUG_RESULT = false @@ -717,22 +719,18 @@ def public build_block_type { // driven by LLVMRunPasses. Use true for JIT and JIT-DLL-cache emission // (the artifact only ever runs on this host); use false for redistributable // exe/dll emission where the binary must run on any compatible CPU. +// Build a TargetMachine for an arbitrary triple/cpu/features. Caller owns +// triple/cpu/features (we do not LLVMDisposeMessage them — they are plain +// das `string`s here, not heap-owned LLVM messages). [macro_function] -def public with_default_target_machine(opt_level : uint; use_host_cpu : bool; - blk : block<(LLVMTargetMachineRef) : void>) { - LLVMInitializeAllTargetInfos() - LLVMInitializeAllTargets() - LLVMInitializeAllTargetMCs() - LLVMInitializeAllAsmPrinters() - - let triple = LLVMGetDefaultTargetTriple() - +def public with_target_machine(triple, cpu, features : string; opt_level : uint; + blk : block<(LLVMTargetMachineRef) : void>) { var target : LLVMTarget? - let error : string? - LLVMGetTargetFromTriple(triple, unsafe(addr(target)), error) + let rc = LLVMGetTargetFromTriple(triple, unsafe(addr(target)), null) + if (rc != 0 || target == null) { + panic("LLVM: unknown / unsupported target triple '{triple}'") + } - let cpu = use_host_cpu ? LLVMGetHostCPUName() : "" - let features = use_host_cpu ? LLVMGetHostCPUFeatures() : "" let cg_level = ( (opt_level >= 3u) ? LLVMCodeGenOptLevel.LLVMCodeGenLevelAggressive : (opt_level >= 2u) ? LLVMCodeGenOptLevel.LLVMCodeGenLevelDefault @@ -750,13 +748,28 @@ def public with_default_target_machine(opt_level : uint; use_host_cpu : bool; ) invoke(blk, targetMachine) + LLVMDisposeTargetMachine(targetMachine) +} + +[macro_function] +def public with_default_target_machine(opt_level : uint; use_host_cpu : bool; + blk : block<(LLVMTargetMachineRef) : void>) { + LLVMInitializeAllTargetInfos() + LLVMInitializeAllTargets() + LLVMInitializeAllTargetMCs() + LLVMInitializeAllAsmPrinters() + + let triple_msg = LLVMGetDefaultTargetTriple() + let cpu_msg = use_host_cpu ? LLVMGetHostCPUName() : "" + let features_msg = use_host_cpu ? LLVMGetHostCPUFeatures() : "" + + with_target_machine(triple_msg, cpu_msg, features_msg, opt_level, blk) if (use_host_cpu) { - LLVMDisposeMessage(cpu) - LLVMDisposeMessage(features) + LLVMDisposeMessage(cpu_msg) + LLVMDisposeMessage(features_msg) } - LLVMDisposeMessage(triple) - LLVMDisposeTargetMachine(targetMachine) + LLVMDisposeMessage(triple_msg) } // @mod - module to dump to dll @@ -786,6 +799,52 @@ def public write_exe(mod : LLVMOpaqueModule?; out_path : string; path_to_dascrip } } +// Locate the wasm32 libDaScript_runtime.a. If `override_path` is non-empty it +// wins (caller-supplied path, e.g. from --jit-runtime-lib CLI flag) and a +// missing file logs a warning. Otherwise auto-locates the archive at +// /web/output/lib/liblibDaScript_runtime.a. +def private find_runtime_lib(override_path : string) : Option { + if (!(override_path |> empty())) { + if (stat(override_path).is_valid) return some(override_path) + to_log(LOG_WARNING, "wasm: --jit-runtime-lib path does not exist: {override_path}\n") + return none(type) + } + let candidate = "{get_das_root()}/web/output/lib/liblibDaScript_runtime.a" + return stat(candidate).is_valid ? some(candidate) : none(type) +} + +// Cross-compile a module to wasm32 and link to a runnable .wasm via emcc. +// triple defaults to wasm32-unknown-emscripten (matches the runtime archive). +// path_to_emcc may be empty; link_wasm falls back to bin/emcc then PATH. +// needs_runtime gates inclusion of libDaScript_runtime.a in the link: +// true → archive is linked when found, otherwise warning + link without; +// false → archive is omitted unconditionally (pure programs). +// explicit_runtime is an optional caller-supplied archive path. +def public write_wasm(mod : LLVMOpaqueModule?; out_path : string; triple : string; path_to_emcc : string; needs_runtime : bool; explicit_runtime : string = "") { + LLVMInitializeWasmTarget() + let runtime_lib = find_runtime_lib(explicit_runtime) + if (needs_runtime && is_none(runtime_lib)) { + to_log(LOG_WARNING, "wasm: runtime symbols referenced but libDaScript_runtime.a not found at web/output/lib/ — run the web/ emscripten build to link in the runtime, or pass --jit-runtime-lib=\n") + } + let real_triple = (!(triple |> empty()) ? triple : "wasm32-unknown-emscripten") + with_target_machine(real_triple, "", "", 2u) $(targetMachine : LLVMTargetMachineRef) { + // Pin the module's data layout + triple so codegen sizes pointers as + // 32-bit. Without this the module inherits host layout and produces + // wrong-sized loads/stores in the emitted wasm. + let dl = LLVMCreateTargetDataLayout(targetMachine) + LLVMSetModuleDataLayout(mod, dl) + LLVMSetTarget(mod, real_triple) + LLVMDisposeTargetData(dl) + + let error : string? + let obj = add_obj_extension(out_path) + let filetype = LLVMCodeGenFileType.LLVMObjectFile + LLVMTargetMachineEmitToFile(targetMachine, mod, obj, filetype, error) + let runtime_arg = needs_runtime ? (runtime_lib ?? "") : "" + link_wasm(obj, "{out_path}.wasm", runtime_arg, path_to_emcc) + } +} + def public build_string_constant(message : string) { if (g_str2v |> key_exists(message)) return g_str2v[message] let msg_size = uint(length(message)) @@ -1022,7 +1081,7 @@ def public LLVMBuildLoadData2Aligned(builder : LLVMOpaqueBuilder?; var typ : LLV } def public LLVMBuildMemSet(builder : LLVMOpaqueBuilder?; ptr : LLVMOpaqueValue?; value : uint64; length : uint64; alignment : uint) { - LLVMBuildMemSet(builder, ptr, LLVMConstInt(g_prim_t.t_int8, value, 0), g_prim_t.ConstI32(length), uint(alignment)) + LLVMBuildMemSet(builder, ptr, LLVMConstInt(g_prim_t.t_int8, value, 0), g_prim_t.ConstI32(length), alignment) } def public expand_scalar(builder : LLVMOpaqueBuilder?; scalar : LLVMOpaqueValue?; vecType : TypeDeclPtr) { diff --git a/modules/dasLLVM/daslib/llvm_macro.das b/modules/dasLLVM/daslib/llvm_macro.das index 8d503a8610..8a2962621f 100644 --- a/modules/dasLLVM/daslib/llvm_macro.das +++ b/modules/dasLLVM/daslib/llvm_macro.das @@ -141,6 +141,14 @@ struct JitCliOptions { @clarg_name = "jit-dump" @clarg_doc = "JIT: dump generated LLVM IR" dump_ir : Option + + @clarg_name = "jit-target" + @clarg_doc = "JIT: cross-compile target triple (use with -exe; e.g. wasm32-unknown-emscripten)" + target : Option + + @clarg_name = "jit-runtime-lib" + @clarg_doc = "JIT: explicit path to libDaScript_runtime.a (wasm cross-compile); overrides auto-locate under web/output/lib/" + runtime_lib : Option } @@ -179,6 +187,15 @@ class JIT_LLVM : AstSimulateMacro { var output_path = config_out_path |> empty() ? ".jitted_scripts/{prog.thisNamespace}" : config_out_path assume path_to_shared_lib = string(prog.policies.jit_path_to_shared_lib) assume path_to_linker = string(prog.policies.jit_path_to_linker) + // Cross-compile triple: --jit-target= CLI wins over the + // script-level `options jit_target = "..."`. + let opt_target = (prog._options |> find_arg("jit_target")) ?as tString ?? "" + let target_triple = cli_opts.target |> unwrap_or(opt_target) + let gen_wasm = gen_exe && (target_triple |> starts_with("wasm")) + // Optional CLI override for the wasm runtime archive location. Falls + // back to `options jit_runtime_lib = "..."` then write_wasm's + // find_runtime_lib() auto-locate under web/output/lib/. + let runtime_lib_override = cli_opts.runtime_lib |> unwrap_or((prog._options |> find_arg("jit_runtime_lib")) ?as tString ?? "") // Set global options LLVM_JIT_ALLOW_UNALIGNED_VECTOR_READ_OUT_OF_BOUNDS = prog._options |> find_arg("jit_vec3_ldu") ?as tBool ?? LLVM_JIT_ALLOW_UNALIGNED_VECTOR_READ_OUT_OF_BOUNDS_DEFAULT @@ -251,9 +268,10 @@ class JIT_LLVM : AstSimulateMacro { to_log(LOG_INFO, "LLVM JIT: DLL cache hit {output_path}.dll\n") } } + var jit_has_externals = false if (recompile_prog) { for (fun in funcs) { - generate_llvm(ctx, g_prim_t, uids, attrs, fun, jit_flags) + jit_has_externals = generate_llvm(ctx, g_prim_t, uids, attrs, fun, jit_flags) || jit_has_externals let fnmna = uids.get_dll_fn_name(fun) var fn_impl = LLVMGetNamedFunction(g_mod, fnmna.impl()) var fn = LLVMGetNamedFunction(g_mod, fnmna.publ()) @@ -268,7 +286,7 @@ class JIT_LLVM : AstSimulateMacro { } } if (gen_exe) { - let res = inject_main(ctx, g_ctx, prog, exe_main, g_mod, g_prim_t, uids, exe_strict) + let res = inject_main(ctx, g_ctx, prog, exe_main, g_mod, g_prim_t, uids, exe_strict, target_triple) LINK_WHOLE_LIB = res.link_whole_lib if (res.fn == null) { finalize_jit(use_dll, gen_exe, g_dynamic_lib_handle) @@ -308,7 +326,13 @@ class JIT_LLVM : AstSimulateMacro { let fn_names = join([ for (fn in funcs); "{fn.name}" ], ",") panic("Internal jit error. Failed to get IR for functions {fn_names}.\n") } - if (gen_exe) { + if (gen_wasm) { + mkdir_rec(dir_name(output_path)) + // path_to_linker is reused as the optional emcc override + // for the wasm link (mirrors how it's used for clang-cl + // on the host path). + write_wasm(g_mod, output_path, target_triple, path_to_linker, jit_has_externals, runtime_lib_override) + } elif (gen_exe) { mkdir_rec(dir_name(output_path)) write_exe(g_mod, output_path, path_to_shared_lib, path_to_linker, LINK_WHOLE_LIB) } elif (use_dll) { diff --git a/modules/dasLLVM/daslib/llvm_targets.das b/modules/dasLLVM/daslib/llvm_targets.das index 2e190ba9a4..44d4264bbf 100644 --- a/modules/dasLLVM/daslib/llvm_targets.das +++ b/modules/dasLLVM/daslib/llvm_targets.das @@ -34,3 +34,14 @@ def LLVMInitializeAllAsmPrinters() { LLVMInitializeARMAsmPrinter() LLVMInitializeX86AsmPrinter() } + +// WebAssembly target — kept separate from LLVMInitializeAll* so the JIT path +// doesn't pay its static-init cost on every startup. Called lazily by the +// wasm32 cross-compile entry point. +def LLVMInitializeWasmTarget() { + LLVMInitializeWebAssemblyTargetInfo() + LLVMInitializeWebAssemblyTarget() + LLVMInitializeWebAssemblyTargetMC() + LLVMInitializeWebAssemblyAsmPrinter() + LLVMInitializeWebAssemblyAsmParser() +} diff --git a/site/README.md b/site/README.md index 1fe9ae463c..d6588fe630 100644 --- a/site/README.md +++ b/site/README.md @@ -35,13 +35,13 @@ site/ ├── news/.html # GENERATED — micro-pages for news entries with bodies ├── playground/ │ ├── index.html # Forge nav + IDE body (mobile gate at top of ) -│ ├── forge-skin.css # CSS overrides on web/ui/src/main.css + mobile notice + splitter +│ ├── forge-skin.css # CSS overrides on web/examples/ui/src/main.css + mobile notice + splitter │ ├── playground-init.js # URL-hash dispatch (#code= legacy / #z= multi-file) │ ├── playground-tabs.js # multi-file state, tab strip, autosave to localStorage │ ├── playground-share.js # ↗ share popover, is.gd shortener │ ├── playground-splitter.js # draggable code | output column splitter -│ ├── samples/ # multi-file sample bundles (gitignored, mirrored from web/ui/samples) -│ └── *.{js,css} # other vendored bits from web/ui/src/ for local-dev (gitignored) +│ ├── samples/ # multi-file sample bundles (gitignored, mirrored from web/examples/ui/samples) +│ └── *.{js,css} # other vendored bits from web/examples/ui/src/ for local-dev (gitignored) ├── tests/ │ └── playground/ # Playwright e2e suite (28 specs, ~5 s no-WASM) └── doc/ # Sphinx HTML output (gitignored, deployed by CI) @@ -162,17 +162,10 @@ The mini playground needs the WASM artifact at `site/files/wasm/`. Build it once (then `pip install`-free; subsequent `ninja` rebuilds are fast): ```bash -cd web/ -bash step0_emsdk_install.sh # ~1 GB into web/emsdk/ -source emsdk/emsdk_env.sh # activate for this shell -mkdir -p build && cd build -cmake -DCMAKE_BUILD_TYPE=Release -G Ninja \ - -DCMAKE_TOOLCHAIN_FILE=../emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake ../ -ninja # 5–15 min first build - -# Drop the artifact where the mini runner expects it: -mkdir -p ../../site/files/wasm -cp ../output/daslang_static.{js,wasm} ../../site/files/wasm/ +# Install emcc (see web/README.md), then: +source ~/emsdk/emsdk_env.sh +emcmake cmake -S web -B web/cmake_temp -G Ninja -DCMAKE_BUILD_TYPE=Release && cmake --build web/cmake_temp +# stage_site_playground auto-drops daslang_static.{js,wasm} into site/files/wasm/. ``` Re-run the http.server. The hero `▶ run` button should now compile and run @@ -181,21 +174,24 @@ re-execute. ## Full playground (`/playground/`) -The full IDE is `web/ui/` (CodeMirror, samples picker, multi-file). For local +The full IDE is `web/examples/ui/` (CodeMirror, samples picker, multi-file). For local preview, vendor its source files into `site/playground/` and copy the sample bundles: ```bash -cp web/ui/src/* site/playground/ -cp -r web/ui/samples site/playground/samples +# stage_site_playground (from the WASM build above) auto-vendors UI + samples +# + daslang_static.{js,wasm} into site/playground/. # Forge skin auto-loads from site/playground/forge-skin.css. # CodeMirror itself + the Forge CM theme load from /files/cm/ (already in site/). ``` -Plus the WASM artifact (same as the mini playground): +### JIT (wasm) engine + +Optional: build precompiled `.wasm` per sample so the playground's "JIT (wasm)" +radio works. `all_wasm` auto-stages them into `site/playground/samples/examples/`. ```bash -cp web/output/daslang_static.{js,wasm} site/playground/ +cmake -B build -DDAS_LLVM_DISABLED=OFF -DDAS_BUILD_WASM=ON && cmake --build build --target all_wasm ``` Open `http://localhost:8000/playground/`. Forge nav at the top, multi-file @@ -243,7 +239,7 @@ python3 site/blog/build_blog.py \ # Playground (assumes web/ has been built — see above) mkdir -p _site/playground _site/files/wasm -cp -r web/ui/src/* _site/playground/ +cp -r web/examples/ui/src/* _site/playground/ cp web/output/daslang_static.{js,wasm} _site/playground/ cp web/output/daslang_static.{js,wasm} _site/files/wasm/ cp site/playground/index.html _site/playground/index.html @@ -301,7 +297,7 @@ no dedicated CI tier yet. WASM artifact yet. Either follow "Mini playground" above, or just preview the static landing without it (the run button is the only thing that won't work). -- **`/playground/` shows a blank page** — you forgot to `cp web/ui/src/* +- **`/playground/` shows a blank page** — you forgot to `cp web/examples/ui/src/* site/playground/`. `main.js`, `main.css`, and `jquery-3.6.0.min.js` are needed; CodeMirror itself lives at `site/files/cm/` and is already in the repo. diff --git a/site/playground/forge-skin.css b/site/playground/forge-skin.css index d08b067d9d..8189fb4e78 100644 --- a/site/playground/forge-skin.css +++ b/site/playground/forge-skin.css @@ -132,6 +132,17 @@ body { border-color: var(--amber-dim) !important; } +.engine-toggle { + display: flex; + gap: 10px; + align-items: center; + font-family: var(--font-mono); + font-size: 12px; + color: var(--fg-dim); +} +.engine-toggle label { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; } +.engine-toggle input[disabled] + * , .engine-toggle label:has(input[disabled]) { opacity: 0.5; cursor: default; } + /* ───── Share popover ───── */ .pg-share { z-index: 100; diff --git a/site/playground/index.html b/site/playground/index.html index 99d0ec581c..16b8588357 100644 --- a/site/playground/index.html +++ b/site/playground/index.html @@ -121,6 +121,10 @@

Open this on a laptop.

+
+ + +
diff --git a/skills/perf_lint.md b/skills/perf_lint.md index ec3d82ff2c..7f02eff51a 100644 --- a/skills/perf_lint.md +++ b/skills/perf_lint.md @@ -126,6 +126,7 @@ After compilation, `Expression._type` is resolved. Check `expr._type.baseType == | PERF018 | `for (i in range(length(arr))) { ... arr[i] ... }` (where `i` only indexes `arr`) | Medium | use `for (c in arr) { ... c ... }`; direct iteration drops the index | | PERF019 | `int(T.a) \| int(T.b)` on the same bitfield (or enum with `operator \|` overload) | Low | collapse to `int(T.a \| T.b)` — one cast instead of two. **Const-foldable forms only fire under lint policies** (`no_optimizations`/`no_infer_time_folding`); dastest validates runtime forms only. Enum-overload probe iterates `program_for_each_module` + `for_each_function(mod, "\|")`, cached per-enum-type on the visitor | | PERF020 | `T(x)` where `x` is already workhorse type `T` (15 names: `int`/`int8`/`int16`/`int64`, `uint`/`uint8`/`uint16`/`uint64`, `float`, `double`, `string`, `bitfield`/`bitfield8`/`bitfield16`/`bitfield64`) | Low | drop the cast — it's a no-op. Match: `call.func.fromGeneric?.name ?? call.func.name` is in the workhorse-cast set AND `arg._type.baseType` equals the cast's target type (Ref/Const/Temp qualifiers ignored). User-named bitfield/enum constructors (`MyBitfield(x)`, `MyEnum(x)`), vector constructors (`int2`/`float3`/…), and `string(das_string)` are out of scope by construction | +| PERF022 | `for (s in A) { B \|> push(s) }` / `push_clone(s)` where body is exactly that one statement, single iter-var, source is `array` / C-array, destination is `array` | Medium | use `B \|> push_from(A)` / `push_clone_from(A)` (bulk reserve+copy in `daslib/builtin.das`). Single-name `push`/`push_clone` is overloaded between single-element and bulk forms (ambiguous when destination is `array`); the `_from` suffix names the bulk intent. Warns at the for-loop's `.at` rather than the inner push's `.at` to avoid colliding with PERF006 under `perf_warning`'s same-location dedup. `emplace` is out of scope: for-iter-var is const-ref, but `emplace` requires var-ref, so the hand-rolled shape doesn't compile. Range/iterator/generator sources are not flagged — no bulk overload to migrate to | ## Visitor gotchas diff --git a/src/builtin/module_jit.cpp b/src/builtin/module_jit.cpp index 2691d27a51..faf067988c 100644 --- a/src/builtin/module_jit.cpp +++ b/src/builtin/module_jit.cpp @@ -35,6 +35,12 @@ #include #include +// MSVC ships popen/pclose under _popen/_pclose; alias once for the whole TU. +#if defined(_WIN32) || defined(_WIN64) + #define popen _popen + #define pclose _pclose +#endif + namespace das { // forward declarations from module_builtin_fio.cpp void * register_dynamic_module(const char *, const char *, int, Context *, LineInfoArg *); @@ -738,6 +744,19 @@ extern "C" { string compilerLibrary; // non-empty when linkWholeLib — exe also needs compiler lib }; + // Resolve a linker / tool path. Lookup order: + // 1. `custom` — explicit override (non-null, non-empty) + // 2. `/bin/` if present on disk + // 3. `fallback` — bare name picked up via PATH + // Callers pass the platform-specific bundled name (e.g. `wasm-ld.exe` on + // Windows, `wasm-ld` on POSIX, `emcc.bat` on Windows, etc.). + static string find_linker(const char * custom, const string & bundledName, const string & fallback) { + if ( custom != nullptr && custom[0] != '\0' ) return custom; + string bundled = getDasRoot() + "/bin/" + bundledName; + if ( check_file_present(bundled.c_str()) ) return bundled; + return fallback; + } + static LinkerPaths get_real_lib_linker_paths(const char * dasLib, const char * customLinker, bool isShared, bool linkWholeLib) { LinkerPaths result; result.linker = customLinker != nullptr ? customLinker : ""; @@ -746,7 +765,7 @@ extern "C" { if (result.linker.empty() || result.runtimeLibrary.empty()) { #if defined(_WIN32) || defined(_WIN64) if (result.linker.empty()) { - result.linker = getDasRoot() + "/bin/clang-cl.exe"; + result.linker = find_linker(nullptr, "clang-cl.exe", "clang-cl"); } if (result.runtimeLibrary.empty()) { const auto path = get_prefix(getExecutableFileName()); @@ -780,6 +799,45 @@ extern "C" { } #if (defined(_MSC_VER) || defined(__linux__) || defined(__APPLE__)) && !defined(_GAMING_XBOX) && !defined(_DURANGO) + // Run a fully-formed linker command via popen, capture combined stdout/ + // stderr into a 16KB buffer, then log success or a failure diagnostic + // (with the captured output) through the daslang Context. + // cmd — complete shell command, already includes 2>&1. + // artifactPath — output file path (logged on success/failure). + // artifactKind — short label for messages ("Library", "Wasm", ...). + static void run_link_cmd(const char * cmd, const char * artifactPath, + const char * artifactKind, Context * context) { + FILE * fp = popen(cmd, "r"); + if ( fp == NULL ) { + LOG(LogLevel::error) << "Failed to run command '" << cmd << "'\n"; + return; + } + static constexpr int MAX_OUTPUT_SIZE = 16 * 1024; + char buffer[1024], output[MAX_OUTPUT_SIZE]; + output[0] = '\0'; + size_t output_length = 0; + while ( fgets(buffer, sizeof(buffer), fp) != NULL ) { + size_t buffer_length = strlen(buffer); + if ( output_length + buffer_length < MAX_OUTPUT_SIZE ) { + strcat(output, buffer); + output_length += buffer_length; + } else { + strncat(output, buffer, MAX_OUTPUT_SIZE - output_length - 1); + break; + } + } + auto li = LineInfo(); + if ( int status = pclose(fp); status != 0 ) { + string msg = string("Failed to link ") + artifactKind + " " + artifactPath + ", command '" + cmd + "'\n"; + context->to_out(&li, LogLevel::error, msg.c_str()); + string err = string("Output:\n") + output; + context->to_out(&li, LogLevel::error, err.c_str()); + } else { + string msg = string(artifactKind) + " " + artifactPath + " linked - ok\n"; + context->to_out(&li, LogLevel::info, msg.c_str()); + } + } + void create_shared_library ( const char * objFilePath, const char * libraryName, [[maybe_unused]] const char * dasLib, const char * customLinker, bool isShared, bool linkWholeLib, Context *context ) { char cmd[1024]; const auto paths = get_real_lib_linker_paths(dasLib, customLinker, isShared, linkWholeLib); @@ -829,50 +887,51 @@ extern "C" { linker, linkerParam, rpath, libraryName, objFilePath, runtimeLibrary, compilerLibrary); #endif *result = '\0'; - -#if defined(_WIN32) || defined(_WIN64) - #define popen _popen - #define pclose _pclose + run_link_cmd(cmd, libraryName, "Library", context); + } +#else + void create_shared_library ( const char * objFilePath, const char * libraryName, [[maybe_unused]] const char * dasLib, const char * customLinker, bool isShared, bool linkWholeLib, Context *context ) { } #endif - FILE * fp = popen(cmd, "r"); - if ( fp == NULL ) { - LOG(LogLevel::error) << "Failed to run command '" << cmd << "'\n"; +#if (defined(_MSC_VER) || defined(__linux__) || defined(__APPLE__)) && !defined(_GAMING_XBOX) && !defined(_DURANGO) + // Cross-compile-link a wasm32 object into a standalone .wasm via emcc. + // runtimeLibPath — path to libDaScript_runtime.a (wasm32). Optional: + // present → link runtime symbols (das_*, malloc, memcpy, …) in. + // missing → link without; only safe for pure programs that touch + // no daslang runtime. + // customEmcc — explicit emcc override (nullptr/empty = resolve via + // bin/ then PATH). + // emcc drives wasm-ld plus libc / wasi / wasm-exceptions glue, so output + // is self-contained -sSTANDALONE_WASM (only wasi imports). + void link_wasm ( const char * objFilePath, const char * wasmPath, + const char * runtimeLibPath, const char * customEmcc, + Context * context ) { + #if defined(_WIN32) || defined(_WIN64) + const auto linker = find_linker(customEmcc, "emcc.bat", "emcc"); + #else + const auto linker = find_linker(customEmcc, "emcc", "emcc"); + #endif + if ( !check_file_present(objFilePath) ) { + LOG(LogLevel::error) << "File '" << objFilePath << "' , containing wasm32 object, does not exist\n"; return; } - - static constexpr int MAX_OUTPUT_SIZE = 16 * 1024; - - char buffer[1024], output[MAX_OUTPUT_SIZE]; - output[0] = '\0'; - - size_t output_length = 0; - - // Read the output a line at a time and accumulate it - while (fgets(buffer, sizeof(buffer), fp) != NULL) { - size_t buffer_length = strlen(buffer); - if (output_length + buffer_length < MAX_OUTPUT_SIZE) { - strcat(output, buffer); - output_length += buffer_length; - } else { - strncat(output, buffer, MAX_OUTPUT_SIZE - output_length - 1); - break; - } - } - - auto li = LineInfo(); - if ( int status = pclose(fp); status != 0 ) { - string msg = string("Failed to make shared library ") + libraryName + ", command '" + cmd + "'\n"; - context->to_out(&li, LogLevel::error, msg.c_str()); - string err = string("Output:\n") + output; - context->to_out(&li, LogLevel::error, err.c_str()); - } else { - string msg = string("Library ") + libraryName + " made - ok\n"; - context->to_out(&li, LogLevel::info, msg.c_str()); + const bool withRuntime = runtimeLibPath != nullptr && runtimeLibPath[0] != '\0' + && check_file_present(runtimeLibPath); + if ( runtimeLibPath != nullptr && runtimeLibPath[0] != '\0' && !withRuntime ) { + LOG(LogLevel::warning) << "libDaScript_runtime archive '" << runtimeLibPath + << "' not found - linking without; runtime refs will fail\n"; } + // -sSTANDALONE_WASM: emit self-contained .wasm with wasi imports only. + // -fwasm-exceptions + -sWASM_LEGACY_EXCEPTIONS=0: match the runtime + // archive's modern wasm EH, avoid emcc's JS invoke_* trampolines. + const string runtimeArg = withRuntime ? fmt::format("\"{}\" ", runtimeLibPath) : ""; + const string cmd = fmt::format( + FMT_STRING("\"{}\" \"{}\" {}-o \"{}\" -sSTANDALONE_WASM -fwasm-exceptions -sWASM_LEGACY_EXCEPTIONS=0 2>&1"), + linker, objFilePath, runtimeArg, wasmPath); + run_link_cmd(cmd.c_str(), wasmPath, "Wasm", context); } #else - void create_shared_library ( const char * objFilePath, const char * libraryName, [[maybe_unused]] const char * dasLib, const char * customLinker, bool isShared, bool linkWholeLib, Context *context ) { } + void link_wasm ( const char *, const char *, const char *, const char *, Context * ) { } #endif void jit_set_jit_state(Context & context, void *shared_lib, void *llvm_ee, void *llvm_context) { @@ -1017,6 +1076,9 @@ extern "C" { addExtern(*this, lib, "create_shared_library", SideEffects::worstDefault, "create_shared_library") ->args({"objFilePath","libraryName","dasLib","customLinker", "isShared", "linkWholeLib", "context"}); + addExtern(*this, lib, "link_wasm", + SideEffects::worstDefault, "link_wasm") + ->args({"objFilePath","wasmPath","runtimeLibPath","customEmcc","context"}); addExtern(*this, lib, "set_jit_state", SideEffects::worstDefault, "jit_set_jit_state") ->args({"context","shared_lib","llvm_ee","llvm_ctx"}); diff --git a/src/misc/sysos.cpp b/src/misc/sysos.cpp index 7271e5e001..2e5ae24397 100644 --- a/src/misc/sysos.cpp +++ b/src/misc/sysos.cpp @@ -189,9 +189,16 @@ return false; } size_t getExecutablePathName(char* pathName, size_t pathNameCapacity) { +#if defined(_EMSCRIPTEN_VER) + // wasm-standalone has no /proc/self/exe; readlink would import + // env::__syscall_readlinkat which wasi-only hosts can't satisfy. + if (pathNameCapacity > 0) pathName[0] = '\0'; + return 0; +#else size_t pathNameSize = readlink("/proc/self/exe", pathName, pathNameCapacity - 1); pathName[pathNameSize] = '\0'; return pathNameSize; +#endif } void * loadDynamicLibrary ( const char * lib ) { // RTLD_GLOBAL so symbols from loaded .shared_module files diff --git a/src/parser/ds2_lexer.cpp b/src/parser/ds2_lexer.cpp index fd1b5a5243..df733ae3d2 100644 --- a/src/parser/ds2_lexer.cpp +++ b/src/parser/ds2_lexer.cpp @@ -1,6 +1,6 @@ -#line 1 "ds2_lexer.cpp" +#line 2 "ds2_lexer.cpp" -#line 3 "ds2_lexer.cpp" +#line 4 "ds2_lexer.cpp" #define YY_INT_ALIGNED short int @@ -1219,11 +1219,11 @@ void das_accept_cpp_comment ( vector & crdi, yyscan_t scanner, #define YY_EXTRA_TYPE das::DasParserState * -#line 1222 "ds2_lexer.cpp" +#line 1223 "ds2_lexer.cpp" #define YY_NO_UNISTD_H 1 /* %option debug */ -#line 1226 "ds2_lexer.cpp" +#line 1227 "ds2_lexer.cpp" #define INITIAL 0 #define normal 1 @@ -1508,7 +1508,7 @@ YY_DECL #line 78 "ds2_lexer.lpp" -#line 1511 "ds2_lexer.cpp" +#line 1512 "ds2_lexer.cpp" while ( /*CONSTCOND*/1 ) /* loops until end-of-file is reached */ { @@ -3261,7 +3261,7 @@ YY_RULE_SETUP #line 786 "ds2_lexer.lpp" ECHO; YY_BREAK -#line 3264 "ds2_lexer.cpp" +#line 3265 "ds2_lexer.cpp" case YY_STATE_EOF(INITIAL): case YY_STATE_EOF(include): yyterminate(); diff --git a/src/parser/ds_lexer.cpp b/src/parser/ds_lexer.cpp index b6de502ddf..12770a8227 100644 --- a/src/parser/ds_lexer.cpp +++ b/src/parser/ds_lexer.cpp @@ -1,6 +1,6 @@ -#line 1 "ds_lexer.cpp" +#line 2 "ds_lexer.cpp" -#line 3 "ds_lexer.cpp" +#line 4 "ds_lexer.cpp" #define YY_INT_ALIGNED short int @@ -1328,11 +1328,11 @@ void das_accept_cpp_comment ( vector & crdi, yyscan_t scanner, #define YY_EXTRA_TYPE das::DasParserState * -#line 1331 "ds_lexer.cpp" +#line 1332 "ds_lexer.cpp" #define YY_NO_UNISTD_H 1 /* %option debug */ -#line 1335 "ds_lexer.cpp" +#line 1336 "ds_lexer.cpp" #define INITIAL 0 #define indent 1 @@ -1617,7 +1617,7 @@ YY_DECL #line 77 "ds_lexer.lpp" -#line 1620 "ds_lexer.cpp" +#line 1621 "ds_lexer.cpp" while ( /*CONSTCOND*/1 ) /* loops until end-of-file is reached */ { @@ -3834,7 +3834,7 @@ YY_RULE_SETUP #line 1134 "ds_lexer.lpp" ECHO; YY_BREAK -#line 3837 "ds_lexer.cpp" +#line 3838 "ds_lexer.cpp" case YY_STATE_EOF(INITIAL): case YY_STATE_EOF(include): yyterminate(); diff --git a/src/parser/lex.yy.h b/src/parser/lex.yy.h index 8753d9daad..115acc77ad 100644 --- a/src/parser/lex.yy.h +++ b/src/parser/lex.yy.h @@ -2,9 +2,9 @@ #define das_yyHEADER_H 1 #define das_yyIN_HEADER 1 -#line 5 "lex.yy.h" +#line 6 "lex.yy.h" -#line 7 "lex.yy.h" +#line 8 "lex.yy.h" #define YY_INT_ALIGNED short int @@ -723,6 +723,6 @@ extern int yylex \ #line 1134 "ds_lexer.lpp" -#line 726 "lex.yy.h" +#line 727 "lex.yy.h" #undef das_yyIN_HEADER #endif /* das_yyHEADER_H */ diff --git a/src/parser/lex2.yy.h b/src/parser/lex2.yy.h index 64dbe1ebb3..4b370cd3b3 100644 --- a/src/parser/lex2.yy.h +++ b/src/parser/lex2.yy.h @@ -2,9 +2,9 @@ #define das2_yyHEADER_H 1 #define das2_yyIN_HEADER 1 -#line 5 "lex2.yy.h" +#line 6 "lex2.yy.h" -#line 7 "lex2.yy.h" +#line 8 "lex2.yy.h" #define YY_INT_ALIGNED short int @@ -723,6 +723,6 @@ extern int yylex \ #line 786 "ds2_lexer.lpp" -#line 726 "lex2.yy.h" +#line 727 "lex2.yy.h" #undef das2_yyIN_HEADER #endif /* das2_yyHEADER_H */ diff --git a/tests/language/array.das b/tests/language/array.das index 902bf3628b..894861c44b 100644 --- a/tests/language/array.das +++ b/tests/language/array.das @@ -95,7 +95,7 @@ def testIntArray(t : T?) { } def testInterop(t : T?) { - let res = temp_array_example([ "one" ]) <| $(arr) { + let res = temp_array_example([ "one" ]) $(arr) { t |> equal(length(arr), 1) t |> equal(arr[0], "one") } @@ -161,6 +161,7 @@ def test_array(t : T?) { testConstInArray() testInterop(t) testMultiPush(t) + testMultiPushFrom(t) } // this test bellow is only here to make sure AOT compiles and runs for array @@ -172,14 +173,10 @@ struct Foo { [sideeffects] def testAccept(t : array; e : Foo const[10]) { for (i in t) { - if (i.bar == 13) { - return true - } + return true if (i.bar == 13) } for (j in e) { - if (j.bar == 13) { - return false - } + return false if (j.bar == 13) } return false } @@ -219,3 +216,48 @@ def testMultiPush(t : T?) { t |> equal(length(b), 4) t |> equal(b[3].bar, 3) } + +[sideeffects] +def testMultiPushFrom(t : T?) { + // 1. push_from(arr, array source) + var a <- [Foo(bar=10), Foo(bar=20)] + var b : array + push_from(b, a) + t |> equal(length(a), 2) + t |> equal(length(b), 2) + t |> equal(b[0].bar, 10) + t |> equal(b[1].bar, 20) + + // 2. push_from(arr, C-array source) — also exercises Arr-already-has-elements reserve + var c = fixed_array(Foo(bar = 30), Foo(bar = 40)) + push_from(b, c) + t |> equal(length(b), 4) + t |> equal(b[2].bar, 30) + t |> equal(b[3].bar, 40) + + // 3. push_clone_from(arr, array source) — the array gap-fill + var d : array + push_clone_from(d, a) + t |> equal(length(d), 2) + t |> equal(d[0].bar, 10) + t |> equal(d[1].bar, 20) + + // 4. push_clone_from(arr, C-array source) + push_clone_from(d, c) + t |> equal(length(d), 4) + t |> equal(d[2].bar, 30) + t |> equal(d[3].bar, 40) + + // 5. emplace_from(arr, array source) — the array gap-fill + var e : array + emplace_from(e, a) + t |> equal(length(e), 2) + t |> equal(e[0].bar, 10) + t |> equal(e[1].bar, 20) + + // 6. emplace_from(arr, C-array source) + emplace_from(e, c) + t |> equal(length(e), 4) + t |> equal(e[2].bar, 30) + t |> equal(e[3].bar, 40) +} diff --git a/tests/linq/test_linq_fold.das b/tests/linq/test_linq_fold.das index 6092087db8..befcb47795 100644 --- a/tests/linq/test_linq_fold.das +++ b/tests/linq/test_linq_fold.das @@ -1724,6 +1724,21 @@ def test_distinct_extended_parity(t : T?) { t |> run("distinct.long_count: [1,2,2,3,3,3] → int64 3") @(tt : T?) { tt |> equal(3l, _fold([1, 2, 2, 3, 3, 3].to_sequence().distinct().long_count())) } + // D8: predicate-form terminators must NOT be silently dropped — plan_distinct guards arg count and cascades to tier 2. + t |> run("distinct.count(pred) parity: [1,1,2,2,3,3,4,4,5,5].distinct.count(>2) → 3 (not 5)") @(tt : T?) { + let arr = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] + let got = _fold(arr.to_sequence().distinct().count($(x : int) => x > 2)) + let raw = arr.to_sequence().distinct().count($(x : int) => x > 2) + tt |> equal(got, raw, "predicate must be honored, not dropped") + tt |> equal(got, 3) + } + t |> run("distinct.long_count(pred) parity: [1,1,2,2,3,3,4,4,5,5].distinct.long_count(>=3) → 3l") @(tt : T?) { + let arr = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] + let got = _fold(arr.to_sequence().distinct().long_count($(x : int) => x >= 3)) + let raw = arr.to_sequence().distinct().long_count($(x : int) => x >= 3) + tt |> equal(got, raw, "predicate must be honored, not dropped") + tt |> equal(got, 3l) + } } [test] diff --git a/tests/linq/test_linq_from_decs.das b/tests/linq/test_linq_from_decs.das index 3dd9f41004..114ada6536 100644 --- a/tests/linq/test_linq_from_decs.das +++ b/tests/linq/test_linq_from_decs.das @@ -2070,3 +2070,302 @@ def test_unroll5d_reverse_count_splice_shape(t : T?) { } } +// ───────────────────────────────────────────────────────────────────────────── +// Slice 5c: distinct / distinct_by on decs (streaming dedup via hoisted table) +// ───────────────────────────────────────────────────────────────────────────── + +[decs_template(prefix = "unroll5c_")] +struct Unroll5cRow { + id : int + brand : int + price : int +} + +def private fixture_unroll5c() { + restart() + // 12 rows. brand cycles 0..3 (so 4 distinct brands). price = id*10 (12 distinct values). + for (i in range(12)) { + create_entity() @(eid, cmp) { + cmp.eid := eid + cmp.unroll5c_id := i + cmp.unroll5c_brand := i % 4 + cmp.unroll5c_price := i * 10 + } + } + commit() +} + +// ── distinct |> to_array (bare select + distinct) ── + +[export, marker(no_coverage)] +def target_unroll5c_select_distinct_to_array_fold() : array { + return <- _fold(from_decs_template(type)._select(_.brand).distinct().to_array()) +} + +[test] +def test_unroll5c_select_distinct_to_array_parity(t : T?) { + fixture_unroll5c() + let got <- target_unroll5c_select_distinct_to_array_fold() + t |> equal(got.length(), 4, "4 distinct brands (0..3)") + // Brands cycle 0,1,2,3,0,1,2,3,0,1,2,3 → first-seen order is 0,1,2,3. + let expected = [0, 1, 2, 3] + for (i in range(4)) { + t |> equal(got[i], expected[i]) + } +} + +[test] +def test_unroll5c_select_distinct_to_array_empty(t : T?) { + restart() + commit() + let got <- target_unroll5c_select_distinct_to_array_fold() + t |> equal(got.length(), 0, "empty decs → empty distinct") +} + +// ── distinct |> count ── + +[export, marker(no_coverage)] +def target_unroll5c_select_distinct_count_fold() : int { + return _fold(from_decs_template(type)._select(_.brand).distinct().count()) +} + +[test] +def test_unroll5c_select_distinct_count_parity(t : T?) { + fixture_unroll5c() + t |> equal(target_unroll5c_select_distinct_count_fold(), 4, "length(seen) = 4 distinct brands") +} + +// ── distinct |> long_count ── + +[export, marker(no_coverage)] +def target_unroll5c_select_distinct_long_count_fold() : int64 { + return _fold(from_decs_template(type)._select(_.brand).distinct().long_count()) +} + +[test] +def test_unroll5c_select_distinct_long_count_parity(t : T?) { + fixture_unroll5c() + t |> equal(target_unroll5c_select_distinct_long_count_fold(), 4l, "int64(length(seen)) = 4") +} + +// ── distinct |> sum ── + +[export, marker(no_coverage)] +def target_unroll5c_select_distinct_sum_fold() : int { + return _fold(from_decs_template(type)._select(_.brand).distinct().sum()) +} + +[test] +def test_unroll5c_select_distinct_sum_parity(t : T?) { + fixture_unroll5c() + // sum of distinct brands {0,1,2,3} = 6 + t |> equal(target_unroll5c_select_distinct_sum_fold(), 6, "acc folds at fresh-key site only") +} + +// ── distinct |> take(N) |> to_array (streaming early-exit) ── + +[export, marker(no_coverage)] +def target_unroll5c_select_distinct_take2_fold() : array { + return <- _fold(from_decs_template(type)._select(_.brand).distinct().take(2).to_array()) +} + +[test] +def test_unroll5c_select_distinct_take_parity(t : T?) { + fixture_unroll5c() + let got <- target_unroll5c_select_distinct_take2_fold() + // First 2 distinct in source order: 0, 1. + t |> equal(got.length(), 2) + t |> equal(got[0], 0) + t |> equal(got[1], 1) +} + +[test] +def test_unroll5c_select_distinct_take_zero(t : T?) { + fixture_unroll5c() + let got <- _fold(from_decs_template(type)._select(_.brand).distinct().take(0).to_array()) + t |> equal(got.length(), 0, "take(0) yields empty") +} + +[test] +def test_unroll5c_select_distinct_take_beyond(t : T?) { + fixture_unroll5c() + let got <- _fold(from_decs_template(type)._select(_.brand).distinct().take(99).to_array()) + t |> equal(got.length(), 4, "take(N>num_distinct) returns all distinct") +} + +// ── where + distinct |> to_array ── + +[export, marker(no_coverage)] +def target_unroll5c_where_distinct_to_array_fold() : array { + return <- _fold(from_decs_template(type)._where(_.id < 8)._select(_.brand).distinct().to_array()) +} + +[test] +def test_unroll5c_where_distinct_parity(t : T?) { + fixture_unroll5c() + let got <- target_unroll5c_where_distinct_to_array_fold() + // id<8 → ids 0..7 → brands 0,1,2,3,0,1,2,3 → distinct = 4. + t |> equal(got.length(), 4) +} + +// ── distinct_by(key) |> to_array ── + +[export, marker(no_coverage)] +def target_unroll5c_distinct_by_to_array_fold() : array> { + return <- _fold(from_decs_template(type)._distinct_by(_.brand).to_array()) +} + +[test] +def test_unroll5c_distinct_by_parity(t : T?) { + fixture_unroll5c() + let got <- target_unroll5c_distinct_by_to_array_fold() + // First row per distinct brand: ids 0,1,2,3 (brands 0,1,2,3). + t |> equal(got.length(), 4) + for (i in range(4)) { + t |> equal(got[i].id, i) + t |> equal(got[i].brand, i) + } +} + +// ── distinct_by(key) |> count ── + +[export, marker(no_coverage)] +def target_unroll5c_distinct_by_count_fold() : int { + return _fold(from_decs_template(type)._distinct_by(_.brand).count()) +} + +[test] +def test_unroll5c_distinct_by_count_parity(t : T?) { + fixture_unroll5c() + t |> equal(target_unroll5c_distinct_by_count_fold(), 4) +} + +// ── distinct_by(key) |> take(N) |> to_array ── + +[export, marker(no_coverage)] +def target_unroll5c_distinct_by_take2_fold() : array> { + return <- _fold(from_decs_template(type)._distinct_by(_.brand).take(2).to_array()) +} + +[test] +def test_unroll5c_distinct_by_take_parity(t : T?) { + fixture_unroll5c() + let got <- target_unroll5c_distinct_by_take2_fold() + t |> equal(got.length(), 2) + t |> equal(got[0].brand, 0) + t |> equal(got[1].brand, 1) +} + +// ── Side-effect single-eval guard: take(N) limit binds once, projection binds once per element ── + +var g_unroll5c_take_arg_calls : int = 0 +def trace_take_arg() : int { + g_unroll5c_take_arg_calls ++ + return 2 +} + +[test] +def test_unroll5c_distinct_take_arg_evaluated_once(t : T?) { + fixture_unroll5c() + g_unroll5c_take_arg_calls = 0 + let got <- _fold(from_decs_template(type)._select(_.brand).distinct().take(trace_take_arg()).to_array()) + t |> equal(g_unroll5c_take_arg_calls, 1, "take(N) arg must evaluate exactly once at invoke entry, not per fresh-key check") + t |> equal(got.length(), 2, "result still matches the bound N") +} + +// ── AST shape gates ── + +[test] +def test_unroll5c_select_distinct_count_splice_shape(t : T?) { + ast_gc_guard() { + var func = find_module_function_via_rtti(compiling_module(), @@target_unroll5c_select_distinct_count_fold) + t |> success(func != null, "RTTI must resolve target") + if (func == null) return + var body_expr : ExpressionPtr + let r = qmatch_function(func) $() { + return <- $e(body_expr) + } + t |> success(r.matched, "qmatch must capture body") + t |> equal(describe_count(body_expr, "to_sequence"), 0, "distinct+count splice must NOT fall to tier-2 to_sequence") + t |> equal(describe_count(body_expr, "for_each_archetype"), 1, "exactly one for_each_archetype (no _find — no take present)") + t |> equal(describe_count(body_expr, "for_each_archetype_find"), 0, "no take → no _find") + t |> equal(describe_count(body_expr, "decs_buf"), 0, "count terminator elides the buffer") + t |> success(describe_count(body_expr, "decs_seen") >= 2, "seen-table declared + queried") + t |> equal(describe_count(body_expr, "key_exists"), 1, "single fresh-key check per element") + } +} + +[test] +def test_unroll5c_select_distinct_take_splice_shape(t : T?) { + ast_gc_guard() { + var func = find_module_function_via_rtti(compiling_module(), @@target_unroll5c_select_distinct_take2_fold) + t |> success(func != null, "RTTI must resolve target") + if (func == null) return + var body_expr : ExpressionPtr + let r = qmatch_function(func) $() { + return <- $e(body_expr) + } + t |> success(r.matched, "qmatch must capture body") + t |> equal(describe_count(body_expr, "to_sequence"), 0, "distinct+take splice must NOT fall to tier-2 to_sequence") + // take present → outer iteration switches to _find so take-cap can stop across archetypes. + t |> equal(describe_count(body_expr, "for_each_archetype_find"), 1, "take present → for_each_archetype_find") + t |> success(describe_count(body_expr, "decs_buf") >= 2, "buffer needed for to_array terminator") + t |> success(describe_count(body_expr, "decs_seen") >= 2, "seen-table declared + queried") + t |> success(describe_count(body_expr, "decs_taken") >= 2, "taken counter declared + incremented") + } +} + +[test] +def test_unroll5c_distinct_by_to_array_splice_shape(t : T?) { + ast_gc_guard() { + var func = find_module_function_via_rtti(compiling_module(), @@target_unroll5c_distinct_by_to_array_fold) + t |> success(func != null, "RTTI must resolve target") + if (func == null) return + var body_expr : ExpressionPtr + let r = qmatch_function(func) $() { + return <- $e(body_expr) + } + t |> success(r.matched, "qmatch must capture body") + t |> equal(describe_count(body_expr, "to_sequence"), 0, "distinct_by splice must NOT fall to tier-2") + t |> equal(describe_count(body_expr, "for_each_archetype"), 1, "exactly one for_each_archetype") + t |> equal(describe_count(body_expr, "for_each_archetype_find"), 0, "no take → no _find") + // The key lambda invocation is the distinct_by signature — invoke the key block to compute the key, then unique_key wraps it. + t |> success(describe_count(body_expr, "unique_key") >= 1, "distinct_by routes key through unique_key") + } +} + +[test] +def test_unroll5c_select_distinct_sum_splice_shape(t : T?) { + ast_gc_guard() { + var func = find_module_function_via_rtti(compiling_module(), @@target_unroll5c_select_distinct_sum_fold) + t |> success(func != null, "RTTI must resolve target") + if (func == null) return + var body_expr : ExpressionPtr + let r = qmatch_function(func) $() { + return <- $e(body_expr) + } + t |> success(r.matched, "qmatch must capture body") + t |> equal(describe_count(body_expr, "to_sequence"), 0, "distinct+sum splice must NOT fall to tier-2") + t |> equal(describe_count(body_expr, "for_each_archetype"), 1, "no take → plain for_each_archetype") + t |> equal(describe_count(body_expr, "decs_buf"), 0, "sum terminator elides the buffer") + t |> success(describe_count(body_expr, "decs_acc") >= 2, "accumulator declared + folded") + } +} + +// ── Predicate-form terminator regression (Copilot review on PR #2785) — plan_decs_distinct bails on the predicate overload (arg-count guard at the terminator pop); plan_distinct must also bail (post-fix in the same PR). The chain must NOT silently drop the predicate — pre-fix this returned `length(seen)` ignoring the predicate. ── + +[test] +def test_unroll5c_select_distinct_count_pred_parity(t : T?) { + fixture_unroll5c() + // brands cycle 0..3 (4 distinct). distinct |> count(>=2) → only brands 2 and 3 → 2. + let got = _fold(from_decs_template(type)._select(_.brand).distinct().count($(x : int) => x >= 2)) + t |> equal(got, 2, "predicate must be honored, not dropped") +} + +[test] +def test_unroll5c_select_distinct_long_count_pred_parity(t : T?) { + fixture_unroll5c() + let got = _fold(from_decs_template(type)._select(_.brand).distinct().long_count($(x : int) => x >= 2)) + t |> equal(got, 2l, "predicate must be honored, not dropped") +} + diff --git a/tutorials/language/06_arrays.das b/tutorials/language/06_arrays.das index abeed7bc2f..4e586b4fc9 100644 --- a/tutorials/language/06_arrays.das +++ b/tutorials/language/06_arrays.das @@ -107,6 +107,22 @@ def main { clear(buf) // removes all elements print("after clear: length = {length(buf)}\n") + // === Bulk append: push_from / push_clone_from / emplace_from === + // Element-at-a-time loops are noise. Use the bulk *_from overloads when + // copying every element of a source array — they reserve the combined + // capacity once. + + var prefix <- [1, 2, 3] + var suffix <- [4, 5] + var combined : array + combined |> push_from(prefix) + combined |> push_from(suffix) + print("combined: ") + for (c in combined) { + print("{c} ") + } + print("\n") + // === Array comprehension === // Concise syntax to build arrays from expressions. // [for (var in range); expression] @@ -145,5 +161,6 @@ def main { // source length after move: 0 // resized buf: 0 0 0 0 0 // after clear: length = 0 +// combined: 1 2 3 4 5 // squares: 0 1 4 9 16 25 // evens: 0 2 4 6 8 diff --git a/utils/daspkg/index.das b/utils/daspkg/index.das index 48a7f02974..94561b580d 100644 --- a/utils/daspkg/index.das +++ b/utils/daspkg/index.das @@ -398,10 +398,7 @@ def read_manifest(pkg_dir : string; var manifest : PackageManifest) : bool { manifest.author = meta.author manifest.license = meta.license manifest.min_sdk = meta.min_sdk - manifest.tags |> reserve(length(meta.tags)) - for (tag in meta.tags) { - manifest.tags |> push_clone(tag) - } + manifest.tags |> push_clone_from(meta.tags) if (empty(manifest.name)) { to_log(LOG_ERROR, "Error: .das_package missing package_name()\n") return false @@ -512,14 +509,8 @@ def private manifest_to_entry(manifest : PackageManifest; url : string) : IndexE license = manifest.license, min_sdk = manifest.min_sdk, has_native = manifest.has_native) - entry.tags |> reserve(length(manifest.tags)) - for (t in manifest.tags) { - entry.tags |> push_clone(t) - } - entry.dependencies |> reserve(length(manifest.dependencies)) - for (d in manifest.dependencies) { - entry.dependencies |> push_clone(d) - } + entry.tags |> push_clone_from(manifest.tags) + entry.dependencies |> push_clone_from(manifest.dependencies) return <- entry } @@ -933,18 +924,12 @@ def cmd_update_index(root : string) : int { } if (!empty(manifest.tags)) { delete index[name].tags - index[name].tags |> reserve(length(manifest.tags)) - for (t in manifest.tags) { - index[name].tags |> push_clone(t) - } + index[name].tags |> push_clone_from(manifest.tags) } index[name].has_native = manifest.has_native if (!empty(manifest.dependencies)) { delete index[name].dependencies - index[name].dependencies |> reserve(length(manifest.dependencies)) - for (d in manifest.dependencies) { - index[name].dependencies |> push_clone(d) - } + index[name].dependencies |> push_clone_from(manifest.dependencies) } } diff --git a/utils/detect-dupe/main.das b/utils/detect-dupe/main.das index debfc7f874..660ebcf0fa 100644 --- a/utils/detect-dupe/main.das +++ b/utils/detect-dupe/main.das @@ -101,10 +101,7 @@ struct Config { def resolve_against_files(against : array; from_stdin : bool; var out_set : table) { var raw : array - raw |> reserve(length(against)) - for (p in against) { - raw |> push(p) - } + raw |> push_from(against) if (from_stdin) { read_path_lines(fstdin(), raw) } @@ -389,10 +386,7 @@ def main() { } } else { var seeds : array - seeds |> reserve(length(cfg.path)) - for (p in cfg.path) { - seeds |> push(p) - } + seeds |> push_from(cfg.path) if (!empty(cfg.paths_from)) { var perr : string if (!read_path_lines_from_file(cfg.paths_from, seeds, perr)) { diff --git a/utils/detect-dupe/test_detect_dupe.das b/utils/detect-dupe/test_detect_dupe.das index 3e0799b243..eeb3fcf1b6 100644 --- a/utils/detect-dupe/test_detect_dupe.das +++ b/utils/detect-dupe/test_detect_dupe.das @@ -684,9 +684,7 @@ def test_chunk_files_contiguous_and_complete(t : T?) { var rebuilt : array rebuilt |> reserve(11) for (c in chunks) { - for (s in c) { - rebuilt |> push(s) - } + rebuilt |> push_from(c) } t |> equal(length(rebuilt), 11) for (i in range(11)) { diff --git a/utils/lint/tests/perf006_push_without_reserve.das b/utils/lint/tests/perf006_push_without_reserve.das index 57d3435321..a13e84f76f 100644 --- a/utils/lint/tests/perf006_push_without_reserve.das +++ b/utils/lint/tests/perf006_push_without_reserve.das @@ -47,7 +47,7 @@ def good_emplace_while() { def bad_push_clone() { var source <- [1, 2, 3] var dest : array - for (x in source) { + for (x in source) { // nolint:PERF022 — fixture targets PERF006 dest |> push_clone(x) // PERF006 } delete source @@ -119,7 +119,7 @@ def bad_push_array_source() { def bad_push_fixed_array_source() { let source = fixed_array(1, 2, 3) var result : array - for (x in source) { + for (x in source) { // nolint:PERF022 — fixture targets PERF006 result |> push(x) // PERF006 — fixed array = known length } delete result diff --git a/utils/lint/tests/perf022_for_push_bulk.das b/utils/lint/tests/perf022_for_push_bulk.das new file mode 100644 index 0000000000..6304867ce3 --- /dev/null +++ b/utils/lint/tests/perf022_for_push_bulk.das @@ -0,0 +1,127 @@ +options gen2 +// PERF022: for (s in A) { B |> push(s) } — use bulk *_from +// +// Problem: +// Hand-rolled element-at-a-time append is overloaded against the bulk form +// on the same name (push / push_clone), and silently skips the capacity +// reserve. The bulk `*_from` overloads make intent explicit and reserve up +// front. +// +// Bad pattern: +// for (s in A) { +// B |> push(s) +// } +// +// Good pattern: +// B |> push_from(A) +// +// Compiler folds `B |> push(s)`, `B.push(s)`, and `push(B, s)` to the same +// ExprCall, so the single detector catches all three forms. +// +// Scope: push and push_clone only. `emplace` from a for-loop iter var +// doesn't compile (iter var is const-ref, emplace requires var ref), so the +// rule never has a chance to fire on emplace. The `emplace_from` bulk API +// still exists in daslib/builtin.das for direct bulk-move calls. + +expect 31208:6 + +require daslib/perf_lint + +struct Foo { + bar : int +} + +// --- Positives: 6 simple-shape sites (2 calls × 3 syntactic forms) --- + +def bad_push_pipe(src : array; var dst : array) { + for (s in src) { + dst |> push(s) // nolint:PERF006 + } +} + +def bad_push_dot(src : array; var dst : array) { + for (s in src) { + dst.push(s) // nolint:PERF006 + } +} + +def bad_push_plain(src : array; var dst : array) { + for (s in src) { + push(dst, s) // nolint:PERF006 + } +} + +def bad_push_clone_pipe(src : array; var dst : array) { + for (s in src) { + dst |> push_clone(s) // nolint:PERF006 + } +} + +def bad_push_clone_dot(src : array; var dst : array) { + for (s in src) { + dst.push_clone(s) // nolint:PERF006 + } +} + +def bad_push_clone_plain(src : array; var dst : array) { + for (s in src) { + push_clone(dst, s) // nolint:PERF006 + } +} + +// --- Negatives: same shape but should NOT warn PERF022 --- + +// 1. if-guarded push — selective, can't bulk-fold. PERF006 also suppresses +// on conditional pushes, so no nolint needed. +def good_if_guarded(src : array; var dst : array) { + for (s in src) { + if (s > 0) { + dst |> push(s) + } + } +} + +// 2. Multi-statement body — the loop does more than one push. +def good_multi_stmt(src : array; var dst : array; var counter : int&) { + for (s in src) { + dst |> push(s) // nolint:PERF006 + counter++ + } +} + +// 3. Transform-then-push — value isn't the iter var. +def good_transform(src : array; var dst : array) { + for (s in src) { + dst |> push(s * 2) // nolint:PERF006 + } +} + +// 4. Multi-source for — single bulk_from can't represent the zip. +def good_multi_source(a : array; b : array; var dst : array) { + for (x, y in a, b) { + dst |> push(x + y) // nolint:PERF006 + } +} + +// 5. Nested for — outer body isn't a single ExprCall; inner body multi-statement so +// it isn't a PERF022 hit either. +def good_nested(src : array>; var dst : array; var n : int&) { + for (row in src) { + for (s in row) { + dst |> push(s * 2) // nolint:PERF006 + n++ + } + } +} + +// 6. Push of a different variable — not the loop iter. +def good_push_other(src : array; var dst : array; other : int) { + for (s in src) { + dst |> push(other) // nolint:PERF006 + } +} + +// 7. Already-good form — the bulk call itself must not trigger. +def good_baseline(src : array; var dst : array) { + dst |> push_from(src) +} diff --git a/utils/mcp/tools/common.das b/utils/mcp/tools/common.das index b45e4cbc8d..4eaf9e3bb2 100644 --- a/utils/mcp/tools/common.das +++ b/utils/mcp/tools/common.das @@ -370,9 +370,7 @@ def public build_subtool_argv(exe, subtool_path : string; } argv |> push(subtool_path) argv |> push("--") - for (a in args) { - argv |> push(a) - } + argv |> push_from(args) return <- argv } diff --git a/utils/mcp/tools/cpp_common.das b/utils/mcp/tools/cpp_common.das index 670315cdb6..130a2e21b2 100644 --- a/utils/mcp/tools/cpp_common.das +++ b/utils/mcp/tools/cpp_common.das @@ -551,17 +551,11 @@ def cpp_run_scan(search_dirs : array; glob_filter : string; var err_mess if (empty(search_dirs)) { dirs_to_scan <- cpp_default_search_dirs() } else { - dirs_to_scan |> reserve(length(search_dirs)) - for (d in search_dirs) { - dirs_to_scan |> push(d) - } + dirs_to_scan |> push_from(search_dirs) } var args <- [sg, "scan", "-r", rules_path, "--json"] cpp_push_glob_args(args, dirs_to_scan, glob_filter) - args |> reserve(length(args) + length(dirs_to_scan)) - for (d in dirs_to_scan) { - args |> push(d) - } + args |> push_from(dirs_to_scan) var raw : string let rc = run_and_capture(args, raw, 60.0) if (rc != 0 && empty(raw)) { diff --git a/utils/mcp/tools/cpp_grep_usage.das b/utils/mcp/tools/cpp_grep_usage.das index 7322cff457..0d27a3b9d4 100644 --- a/utils/mcp/tools/cpp_grep_usage.das +++ b/utils/mcp/tools/cpp_grep_usage.das @@ -32,10 +32,7 @@ def do_cpp_grep_usage(symbol, directory, context_lines_str, glob_filter : string if (empty(rule_path)) return make_tool_result(rule_err, true) var args <- [sg, "scan", "-r", rule_path, "--json"] cpp_push_glob_args(args, search_dirs, glob_filter) - args |> reserve(length(args) + length(search_dirs)) - for (d in search_dirs) { - args |> push(d) - } + args |> push_from(search_dirs) var raw_json : string let rc = run_and_capture(args, raw_json, 60.0) if (rc != 0 && empty(raw_json)) return make_tool_result("ast-grep failed (exit {rc})", true) diff --git a/utils/mcp/tools/live.das b/utils/mcp/tools/live.das index e713ef9f80..4e4b2f6b33 100644 --- a/utils/mcp/tools/live.das +++ b/utils/mcp/tools/live.das @@ -250,7 +250,7 @@ def do_live_launch(file, project, project_root : string; port : string = ""; loa fprint(f, "#!/bin/sh\nnohup \"{live_exe}\" {script_args} > /dev/null 2>&1 &\n") } } - var exit_code = 0 + var exit_code : int unsafe { exit_code = system("chmod +x {sh} && {sh}") } diff --git a/web/CMakeLists.txt b/web/CMakeLists.txt index 56fd906c6c..559b424fd4 100644 --- a/web/CMakeLists.txt +++ b/web/CMakeLists.txt @@ -20,11 +20,11 @@ add_compile_options(-msimd128) add_compile_options(-msse2) add_compile_options(-mnontrapping-fptoint) -add_compile_options(-fwasm-exceptions) -add_link_options(-fwasm-exceptions) - -add_compile_definitions(DAS_ENABLE_EXCEPTIONS) -add_compile_definitions(_EMSCRIPTEN_VER) +# Modern wasm EH (try_table/exnref). DAS_ENABLE_EXCEPTIONS routes daslang +# error paths through C++ throw instead of setjmp/longjmp. +add_compile_options(-fwasm-exceptions -sWASM_LEGACY_EXCEPTIONS=0) +add_link_options(-fwasm-exceptions -sWASM_LEGACY_EXCEPTIONS=0) +add_compile_definitions(DAS_ENABLE_EXCEPTIONS _EMSCRIPTEN_VER) add_link_options( # Default STACK_SIZE is 64KB, not enough for daslang. @@ -70,10 +70,113 @@ set_target_properties(daslang_static PROPERTIES EXCLUDE_FROM_ALL FALSE RUNTIME_OUTPUT_DIRECTORY ${DAS_WEB_OUTPUT_DIR}) +# Pin the wasm32 runtime archive into web/output/lib so it doesn't collide +# with the host build's lib/ (root CMakeLists.txt:317 resets +# CMAKE_ARCHIVE_OUTPUT_DIRECTORY inside the subdirectory scope, so we override +# per-target). dasLLVM (link_wasm) auto-locates the archives from this path. +set_target_properties(libDaScript_runtime PROPERTIES + EXCLUDE_FROM_ALL FALSE + ARCHIVE_OUTPUT_DIRECTORY ${DAS_WEB_OUTPUT_DIR}/lib) + # Copy web UI assets to output at build time. add_custom_target(copy_web_ui ALL COMMAND ${CMAKE_COMMAND} -E copy_directory - ${CMAKE_CURRENT_SOURCE_DIR}/ui + ${CMAKE_CURRENT_SOURCE_DIR}/examples/ui ${DAS_WEB_OUTPUT_DIR} COMMENT "Copying web UI to ${DAS_WEB_OUTPUT_DIR}" ) + +# Stage everything site/playground/ needs for local-dev preview: +# - vendored UI sources (main.js/css, jquery) from web/examples/ui/src/ +# - sample bundles (data.json, hello.das, ...) from web/examples/ui/samples/ +# - WASM artifact (daslang_static.{js,wasm}) from daslang_static target +# Files land alongside the committed site/playground/index.html + forge-skin.css +# + playground-*.js. The destination dir is .gitignore'd for these artifacts — +# see site/.gitignore. After `ninja`, `cd site && python3 -m http.server 8000` +# serves /playground/ with no extra cp. +set(SITE_PLAYGROUND_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../site/playground) +set(SITE_FILES_WASM_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../site/files/wasm) +add_custom_target(stage_site_playground ALL + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/examples/ui/src + ${SITE_PLAYGROUND_DIR} + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/examples/ui/samples + ${SITE_PLAYGROUND_DIR}/samples + COMMAND ${CMAKE_COMMAND} -E copy + ${DAS_WEB_OUTPUT_DIR}/daslang_static.js + ${DAS_WEB_OUTPUT_DIR}/daslang_static.wasm + ${SITE_PLAYGROUND_DIR}/ + COMMAND ${CMAKE_COMMAND} -E make_directory ${SITE_FILES_WASM_DIR} + COMMAND ${CMAKE_COMMAND} -E copy + ${DAS_WEB_OUTPUT_DIR}/daslang_static.js + ${DAS_WEB_OUTPUT_DIR}/daslang_static.wasm + ${SITE_FILES_WASM_DIR}/ + DEPENDS daslang_static + COMMENT "Staging site/playground/ and site/files/wasm/ for local-dev preview" +) + +# Host daslang sub-build (outer is emsdk), drives .das→.wasm cross-compile. +include(ExternalProject) +find_program(DAS_HOST_CC NAMES clang gcc REQUIRED) +find_program(DAS_HOST_CXX NAMES clang++ g++ REQUIRED) +find_program(DAS_WASMTIME wasmtime) +set(_host_compiler_args) +if(NOT CMAKE_HOST_WIN32) + set(_host_compiler_args + -DCMAKE_C_COMPILER:FILEPATH=${DAS_HOST_CC} + -DCMAKE_CXX_COMPILER:FILEPATH=${DAS_HOST_CXX}) +endif() +ExternalProject_Add(host_daslang + EXCLUDE_FROM_ALL TRUE + SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/.. + INSTALL_COMMAND "" + BUILD_BYPRODUCTS ${CMAKE_CURRENT_SOURCE_DIR}/../bin/daslang + STEP_TARGETS build + CMAKE_CACHE_ARGS -DCMAKE_TOOLCHAIN_FILE:STRING= + ${_host_compiler_args} + -DCMAKE_BUILD_TYPE:STRING=Release + -DDAS_LLVM_DISABLED:BOOL=OFF + -DDAS_PUGIXML_DISABLED:BOOL=OFF + BUILD_COMMAND ${CMAKE_COMMAND} --build --target daslang --parallel) + +# Cross-compile web playground example scripts to wasm32 via host daslang. +set(_repo ${CMAKE_CURRENT_SOURCE_DIR}/..) +set(DAS_HOST_DASLANG ${_repo}/bin/daslang) +set(_web_ex_src ${CMAKE_CURRENT_SOURCE_DIR}/examples/ui/samples/examples) +set(_web_ex_out ${DAS_WEB_OUTPUT_DIR}/samples/examples) + +add_custom_target(all_wasm) +set(_wasm_run_cmds) +foreach(ex hello loop func) + add_custom_command(OUTPUT ${_web_ex_out}/${ex}.wasm + COMMAND ${CMAKE_COMMAND} -E make_directory ${_web_ex_out} + COMMAND ${DAS_HOST_DASLANG} -exe -output ${_web_ex_out}/${ex} + ${_web_ex_src}/${ex}.das -- + --jit-target=wasm32-unknown-emscripten + --jit-runtime-lib=$ + DEPENDS ${_web_ex_src}/${ex}.das host_daslang libDaScript_runtime + WORKING_DIRECTORY ${_repo} + COMMENT "Cross-compile ${ex} → wasm32" VERBATIM) + add_custom_target(${ex}_web_wasm DEPENDS ${_web_ex_out}/${ex}.wasm) + add_dependencies(all_wasm ${ex}_web_wasm) + if(DAS_WASMTIME) + list(APPEND _wasm_run_cmds + COMMAND ${CMAKE_COMMAND} -E echo "=== ${ex}.wasm ===" + COMMAND ${DAS_WASMTIME} -W exceptions=y ${_web_ex_out}/${ex}.wasm) + endif() +endforeach() + +if(DAS_WASMTIME) + add_custom_target(run_wasm_examples + ${_wasm_run_cmds} + DEPENDS all_wasm + COMMENT "Run cross-compiled .wasm examples under wasmtime") +endif() + +# Stage example .wasm into site/playground for local-dev preview (gitignored). +add_custom_target(stage_site_playground_wasm + COMMAND ${CMAKE_COMMAND} -E make_directory ${_repo}/site/playground/samples/examples + COMMAND ${CMAKE_COMMAND} -E copy_directory ${_web_ex_out} ${_repo}/site/playground/samples/examples + DEPENDS all_wasm + COMMENT "Staging JIT example .wasm into site/playground/") diff --git a/web/README.md b/web/README.md index e2b0253eb2..d0a61ff110 100644 --- a/web/README.md +++ b/web/README.md @@ -1,63 +1,54 @@ -# daslang — WebAssembly build +# daslang — WebAssembly + +Daslang supports WASM interpreter and direct compilation to WASM via LLVM JIT. Builds daslang (compiler + runtime) as WASM via Emscripten. Outputs land in `output/`: `daslang.wasm`, `daslang_static.wasm`, `das-fmt.wasm`, plus JS loaders. ## Prerequisites -- **CMake ≥ 3.16**, **Ninja** -- **Emscripten SDK** — installed by the step scripts below (≈ 1 GB, clones into `web/emsdk/`) +- **CMake ≥ 3.16**, **Ninja**, **Python 3** +- **Emscripten** — `emcc` / `em++` / `emcmake` on `PATH`. Install via your package manager (easiest) or pin a specific version via `emsdk` (CI does this). -## Install emsdk (once) +## Install Emscripten +**Ubuntu / Debian** ```bash -cd web/ -bash step0_emsdk_install.sh +sudo apt install emscripten ``` -Clones https://github.com/emscripten-core/emsdk into `web/emsdk/`. - -## Activate emsdk (each shell session) - -**Linux / macOS** +**macOS (Homebrew)** ```bash -bash step1_emsdk_activate_linux.sh +brew install emscripten ``` -**Windows** -```bat -step1_emsdk_activate_windows.sh +**Windows (Chocolatey)** +```powershell +choco install emscripten ``` -Installs the latest toolchain and puts `emcc` on `PATH`. Must be run in every new shell before building. +After install, `emcc --version` should work in any new shell. ## Configure and build +From `web/`, with emsdk activated: + **Release** (recommended, smaller `.wasm`): ```bash -cmake -DCMAKE_BUILD_TYPE=Release -G Ninja -DCMAKE_TOOLCHAIN_FILE=../emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake ../ -ninja +emcmake cmake -S . -B cmake_temp -G Ninja -DCMAKE_BUILD_TYPE=Release && cmake --build cmake_temp ``` **Debug** (larger, with DWARF symbols): ```bash -cp ../CMakeXxdImpl.txt . -rm -rf cmake_temp && mkdir cmake_temp && cd cmake_temp -cmake -DCMAKE_BUILD_TYPE=Debug -G Ninja \ - -DCMAKE_TOOLCHAIN_FILE=../emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake \ - ../ -ninja +emcmake cmake -S . -B cmake_temp -G Ninja -DCMAKE_BUILD_TYPE=Debug && cmake --build cmake_temp ``` -First build takes 5–15 min; incremental rebuilds are faster (just re-run `ninja` from `cmake_temp/`). - ## Running locally WASM files require a local HTTP server (browsers block `file://` for WASM). ```bash -cd output/ -python3 -m http.server 8080 +python3 -m http.server 8080 -d output/ ``` Open http://localhost:8080 — the in-browser daslang IDE loads. @@ -71,12 +62,34 @@ Or serve with any static server (Node `serve`, nginx, etc.). | `output/daslang_static.js` + `.wasm` | interpreter (no dynamic modules) | | `output/das-fmt.js` + `.wasm` | Source formatter | | `output/index.html` | Web IDE entry point | +| `output/samples/examples/.wasm` | JIT-compiled examples (optional, see below) | + +## JIT-compiled examples (optional) + +The playground's "JIT (wasm)" radio runs precompiled `.wasm` artifacts cross- +compiled from each sample `.das` via dasLLVM's `--jit-target=wasm32-unknown-emscripten`. +Without these, the JIT radio stays disabled and only the interpreter runs. + +Build steps (assumes host daslang already built with dasLLVM): + +```bash +# 1. Build runtime archive (the wasm build documented above): +emcmake cmake -S web -B web/cmake_temp -G Ninja -DCMAKE_BUILD_TYPE=Release && cmake --build web/cmake_temp --target libDaScript_runtime + +# 2. Build the example + util .wasm files (from repo root): +cmake -B build -DDAS_LLVM_DISABLED=OFF -DDAS_BUILD_WASM=ON && cmake --build build --target all_wasm +``` + +Outputs land in `web/output/samples/examples/`. CI builds these in the +`wasm_cross` job (see `.github/workflows/wasm_build.yml`). ## Troubleshooting -**`emcc: not found`** — run Step 1 (`source emsdk/emsdk_env.sh`) in the current shell. +**`emcc: not found`** — package-manager install: open a new shell or re-run your shell init. emsdk install: `source ./emsdk/emsdk_env.sh` in the current shell. + +**CMake can't find Emscripten** — use `emcmake cmake ...` (not bare `cmake`), or verify `EMSDK`/`EMSCRIPTEN` env vars are set: `echo $EMSDK $EMSCRIPTEN`. -**CMake can't find Emscripten** — verify `EMSCRIPTEN` env var is set: `echo $EMSCRIPTEN`. +**`emcc: unrecognized option '-sWASM_LEGACY_EXCEPTIONS=0'` or similar** — distro emscripten is too old. Switch to emsdk install (Option B) and pin a current version. **Build fails with SIGSEGV / stack overflow** — use Release build or increase stack size; Debug stack is much larger. diff --git a/web/ui/samples/data.json b/web/examples/ui/samples/data.json similarity index 100% rename from web/ui/samples/data.json rename to web/examples/ui/samples/data.json diff --git a/web/ui/samples/examples/func.das b/web/examples/ui/samples/examples/func.das similarity index 100% rename from web/ui/samples/examples/func.das rename to web/examples/ui/samples/examples/func.das diff --git a/web/ui/samples/examples/hello.das b/web/examples/ui/samples/examples/hello.das similarity index 100% rename from web/ui/samples/examples/hello.das rename to web/examples/ui/samples/examples/hello.das diff --git a/web/ui/samples/examples/loop.das b/web/examples/ui/samples/examples/loop.das similarity index 100% rename from web/ui/samples/examples/loop.das rename to web/examples/ui/samples/examples/loop.das diff --git a/web/ui/samples/examples/macros/for_loop_macro_mod.das b/web/examples/ui/samples/examples/macros/for_loop_macro_mod.das similarity index 100% rename from web/ui/samples/examples/macros/for_loop_macro_mod.das rename to web/examples/ui/samples/examples/macros/for_loop_macro_mod.das diff --git a/web/ui/samples/examples/macros/main.das b/web/examples/ui/samples/examples/macros/main.das similarity index 100% rename from web/ui/samples/examples/macros/main.das rename to web/examples/ui/samples/examples/macros/main.das diff --git a/web/ui/samples/examples/random_sequence.das b/web/examples/ui/samples/examples/random_sequence.das similarity index 100% rename from web/ui/samples/examples/random_sequence.das rename to web/examples/ui/samples/examples/random_sequence.das diff --git a/web/ui/samples/examples/tests.das b/web/examples/ui/samples/examples/tests.das similarity index 100% rename from web/ui/samples/examples/tests.das rename to web/examples/ui/samples/examples/tests.das diff --git a/web/ui/src/jquery-3.6.0.min.js b/web/examples/ui/src/jquery-3.6.0.min.js similarity index 100% rename from web/ui/src/jquery-3.6.0.min.js rename to web/examples/ui/src/jquery-3.6.0.min.js diff --git a/web/ui/src/main.css b/web/examples/ui/src/main.css similarity index 100% rename from web/ui/src/main.css rename to web/examples/ui/src/main.css diff --git a/web/ui/src/main.js b/web/examples/ui/src/main.js similarity index 61% rename from web/ui/src/main.js rename to web/examples/ui/src/main.js index 496c2438c4..59f55628ed 100644 --- a/web/ui/src/main.js +++ b/web/examples/ui/src/main.js @@ -63,6 +63,25 @@ pageInit = function () { } +// Current sample's JIT-wasm basename (null for multi-file samples or when the +// .wasm artifact is not present). Drives runJit() and the engine radio's +// disabled state. +var currentJitName = null; + +function deriveJitName(files) { + if (!files || files.length !== 1) return null; + return files[0].split('/').pop().replace(/\.das$/, ''); +} + +function updateEngineAvailability(name) { + const jitRadio = document.querySelector('input[name=engine][value=jit]'); + if (!jitRadio) return; + if (!name) { jitRadio.disabled = true; return; } + fetch('./samples/examples/' + name + '.wasm', { method: 'HEAD' }) + .then(r => { jitRadio.disabled = !r.ok; }) + .catch(() => { jitRadio.disabled = true; }); +} + selectSample = function(type, id) { const sel = sampleList[type]; if (!sel && id === undefined) return; // dropdown was removed; nothing to read @@ -71,6 +90,8 @@ selectSample = function(type, id) { // Multi-file samples ship as files[] — load all in parallel, then hand // the bundle to the loader (single editor today, tab strip in phase 3). const files = samplesData[type][vv].files; + currentJitName = deriveJitName(files); + updateEngineAvailability(currentJitName); Promise.all(files.map(f => $.ajax({ url: './samples/' + f, dataType: 'text' }) .then(text => ({ name: f.split('/').pop(), text })) @@ -179,17 +200,31 @@ function updateButtonStates() { // still works without churn — it triggers a full refresh. window.updateTestButtonState = updateButtonStates; +function selectedEngine() { + const el = document.querySelector('input[name=engine]:checked'); + return el ? el.value : 'interpreter'; +} + runCode = function() { + syncUrlToState(); + if (selectedEngine() === 'jit') { + if (!currentJitName) { + printOutput("JIT unavailable: no precompiled .wasm for this sample", '#ff9393'); + return; + } + runJit(currentJitName); + return; + } + // Interpreter path. WASM readiness gate first — both Module.callMain and + // the single-buffer fallback need FS up. if (!isWasmReady()) { printOutput('daslang is still loading, please wait…', '#ff9393'); return; } if (syncMemFsFromState()) { - syncUrlToState(); Module.callMain(['main.das']); return; } - syncUrlToState(); runScript(code.getValue()); } @@ -211,6 +246,130 @@ runTests = function() { Module.callMain(['/dastest/dastest.das', '--', '--test', '/main.das', '--timeout=0']); } +// Minimal wasi_snapshot_preview1 shim — daslang STANDALONE_WASM output only +// touches stdout (fd_write), proc_exit, clock, args/environ stubs, and a few +// fd_* no-ops emscripten's libc emits at link time. See +// modules/dasLLVM/README.md "Cross-compilation" for the import surface. +function makeWasiShim(memoryRef) { + let stdoutBuf = ''; + const decoder = new TextDecoder('utf-8'); + + function mem() { return new DataView(memoryRef.buffer); } + function u8() { return new Uint8Array(memoryRef.buffer); } + + function flushStdout(force) { + // Flush by newline so each printed line gets its own output row. + let nl; + while ((nl = stdoutBuf.indexOf('\n')) >= 0) { + printOutput(stdoutBuf.slice(0, nl), '#ffffff'); + stdoutBuf = stdoutBuf.slice(nl + 1); + } + if (force && stdoutBuf.length) { + printOutput(stdoutBuf, '#ffffff'); + stdoutBuf = ''; + } + } + + return { + fd_write(fd, iovsPtr, iovsLen, nWrittenPtr) { + const dv = mem(); + let total = 0; + for (let i = 0; i < iovsLen; i++) { + const off = iovsPtr + i * 8; + const bufPtr = dv.getUint32(off, true); + const bufLen = dv.getUint32(off + 4, true); + const bytes = u8().subarray(bufPtr, bufPtr + bufLen); + stdoutBuf += decoder.decode(bytes, { stream: true }); + total += bufLen; + } + dv.setUint32(nWrittenPtr, total, true); + flushStdout(false); + return 0; + }, + fd_read() { return 0; }, + fd_close() { return 0; }, + fd_seek(fd, offLo, offHi, whence, newOffPtr) { + const dv = mem(); + dv.setUint32(newOffPtr, 0, true); + dv.setUint32(newOffPtr + 4, 0, true); + return 0; + }, + fd_fdstat_get() { return 0; }, + fd_fdstat_set_flags() { return 0; }, + fd_prestat_get() { return 8; /* BADF — stop probing preopens */ }, + fd_prestat_dir_name() { return 8; }, + args_sizes_get(argcPtr, argvBufSizePtr) { + const dv = mem(); + dv.setUint32(argcPtr, 0, true); + dv.setUint32(argvBufSizePtr, 0, true); + return 0; + }, + args_get() { return 0; }, + environ_sizes_get(envcPtr, envBufSizePtr) { + const dv = mem(); + dv.setUint32(envcPtr, 0, true); + dv.setUint32(envBufSizePtr, 0, true); + return 0; + }, + environ_get() { return 0; }, + clock_time_get(id, precision, timePtr) { + const ns = BigInt(Date.now()) * 1000000n; + mem().setBigUint64(timePtr, ns, true); + return 0; + }, + clock_res_get(id, resPtr) { + mem().setBigUint64(resPtr, 1000000n, true); + return 0; + }, + random_get(bufPtr, bufLen) { + const view = u8().subarray(bufPtr, bufPtr + bufLen); + crypto.getRandomValues(view); + return 0; + }, + path_open() { return 8; }, + path_readlink() { return 8; }, + path_filestat_get() { return 8; }, + proc_exit(code) { + flushStdout(true); + throw new WasiExit(code); + }, + }; +} + +function WasiExit(code) { this.code = code; } + +function runJit(name) { + // Shim's DataView is rebuilt per-call from memRef.buffer, so we can + // create the shim before instantiation and patch the buffer once memory + // is exported. + const memRef = { buffer: new ArrayBuffer(0) }; + const shim = makeWasiShim(memRef); + fetch('./samples/examples/' + name + '.wasm') + .then(r => { + if (!r.ok) throw new Error('HTTP ' + r.status + ' fetching ' + name + '.wasm'); + return r.arrayBuffer(); + }) + .then(bytes => WebAssembly.instantiate(bytes, { + wasi_snapshot_preview1: shim, + env: new Proxy({}, { get: (_, name) => name === 'memory' ? undefined : () => -1 }), + })) + .then(({ instance }) => { + memRef.buffer = instance.exports.memory.buffer; + const exports = instance.exports; + try { + if (typeof exports._initialize === 'function') exports._initialize(); + if (typeof exports._start === 'function') exports._start(); + else if (typeof exports.main === 'function') exports.main(); + else printOutput('JIT error: no entry point exported in wasm', '#ff2d2d'); + } catch (e) { + if (!(e instanceof WasiExit)) { + printOutput('JIT error: ' + (e && e.message ? e.message : e), '#ff2d2d'); + } + } + }) + .catch(e => printOutput('JIT load error: ' + (e && e.message ? e.message : e), '#ff2d2d')); +} + clearOutput = function() { while (editorOutput.firstChild) { diff --git a/web/step0_emsdk_install.sh b/web/step0_emsdk_install.sh deleted file mode 100644 index 5d0a18e1bd..0000000000 --- a/web/step0_emsdk_install.sh +++ /dev/null @@ -1 +0,0 @@ -git clone https://github.com/emscripten-core/emsdk.git \ No newline at end of file diff --git a/web/step1_emsdk_activate_linux.sh b/web/step1_emsdk_activate_linux.sh deleted file mode 100644 index d283afb266..0000000000 --- a/web/step1_emsdk_activate_linux.sh +++ /dev/null @@ -1,3 +0,0 @@ -./emsdk/emsdk install latest -./emsdk/emsdk activate latest -source ./emsdk_env.sh \ No newline at end of file diff --git a/web/step1_emsdk_activate_windows.sh b/web/step1_emsdk_activate_windows.sh deleted file mode 100644 index 2f13a301f2..0000000000 --- a/web/step1_emsdk_activate_windows.sh +++ /dev/null @@ -1,3 +0,0 @@ -./emsdk/emsdk install latest -./emsdk/emsdk activate latest -./emsdk/emsdk_env.bat \ No newline at end of file