diff --git a/.githooks/README.md b/.githooks/README.md
new file mode 100644
index 0000000000..6624b2c4df
--- /dev/null
+++ b/.githooks/README.md
@@ -0,0 +1,50 @@
+# Repo-managed git hooks
+
+Tracked hooks that mirror CI checks. Cross-platform: Linux, macOS, Windows (Git Bash).
+
+## Enable (once per clone)
+
+```sh
+git config core.hooksPath .githooks
+```
+
+Reverts via `git config --unset core.hooksPath`. Skip once with `git push --no-verify`.
+
+### Windows notes
+
+- Git for Windows ships Git Bash — the `#!/usr/bin/env bash` shebang works out of the box. No extra install needed.
+- If you use PowerShell or `cmd` for `git push`, that's fine — git invokes the hook through its own shell, not yours.
+- File-mode permissions are ignored on Windows; just having the file in `.githooks/` is enough.
+
+### Linux / macOS
+
+Hooks must be executable. Cloning preserves the executable bit (it's set in the repo via `git update-index --chmod=+x`). If you ever lose it locally:
+
+```sh
+chmod +x .githooks/*
+```
+
+## Hooks
+
+- **pre-push** — formatter (`--verify` on the whole tree) + lint on pushed `.das` files. Mirrors `.github/workflows/extended_checks.yml`.
+
+## Requirements
+
+A built `daslang` binary. The script auto-detects:
+
+- `bin/daslang` (Linux / macOS, single-config Make/Ninja)
+- `bin/daslang.exe` (MSYS / cygwin)
+- `bin/Release/daslang.exe` / `bin/Debug/daslang.exe` (Windows MSVC)
+- `build/daslang`, `build/bin/daslang`
+
+Build before pushing:
+
+```sh
+cmake --build build --target daslang
+```
+
+Override the resolved path:
+
+```sh
+DASLANG=/custom/path/daslang git push
+```
diff --git a/.githooks/pre-push b/.githooks/pre-push
new file mode 100755
index 0000000000..29a709f72a
--- /dev/null
+++ b/.githooks/pre-push
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+# pre-push: mirror CI's formatter + lint (.github/workflows/extended_checks.yml).
+# Cross-platform: runs under Git Bash on Windows, bash on Linux/macOS.
+#
+# Enable per-clone:
+# git config core.hooksPath hooks
+# Skip once:
+# git push --no-verify
+# Override binary path:
+# DASLANG=/path/to/daslang git push
+
+set -eu
+
+ROOT="$(git rev-parse --show-toplevel)"
+cd "$ROOT"
+
+# Resolve daslang binary across single/multi-config layouts.
+resolve_daslang() {
+ if [ -n "${DASLANG:-}" ] && [ -x "$DASLANG" ]; then
+ echo "$DASLANG"; return
+ fi
+ for p in \
+ "$ROOT/bin/daslang" \
+ "$ROOT/bin/daslang.exe" \
+ "$ROOT/bin/Release/daslang.exe" \
+ "$ROOT/bin/Debug/daslang.exe" \
+ "$ROOT/build/daslang" \
+ "$ROOT/build/bin/daslang"
+ do
+ [ -x "$p" ] && { echo "$p"; return; }
+ done
+ return 1
+}
+
+DASLANG="$(resolve_daslang || true)"
+if [ -z "$DASLANG" ]; then
+ echo "pre-push: daslang not found — build it (cmake --build build --target daslang)" >&2
+ echo " or set DASLANG=/path/to/daslang" >&2
+ exit 1
+fi
+
+echo "pre-push: using $DASLANG"
+
+# Formatter: whole tree, verify-only (CI parity).
+echo "pre-push: formatter --verify ..."
+"$DASLANG" "$ROOT/utils/das-fmt/dasfmt.das" -- --path "$ROOT" --verify
+
+# Lint: only .das files changed in the pushed range vs remote tip
+# (mirrors CI's `git diff origin/...HEAD`).
+ZERO40="0000000000000000000000000000000000000000"
+ZERO64="0000000000000000000000000000000000000000000000000000000000000000"
+RANGES=()
+while read -r LOCAL_REF LOCAL_SHA REMOTE_REF REMOTE_SHA; do
+ case "$LOCAL_SHA" in "$ZERO40"|"$ZERO64") continue;; esac # branch delete
+ case "$REMOTE_SHA" in
+ "$ZERO40"|"$ZERO64")
+ BASE="$(git merge-base "$LOCAL_SHA" origin/master 2>/dev/null || true)"
+ [ -n "$BASE" ] && RANGES+=("$BASE..$LOCAL_SHA")
+ ;;
+ *)
+ RANGES+=("$REMOTE_SHA..$LOCAL_SHA")
+ ;;
+ esac
+done
+
+if [ ${#RANGES[@]} -eq 0 ]; then
+ echo "pre-push: no ranges (deleting or empty); skipping lint"
+ exit 0
+fi
+
+# Portable read-into-array (mapfile is bash 4+; macOS default ships bash 3.2).
+CHANGED=()
+while IFS= read -r line; do
+ CHANGED+=("$line")
+done < <(git diff --name-only --diff-filter=AM "${RANGES[@]}" -- '*.das' | sort -u)
+
+if [ ${#CHANGED[@]} -eq 0 ]; then
+ echo "pre-push: no .das files changed; skipping lint"
+ exit 0
+fi
+
+echo "pre-push: linting ${#CHANGED[@]} .das file(s)"
+"$DASLANG" "$ROOT/utils/lint/main.das" -- "${CHANGED[@]}" --quiet
diff --git a/benchmarks/fusion/bench_arr_at_i64.das b/benchmarks/fusion/bench_arr_at_i64.das
new file mode 100644
index 0000000000..2352225301
--- /dev/null
+++ b/benchmarks/fusion/bench_arr_at_i64.das
@@ -0,0 +1,82 @@
+options gen2
+
+require dastest/testing_boost
+
+// Apples-to-apples cost of int / int64 / uint64-indexed array access.
+// All three benches use the same `for (i in (N))` shape,
+// differing only in the index type. Write workload is `arr[i] = 1`
+// (no cast in the body — isolates the index/fusion cost). The outer
+// `for (j in range(OUTER))` amortizes the per-body harness overhead
+// over OUTER * N inner ops.
+
+let N = 10000
+let OUTER = 10
+let TOTAL = N * OUTER
+
+[benchmark]
+def arr_at_int_idx(b : B?) {
+ var arr : array
+ arr |> resize(N)
+ b |> run("write_int_idx/{TOTAL}", TOTAL) {
+ for (_j in range(OUTER)) {
+ for (i in range(N)) {
+ arr[i] = 1
+ }
+ }
+ }
+ b |> run("read_int_idx/{TOTAL}", TOTAL) {
+ var sum = 0
+ for (_j in range(OUTER)) {
+ for (i in range(N)) {
+ sum += arr[i]
+ }
+ }
+ strictEqual(b, length(arr), N)
+ }
+}
+
+[benchmark]
+def arr_at_int64_idx(b : B?) {
+ var arr : array
+ arr |> resize(N)
+ let N64 = int64(N)
+ b |> run("write_int64_idx/{TOTAL}", TOTAL) {
+ for (_j in range(OUTER)) {
+ for (i in range64(N64)) {
+ arr[i] = 1
+ }
+ }
+ }
+ b |> run("read_int64_idx/{TOTAL}", TOTAL) {
+ var sum = 0
+ for (_j in range(OUTER)) {
+ for (i in range64(N64)) {
+ sum += arr[i]
+ }
+ }
+ strictEqual(b, length(arr), N)
+ }
+}
+
+[benchmark]
+def arr_at_uint64_idx(b : B?) {
+ var arr : array
+ arr |> resize(N)
+ let N64 = uint64(N)
+ b |> run("write_uint64_idx/{TOTAL}", TOTAL) {
+ for (_j in range(OUTER)) {
+ for (i in urange64(N64)) {
+ arr[i] = 1
+ }
+ }
+ }
+ b |> run("read_uint64_idx/{TOTAL}", TOTAL) {
+ var sum = 0
+ for (_j in range(OUTER)) {
+ for (i in urange64(N64)) {
+ sum += arr[i]
+ }
+ }
+ strictEqual(b, length(arr), N)
+ }
+}
diff --git a/benchmarks/fusion/bench_table_index_i64.das b/benchmarks/fusion/bench_table_index_i64.das
new file mode 100644
index 0000000000..1a03f49c1c
--- /dev/null
+++ b/benchmarks/fusion/bench_table_index_i64.das
@@ -0,0 +1,66 @@
+options gen2
+
+require dastest/testing_boost
+
+// Apples-to-apples cost of int / int64 / uint64-keyed table indexing.
+// All three benches use the same `for (k in (N))` shape,
+// differing only in the key type. Read workload is `sum += tab[k]`
+// (no cast in the body). The outer `for (j in range(OUTER))` amortizes
+// per-body harness overhead over OUTER * N inner ops.
+
+let N = 10000
+let OUTER = 10
+let TOTAL = N * OUTER
+
+[benchmark]
+def table_index_int_key(b : B?) {
+ var tab : table
+ for (k in range(N)) {
+ tab[k] = 1
+ }
+ b |> run("read_int_key/{TOTAL}", TOTAL) {
+ var sum = 0
+ for (_j in range(OUTER)) {
+ for (k in range(N)) {
+ sum += tab[k]
+ }
+ }
+ strictEqual(b, length(tab), N)
+ }
+}
+
+[benchmark]
+def table_index_int64_key(b : B?) {
+ var tab : table
+ let N64 = int64(N)
+ for (k in range64(N64)) {
+ tab[k] = 1
+ }
+ b |> run("read_int64_key/{TOTAL}", TOTAL) {
+ var sum = 0
+ for (_j in range(OUTER)) {
+ for (k in range64(N64)) {
+ sum += tab[k]
+ }
+ }
+ strictEqual(b, length(tab), N)
+ }
+}
+
+[benchmark]
+def table_index_uint64_key(b : B?) {
+ var tab : table
+ let N64 = uint64(N)
+ for (k in urange64(N64)) {
+ tab[k] = 1
+ }
+ b |> run("read_uint64_key/{TOTAL}", TOTAL) {
+ var sum = 0
+ for (_j in range(OUTER)) {
+ for (k in urange64(N64)) {
+ sum += tab[k]
+ }
+ }
+ strictEqual(b, length(tab), N)
+ }
+}
diff --git a/benchmarks/sql/LINQ.md b/benchmarks/sql/LINQ.md
index 4879f3eecd..91289d165b 100644
--- a/benchmarks/sql/LINQ.md
+++ b/benchmarks/sql/LINQ.md
@@ -778,3 +778,7 @@ dastest reports `ns/op` in INTERP mode by default. To bump dataset size as the s
**`PERF009` suppression in `fold_linq_default`.** The macro's `var pass_N = call` + later `return <- pass_N` pattern triggers PERF009 on single-pass chains. The shape is load-bearing for the array-pipeline semantics (every stage binds so the next can reuse the buffer in-place), so we suppress inline at the qmacro_expr emission site and document why.
**Benchmark variants where SQL has no clean form.** `zip` (not a relational op), `_all(pred)` (no direct `_all` chain terminal in sqlite_linq), `join` with inner-select-from (wiring not exposed), `distinct |> count` (no `COUNT(DISTINCT col)` yet), `take/skip` before aggregate (LIMIT/OFFSET semantics conflict with aggregate-collapse). We either reformulate to a SQL-friendly shape (`count(where ¬p)` for all_match), omit the m1 column (zip, join), or terminate the chain in `to_array` instead of an aggregate (take/skip/distinct).
+
+## Future work — 64-bit sweep
+
+Gated on 64-bit arrays + tables landing in daslang. Once they do, sweep `daslib/linq.das` to add `int64` overloads on every count/index/N-parameter surface. Today only the count family is symmetric — `count(iter; pred)` / `count(arr; pred)` / `long_count(iter; pred)` / `long_count(arr; pred)` all exist with their bare (no-pred) forms. The rest of the surface (`take(N)`, `skip(N)`, `skip_last(N)`, `take_last(N)`, `top_n(N)`, `top_n_by(N)`, `top_n_by_descending(N)`, `top_n_by_with_cmp(N)`, `element_at(N)`) still takes `int` only. Add `int64` overloads alongside, route splice planners to pick the matching variant by argument type, and refresh benchmarks for the large-N regime.
diff --git a/benchmarks/sql/M4_DECS_EXPANSION.md b/benchmarks/sql/M4_DECS_EXPANSION.md
new file mode 100644
index 0000000000..dc6f204549
--- /dev/null
+++ b/benchmarks/sql/M4_DECS_EXPANSION.md
@@ -0,0 +1,266 @@
+# m4_decs_fold lane — expansion plan
+
+Adds a third benchmark target alongside `m1_sql` (SQL via `_sql`) and `m3`/`m3f` (array linq, with/without `_fold` splice). The new lane is `m4_decs_fold`:
+
+```das
+_fold(from_decs_template(type)._where(...)._select(...).sum())
+```
+
+Goal: tri-platform comparison (SQL vs array vs decs) under the same chain shape, so the splice path's decs win is directly comparable to its array win.
+
+## Baseline matrix (2026-05-20, before m4 expansion)
+
+100K rows, INTERP mode. m3f = `_fold(each(arr)...)`. Lower is better.
+
+### Headlines (m3f wins both)
+
+| benchmark | m1 sql | m3 | m3f | m1/m3f | m3/m3f |
+|---|---:|---:|---:|---:|---:|
+| sum_aggregate | 29 | 30 | **2** | 14.5× | 15.0× |
+| select_where_count | 32 | 57 | **5** | 6.4× | 11.4× |
+| sum_where | 32 | 43 | **4** | 8.0× | 10.8× |
+| chained_where | 36 | 45 | **6** | 6.0× | 7.5× |
+| aggregate_match | 34 | 49 | **5** | 6.8× | 9.8× |
+| all_match | 27 | 21 | **3** | 9.0× | 7.0× |
+| take_while_match | 7 | 22 | **2** | 3.5× | 11.0× |
+| count_aggregate | 29 | 29 | **4** | 7.2× | 7.2× |
+| select_where_sum | 36 | 60 | **7** | 5.1× | 8.6× |
+| to_array_filter | 74 | 42 | **11** | 6.7× | 3.8× |
+
+### Order/sort family (m3f dominates m3)
+
+| benchmark | m1 | m3 | m3f | m3/m3f |
+|---|---:|---:|---:|---:|
+| sort_take | 38 | 763 | **27** | 28.3× |
+| order_take_desc | 38 | 746 | **27** | 27.6× |
+| select_where_order_take | 36 | 379 | **24** | 15.8× |
+| sort_first | 37 | 742 | **756** | (regresses; sort dominates) |
+
+### Group-by family (2-5× wins)
+
+| benchmark | m1 | m3 | m3f | m1/m3f | m3/m3f |
+|---|---:|---:|---:|---:|---:|
+| groupby_sum | 173 | 102 | **37** | 4.7× | 2.8× |
+| groupby_count | 143 | 68 | **36** | 4.0× | 1.9× |
+| groupby_average | 171 | 106 | **52** | 3.3× | 2.0× |
+| groupby_having_count | 144 | 74 | **36** | 4.0× | 2.1× |
+| groupby_having_hidden_sum | 175 | 103 | **40** | 4.4× | 2.6× |
+| groupby_max | 174 | 103 | **44** | 4.0× | 2.3× |
+| groupby_min | 173 | 106 | **43** | 4.0× | 2.5× |
+| groupby_multi_reducer | 190 | 139 | **53** | 3.6× | 2.6× |
+| groupby_select_sum | — | 110 | **59** | — | 1.9× |
+| groupby_where_count | 76 | 64 | **23** | 3.3× | 2.8× |
+| groupby_where_sum | 86 | 79 | **23** | 3.7× | 3.4× |
+| groupby_first | — | 68 | **35** | — | 1.9× |
+
+### Anomalies / weak spots
+
+| benchmark | m1 | m3 | m3f | note |
+|---|---:|---:|---:|---|
+| indexed_lookup | 1,431 | 2,029,891 | 200,399 | SQL B-tree wins 1417× over linear scan; splice helps 10× over m3 but algorithmically can't match SQL. Decs equivalent uses **eid lookup** (archetype hash lookup, fast path). |
+| zip_dot_product | — | 52 | 57 | plan_zip landed but bench shape doesn't hit the splice (m3f slower than m3 by 10%). Worth a follow-up. |
+| join_count | — | 116 | 122 | No splice arm for join. m3f slightly slower. |
+| sort_first | 37 | 742 | 756 | Sort dominates the whole pipeline. m3f doesn't help when terminator is `first` after a full sort. |
+| distinct_take | — | 30 | 0 | m3f=0 ns/op — bench may be constant-folded; verify it actually exercises the chain. |
+| take_count_filtered / take_sum_aggregate / select_count / first_match / element_at_match / reverse_take / skip_take | various | various | 0 | Several 0 ns/op cases — verify the chain isn't being eliminated by DCE. |
+
+## Decs benchmarks (already in `benchmarks/decs/`)
+
+| benchmark | m1_hand_query | m2_eager_bridge | m3_fold_splice / m4_template_fold |
+|---|---:|---:|---:|
+| from_decs_count | 0 (arch.size) | 60 | **0** (matches hand) |
+| from_decs_sum | 4 (query) | 202 | **8** (within 2× of hand) |
+
+## m4_decs_fold expansion — triage
+
+Inventory of every `benchmarks/sql/*.das` benchmark with **decs feasibility** classification. Surfaces that don't yet exist on the decs side are flagged so we can decide whether to expand the decs surface.
+
+### Category A — clean decs map (no new surface needed)
+
+These translate directly: array `each(arr)._chain()` becomes decs `from_decs_template(type)._chain()`. Splice already covers all chain shapes used.
+
+- aggregate_match
+- all_match
+- any_match
+- average_aggregate
+- chained_where
+- contains_match
+- count_aggregate
+- distinct_count
+- first_match
+- first_or_default_match
+- last_match
+- long_count_aggregate
+- max_aggregate
+- min_aggregate
+- select_count
+- select_where
+- select_where_count
+- select_where_sum
+- single_match
+- sum_aggregate
+- sum_where
+- take_count
+- take_count_filtered
+- take_sum_aggregate
+- take_while_match
+- skip_while_match
+- to_array_filter
+
+### Category B — clean decs map but needs Slice 5+ splice arms
+
+Chain shape works on decs surface today (via eager bridge), but splice planner doesn't yet handle the shape — would fall to tier-2 cascade. Listing here so each entry doubles as a future-slice trigger.
+
+- bare_order_where (order_by / reverse on decs — Slice 5+)
+- distinct_take (distinct_by + take on decs — Slice 5+)
+- element_at_match (element_at — Slice 5+)
+- groupby_average / groupby_count / groupby_first / groupby_having_count / groupby_having_hidden_sum / groupby_max / groupby_min / groupby_multi_reducer / groupby_select_sum / groupby_sum / groupby_where_count / groupby_where_sum — all need decs group_by splice (state-table family Slice 5+)
+- order_take_desc / select_where_order_take / sort_first / sort_take — order_by family on decs (Slice 5+)
+- reverse_take — reverse on decs (Slice 5+)
+- skip_take — skip+take chain on decs (Slice 5+)
+
+### Category C — needs new decs surface
+
+Benchmarks whose shape doesn't have a corresponding decs equivalent yet. Decision: build the surface OR skip.
+
+- **indexed_lookup** — SQL uses B-tree on `id`. Decs analog: **eid-based lookup** via `lookup_entity` (decs.das has `[eid]` component lookup). Build a `m4_decs_eid_lookup` lane that exercises this. NEW SURFACE: thin wrapper if needed.
+- **zip_dot_product** — pairs two parallel arrays. Decs analog: pair two component streams from the SAME archetype (intra-archetype zip is free — it's just multi-iter for over both components). Or zip across two archetypes (cross-archetype, harder). NEW SURFACE: `from_decs_template_zip` or syntactic-sugar wrapper. Worth investigating; could be a clean Slice surface add.
+- **join_count** — two tables joined. Decs analog: cross-archetype query with eid linkage. NEW SURFACE: `join_decs` or two-template iter. Larger design exercise; defer to follow-up.
+
+## Proposed execution order
+
+1. **Wave 1 — Category A only** (28 benchmarks). All splice today, all comparable now. Establishes the m4 surface in `_common.das` + per-bench m4 lane. Validates the lane convention before scope grows.
+2. **Wave 2 — Category C surface adds**:
+ - `indexed_lookup` via decs eid lookup (small change)
+ - `zip_dot_product` via decs intra-archetype zip (design discussion)
+ - `join_count` deferred to a later wave (full decs join design)
+3. **Wave 3 — Category B**: ship m4 lanes for these now using the eager bridge (so the matrix is complete), THEN as plan_decs_unroll Slice 5+ lands, the lanes start showing the splice win. Each lane stays valid throughout.
+
+## Conventions for m4 lanes
+
+Per Boris (2026-05-20):
+- One lane per benchmark, named `_m4` reported as `m4_decs_fold/{n}` (`m4` for ordinal, `decs_fold` for clarity)
+- Per-benchmark fixture call (each file calls `fixture_decs(n)` inline, mirroring how m3 calls `fixture_array(n)` inline)
+- Shared `[decs_template] DecsCar` + `fixture_decs(n)` in `_common.das` (mirrors how m3's `Car` + `fixture_array` are shared)
+- Lambda-typed args (`$(c : Car)`) replaced with `_select(_.field)` macro form in m4 bodies (Car type doesn't match the decs tuple element)
+- Sentinel values (`first_or_default`) use named-tuple literal `(id=…, name=…, …)` matching the iterator element type
+
+## First m4 sweep — results (2026-05-20, 100K rows, INTERP)
+
+47 benchmarks gained an m4 lane (all Cat A + Cat B). Skipped: `indexed_lookup`, `join_count`, `zip_dot_product` (Cat C, need surface).
+
+### Cat A — m4 splices today (chain shape covered by plan_decs_unroll)
+
+| benchmark | m1 | m3 | m3f | m4 | m4/m1 | m4/m3f | notes |
+|---|---:|---:|---:|---:|---:|---:|---|
+| sum_aggregate | 30 | 29 | 2 | **16** | 0.5x | 8x | 6-component multi-iter overhead floor |
+| select_where_sum | 36 | 57 | 7 | **18** | 0.5x | 2.6x | chain splice; beats m1 + m3 |
+| select_where_count | 32 | 58 | 5 | **18** | 0.6x | 3.6x | chain splice; beats m1 + m3 |
+| count_aggregate | 30 | 28 | 4 | **15** | 0.5x | 3.8x | (count with filter — not bare; filter walks all entities) |
+| long_count_aggregate | 29 | 28 | 4 | **15** | 0.5x | 3.8x | parallel to count |
+| sum_where | 32 | 45 | 4 | **17** | 0.5x | 4.2x | |
+| chained_where | 36 | 46 | 6 | **18** | 0.5x | 3x | |
+| select_where | 190 | 28 | 11 | **22** | **0.1x** | 2x | m4 8.6x faster than m1 SQL |
+| max_aggregate | 30 | 36 | 5 | **19** | 0.6x | 3.8x | |
+| min_aggregate | 30 | 38 | 6 | **19** | 0.6x | 3.2x | |
+| average_aggregate | 30 | 34 | 5 | **21** | 0.7x | 4.2x | |
+| all_match | 27 | 20 | 3 | **15** | 0.6x | 5x | early-exit on bridge |
+| to_array_filter | 70 | 43 | 11 | **24** | **0.3x** | 2.2x | m4 2.9x faster than m1 SQL |
+| first_match | 0 | 28 | 0 | **0** | — | — | early-exit on first hit |
+| first_or_default_match | 0 | 31 | 0 | **0** | — | — | |
+| any_match | 0 | 0 | 0 | **0** | — | — | |
+| contains_match | 0 | 28 | 2 | **8** | — | 4x | |
+
+**Net Cat A:** m4 beats SQL on most shapes (8 of 17 with concrete m1+m4 numbers; another 4 at 0 ns/op). m4 vs m3f shows a ~3-5× decs overhead from the 6-component multi-iter for-loop (every component's get_ro participates even when chain only reads one field). This is the splice's structural cost on a multi-field decs schema.
+
+### Cat B — m4 falls back to eager bridge (splice deferred to Slice 5+)
+
+These chain shapes splice on array but not yet on decs. m4 = eager bridge (materialize array, then run on array). Slower than m3f but the comparison is real today.
+
+| benchmark | m1 | m3 | m3f | m4 | m4/m3f | needs splice arm |
+|---|---:|---:|---:|---:|---:|---|
+| bare_order_where | 273 | 357 | 120 | 196 | 1.6x | order_by on decs |
+| distinct_count | 41 | 43 | 15 | 97 | 6.5x | distinct on decs |
+| distinct_take | 0 | 30 | 0 | 34 | — | distinct + take on decs |
+| order_take_desc | 37 | 698 | 27 | 117 | 4.3x | order_by + take |
+| reverse_take | 0 | 22 | 0 | 114 | — | reverse + take |
+| select_count | 0 | 32 | 0 | 3 | — | (m4 likely DCE — verify) |
+| select_where_order_take | 36 | 355 | 24 | 102 | 4.2x | order + take after where |
+| skip_take | 0 | 15 | 0 | 37 | — | take/skip on decs |
+| skip_while_match | 3 | 20 | 5 | 83 | 16.6x | skip_while on decs |
+| sort_first | 37 | 713 | 722 | 802 | 1.1x | sort dominates; splice barely helps |
+| sort_take | 38 | 715 | 27 | 119 | 4.4x | order + take |
+| take_count | 3 | 0 | 0 | 36 | — | take on decs |
+| take_count_filtered | — | 29 | 0 | 35 | — | take after where |
+| take_sum_aggregate | — | 28 | 0 | 34 | — | take + sum |
+| take_while_match | 7 | 22 | 2 | 55 | 27.5x | take_while on decs |
+| element_at_match | 0 | 28 | 0 | 35 | — | element_at on decs |
+| last_match | 0 | 29 | 5 | 83 | 16.6x | last on decs |
+| single_match | 0 | 19 | 2 | 80 | 40x | single on decs |
+| groupby_count | 142 | 65 | 37 | 115 | 3.1x | group_by on decs (state-table) |
+| groupby_sum | 171 | 101 | 36 | 115 | 3.2x | group_by on decs |
+| groupby_average | 172 | 99 | 52 | 128 | 2.5x | group_by on decs |
+| groupby_max | 174 | 103 | 43 | 120 | 2.8x | group_by on decs |
+| groupby_min | 175 | 105 | 42 | 122 | 2.9x | group_by on decs |
+| groupby_first | — | 68 | 35 | 112 | 3.2x | group_by on decs |
+| groupby_having_count | 142 | 71 | 37 | 114 | 3.1x | group_by on decs |
+| groupby_having_hidden_sum | 176 | 102 | 40 | 122 | 3.0x | group_by on decs |
+| groupby_multi_reducer | 191 | 138 | 52 | 130 | 2.5x | group_by on decs |
+| groupby_select_sum | — | 109 | 60 | 137 | 2.3x | group_by on decs |
+| groupby_where_count | 76 | 63 | 23 | 101 | 4.4x | group_by on decs |
+| groupby_where_sum | 101 | 81 | 23 | 105 | 4.6x | group_by on decs |
+| aggregate_match | 34 | 50 | 5 | 84 | 16.8x | `aggregate(init, $(acc, c) => …)` — not a `_select(_.x).sum()` shape, distinct planner |
+
+**Net Cat B:** Establishes today's baseline. As Slice 5+ lands group_by/order/distinct/take splice arms on the decs bridge, these rows will drop into Cat A territory and the splice win will become visible without changing the benchmark.
+
+### Suspect "0 ns/op" m4 results — verify the chain isn't getting DCE'd
+
+- `first_match` / `first_or_default_match` — early-exit on the first archetype's first entity is genuinely cheap; plausible
+- `any_match` / `select_count` — both 0 on m4 and m3f; likely constant-folded
+- `take_count` / `take_count_filtered` / `take_sum_aggregate` / `reverse_take` / `skip_take` / `distinct_take` — m4=34-37 ns (eager bridge); m3f=0 ns; m1 mostly 0 too. The m3f=0 cases are suspicious — bench may be eliminating the chain at compile time. Worth dropping a `b->failNow()` floor check or a side effect to confirm.
+
+## Wave 2 — surface expansions
+
+After this m4 sweep ships, expand the surface for Cat C benchmarks:
+
+1. **`indexed_lookup`** — add `m4_decs_eid_lookup` lane using `lookup_entity` / archetype hash lookup. Decs hash-of-eid IS the fast path; benchmark it against SQL B-tree. May require minor surface (a `find_entity_by_field` helper)
+2. **`zip_dot_product`** — design intra-archetype zip surface. Two components from the SAME archetype is just a multi-iter for-loop (free). Cross-archetype zip is harder. Pick the design that lets `_zip` on decs match the array_zip shape
+3. **`join_count`** — needs full decs join design. Multi-table query equivalent via two `[decs_template]` structs + eid linkage. Larger design exercise; defer
+
+## Wave 3 — Slice 5+ enables Cat B splice
+
+As `plan_decs_unroll` gains:
+- take/skip/take_while/skip_while arms (cross-archetype counter / for_each_archetype_find early-exit)
+- distinct/group_by state-table arms (hoisted table above outer for_each_archetype)
+- order_by / reverse buffer arms
+
+…Cat B m4 numbers will drop dramatically (from ~100-130 ns down to the 5-20 ns range), matching the Cat A pattern. The matrix becomes a regression guard for each splice arm.
+
+## Wave 4 — perf optimizations on plan_decs_unroll
+
+Known overhead in the current splice: **6-component multi-iter for-loop walks ALL components** even when the chain only reads one field. Possible optimizations:
+- Track per-chain "components actually accessed" set; emit `get_ro` + iter binding only for those
+- For bare-count chains: arch.size shortcut works (already implemented) → 0 ns
+- For single-field selects: 1-component for-loop should match the array case
+
+If implemented, m4 numbers on Cat A would close most of the 3-5× gap vs m3f, making decs effectively as fast as array for projection-heavy chains.
+
+## Update — sort_first fix (2026-05-20, plan_order_family + first arm)
+
+Extended `plan_order_family` to recognize `first` / `first_or_default` as terminators alongside `take(N)`. `order_by + first` now splices to a single-pass `min_by` (array source: zero-alloc empty-guard + `min_by`; iterator source: `top_n_by(_, 1, _) |> first()`). `order_by_descending` routes to `max_by`. Preserves the eager `first()` panic-on-empty contract.
+
+| benchmark | m1 | m3 | m3f (old) | m3f (new) | m4 (old) | m4 (new) | m3f win |
+|---|---:|---:|---:|---:|---:|---:|---:|
+| sort_first | 37 | 713 | 722 | **42** | 802 | **121** | 17× |
+
+Now `sort_first` lands in line with the rest of the order-family. m4_decs_fold still rides the eager bridge (Slice 5+ will close the gap).
+
+## Update — zip_dot_product fix (2026-05-20, plan_zip + accumulator/early-exit lanes)
+
+PR #2742's accumulator + early-exit terminator work on `plan_zip` was orphaned on a stacked PR base when #2741 merged. Cherry-picked the 3 commits onto fresh master (auto-merged cleanly). `plan_zip` now dispatches to the generalized multi-source `emit_accumulator_lane` (sum / min / max / average / long_count) and `emit_early_exit_lane` (first / first_or_default / any / all / contains) via parallel-array helpers (`srcNames`, `topExprs`) + new `finalize_lane_emission` wrap.
+
+| benchmark | m1 | m3 | m3f (old) | m3f (new) | m3f win |
+|---|---:|---:|---:|---:|---:|
+| zip_dot_product | — | 53 | 58 | **7** | 8.3× |
+
+`zip(xs, ys)._select(_._0 * _._1).sum()` now fuses to a single multi-iter for-loop with inline accumulator, zero alloc. Falls in line with the rest of the accumulator-class benchmarks.
diff --git a/benchmarks/sql/_common.das b/benchmarks/sql/_common.das
index 36c94e42f0..ffa12db7d3 100644
--- a/benchmarks/sql/_common.das
+++ b/benchmarks/sql/_common.das
@@ -7,6 +7,7 @@ require sqlite/sqlite_boost public
require sqlite/sqlite_linq public
require dastest/testing_boost public
require daslib/fio public
+require daslib/decs_boost public
let public BRAND_COUNT = 5
let public DEALER_COUNT = 100
@@ -75,3 +76,28 @@ def public fixture_dealers_array() : array {
}
return <- arr
}
+
+// m4_decs_fold lane: same Car schema, decs-archetype-backed. Same row generator as fixture_array so fold splice perf is directly comparable across SQL / array / decs.
+[decs_template(prefix = "car_")]
+struct public DecsCar {
+ id : int
+ name : string
+ price : int
+ brand : int
+ year : int
+ dealer_id : int
+}
+
+def public fixture_decs(n : int) {
+ restart()
+ create_entities(n) $(eid : EntityId; i : int; var cmp : ComponentMap) {
+ apply_decs_template(cmp, DecsCar(
+ id = i + 1,
+ name = "Car{i}",
+ price = (i * 37) % 1000,
+ brand = i % BRAND_COUNT,
+ year = 2010 + (i * 7) % 16,
+ dealer_id = (i % DEALER_COUNT) + 1
+ ))
+ }
+}
diff --git a/benchmarks/sql/aggregate_match.das b/benchmarks/sql/aggregate_match.das
index f21df6cd36..fd8363ac61 100644
--- a/benchmarks/sql/aggregate_match.das
+++ b/benchmarks/sql/aggregate_match.das
@@ -16,6 +16,7 @@ def run_m1(b : B?; n : int) {
b |> run("m1_sql/{n}", n) {
let total = _sql(db |> select_from(type) |> _where(_.price > THRESHOLD)
|> _select(_.price) |> sum())
+ b |> accept(total)
if (total == 0) {
b->failNow()
}
@@ -28,6 +29,7 @@ def run_m3(b : B?; n : int) {
b |> run("m3_array/{n}", n) {
let total = (arr |> _where(_.price > THRESHOLD)
|> aggregate(0, $(acc : int, c : Car) => acc + c.price))
+ b |> accept(total)
if (total == 0) {
b->failNow()
}
@@ -38,6 +40,19 @@ def run_m3f(b : B?; n : int) {
b |> run("m3f_array_fold/{n}", n) {
let total = _fold(each(arr)._where(_.price > THRESHOLD)
.aggregate(0, $(acc : int, c : Car) => acc + c.price))
+ b |> accept(total)
+ if (total == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let total = _fold(from_decs_template(type)._where(_.price > THRESHOLD)
+ .aggregate(0, $(acc, c) => acc + c.price))
+ b |> accept(total)
if (total == 0) {
b->failNow()
}
@@ -58,3 +73,8 @@ def aggregate_match_m3(b : B?) {
def aggregate_match_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def aggregate_match_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/all_match.das b/benchmarks/sql/all_match.das
index de705ce1df..b140da7537 100644
--- a/benchmarks/sql/all_match.das
+++ b/benchmarks/sql/all_match.das
@@ -14,6 +14,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let bad = _sql(db |> select_from(type) |> _where(_.price >= 9999) |> count())
+ b |> accept(bad)
if (bad != 0) {
b->failNow()
}
@@ -25,6 +26,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let yes = arr |> _all(_.price < 9999)
+ b |> accept(yes)
if (!yes) {
b->failNow()
}
@@ -34,6 +36,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let yes = _fold(each(arr)._all(_.price < 9999))
+ b |> accept(yes)
+ if (!yes) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let yes = _fold(from_decs_template(type)._all(_.price < 9999))
+ b |> accept(yes)
if (!yes) {
b->failNow()
}
@@ -54,3 +68,8 @@ def all_match_m3(b : B?) {
def all_match_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def all_match_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/any_match.das b/benchmarks/sql/any_match.das
index 70f694e9f6..55c2033e14 100644
--- a/benchmarks/sql/any_match.das
+++ b/benchmarks/sql/any_match.das
@@ -14,6 +14,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let opt = _sql(db |> select_from(type) |> _where(_.price > THRESHOLD) |> _first_opt())
+ b |> accept(opt)
if (!is_some(opt)) {
b->failNow()
}
@@ -25,6 +26,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let yes = arr |> _any(_.price > THRESHOLD)
+ b |> accept(yes)
if (!yes) {
b->failNow()
}
@@ -34,6 +36,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let yes = _fold(each(arr)._any(_.price > THRESHOLD))
+ b |> accept(yes)
+ if (!yes) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let yes = _fold(from_decs_template(type)._any(_.price > THRESHOLD))
+ b |> accept(yes)
if (!yes) {
b->failNow()
}
@@ -54,3 +68,8 @@ def any_match_m3(b : B?) {
def any_match_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def any_match_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/average_aggregate.das b/benchmarks/sql/average_aggregate.das
index af9b2adbd4..f7aa282438 100644
--- a/benchmarks/sql/average_aggregate.das
+++ b/benchmarks/sql/average_aggregate.das
@@ -11,6 +11,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let a = _sql(db |> select_from(type) |> _select(_.price) |> average())
+ b |> accept(a)
if (a == 0.0lf) {
b->failNow()
}
@@ -22,6 +23,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let a = arr |> _select(double(_.price)) |> average()
+ b |> accept(a)
if (a == 0.0lf) {
b->failNow()
}
@@ -31,6 +33,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let a = _fold(each(arr)._select(double(_.price)).average())
+ b |> accept(a)
+ if (a == 0.0lf) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let a = _fold(from_decs_template(type)._select(double(_.price)).average())
+ b |> accept(a)
if (a == 0.0lf) {
b->failNow()
}
@@ -51,3 +65,8 @@ def average_aggregate_m3(b : B?) {
def average_aggregate_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def average_aggregate_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/bare_order_where.das b/benchmarks/sql/bare_order_where.das
index 744b6805e7..400c24b3f0 100644
--- a/benchmarks/sql/bare_order_where.das
+++ b/benchmarks/sql/bare_order_where.das
@@ -18,6 +18,7 @@ def run_m1(b : B?; n : int) {
let rows <- _sql(db |> select_from(type)
|> _where(_.price > THRESHOLD)
|> _order_by(_.price))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -30,6 +31,7 @@ def run_m3(b : B?; n : int) {
b |> run("m3_array/{n}", n) {
let rows <- (arr |> _where(_.price > THRESHOLD)
|> _order_by(_.price))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -42,6 +44,20 @@ def run_m3f(b : B?; n : int) {
let rows <- _fold(each(arr)._where(_.price > THRESHOLD)
._order_by(_.price)
.to_array())
+ b |> accept(rows)
+ if (empty(rows)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let rows <- _fold(from_decs_template(type)._where(_.price > THRESHOLD)
+ ._order_by(_.price)
+ .to_array())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -62,3 +78,8 @@ def bare_order_where_m3(b : B?) {
def bare_order_where_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def bare_order_where_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/chained_where.das b/benchmarks/sql/chained_where.das
index b433d1ca11..2a37eea0b9 100644
--- a/benchmarks/sql/chained_where.das
+++ b/benchmarks/sql/chained_where.das
@@ -19,6 +19,7 @@ def run_m1(b : B?; n : int) {
|> _where(_.price > THRESHOLD)
|> _where(_.year >= YEAR_FLOOR)
|> count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -32,6 +33,7 @@ def run_m3(b : B?; n : int) {
let c = (arr |> _where(_.price > THRESHOLD)
|> _where(_.year >= YEAR_FLOOR)
|> count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -43,6 +45,20 @@ def run_m3f(b : B?; n : int) {
let c = _fold(each(arr)._where(_.price > THRESHOLD)
._where(_.year >= YEAR_FLOOR)
.count())
+ b |> accept(c)
+ if (c == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let c = _fold(from_decs_template(type)._where(_.price > THRESHOLD)
+ ._where(_.year >= YEAR_FLOOR)
+ .count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -63,3 +79,8 @@ def chained_where_m3(b : B?) {
def chained_where_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def chained_where_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/contains_match.das b/benchmarks/sql/contains_match.das
index 8eb6e2e976..e2ee872219 100644
--- a/benchmarks/sql/contains_match.das
+++ b/benchmarks/sql/contains_match.das
@@ -15,6 +15,7 @@ def run_m1(b : B?; n : int) {
b |> run("m1_sql/{n}", n) {
// SQL doesn't have a direct CONTAINS for arbitrary values; use _any with _where.
let opt = _sql(db |> select_from(type) |> _where(_.id == TARGET_ID) |> _first_opt())
+ b |> accept(opt)
if (!is_some(opt)) {
b->failNow()
}
@@ -28,6 +29,7 @@ def run_m3(b : B?; n : int) {
// Project ids out then contains. Mirrors what `_fold(...select.contains(...))` does
// — an array-source linq chain materializes `select` first then iterates contains.
let yes = arr |> _select(_.id) |> contains(TARGET_ID)
+ b |> accept(yes)
if (!yes) {
b->failNow()
}
@@ -37,6 +39,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let yes = _fold(each(arr)._select(_.id).contains(TARGET_ID))
+ b |> accept(yes)
+ if (!yes) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let yes = _fold(from_decs_template(type)._select(_.id).contains(TARGET_ID))
+ b |> accept(yes)
if (!yes) {
b->failNow()
}
@@ -57,3 +71,8 @@ def contains_match_m3(b : B?) {
def contains_match_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def contains_match_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/count_aggregate.das b/benchmarks/sql/count_aggregate.das
index f6d92dfca1..d0f770f80a 100644
--- a/benchmarks/sql/count_aggregate.das
+++ b/benchmarks/sql/count_aggregate.das
@@ -15,6 +15,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let c = _sql(db |> select_from(type) |> _where(_.price > THRESHOLD) |> count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -27,6 +28,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let c = arr |> _where(_.price > THRESHOLD) |> count()
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -38,6 +40,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let c = _fold(each(arr)._where(_.price > THRESHOLD).count())
+ b |> accept(c)
+ if (c == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let c = _fold(from_decs_template(type)._where(_.price > THRESHOLD).count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -58,3 +72,8 @@ def count_aggregate_m3(b : B?) {
def count_aggregate_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def count_aggregate_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/distinct_count.das b/benchmarks/sql/distinct_count.das
index 4ff234e8ee..290598dfc1 100644
--- a/benchmarks/sql/distinct_count.das
+++ b/benchmarks/sql/distinct_count.das
@@ -15,6 +15,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let rows <- _sql(db |> select_from(type) |> _select(_.brand) |> distinct())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -26,6 +27,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let rows <- (arr |> _select(_.brand) |> distinct())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -35,6 +37,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let rows <- _fold(each(arr)._select(_.brand).distinct().to_array())
+ b |> accept(rows)
+ if (empty(rows)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let rows <- _fold(from_decs_template(type)._select(_.brand).distinct().to_array())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -55,3 +69,8 @@ def distinct_count_m3(b : B?) {
def distinct_count_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def distinct_count_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/distinct_take.das b/benchmarks/sql/distinct_take.das
index 9afee6c444..2bb7d2394b 100644
--- a/benchmarks/sql/distinct_take.das
+++ b/benchmarks/sql/distinct_take.das
@@ -20,6 +20,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let rows <- _sql(db |> select_from(type) |> _select(_.brand) |> distinct() |> take(TAKE_N))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -32,6 +33,7 @@ def run_m3(b : B?; n : int) {
b |> run("m3_array/{n}", n) {
unsafe {
let rows <- (each(arr) |> _select(_.brand) |> distinct() |> take(TAKE_N) |> to_array())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -44,6 +46,20 @@ def run_m3f(b : B?; n : int) {
b |> run("m3f_array_fold/{n}", n) {
unsafe {
let rows <- _fold(each(arr)._select(_.brand).distinct().take(TAKE_N).to_array())
+ b |> accept(rows)
+ if (empty(rows)) {
+ b->failNow()
+ }
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ unsafe {
+ let rows <- _fold(from_decs_template(type)._select(_.brand).distinct().take(TAKE_N).to_array())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -65,3 +81,8 @@ def distinct_take_m3(b : B?) {
def distinct_take_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def distinct_take_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/element_at_match.das b/benchmarks/sql/element_at_match.das
index 61c51fa02c..114977ccd6 100644
--- a/benchmarks/sql/element_at_match.das
+++ b/benchmarks/sql/element_at_match.das
@@ -16,6 +16,7 @@ def run_m1(b : B?; n : int) {
b |> run("m1_sql/{n}", n) {
let row = _sql(db |> select_from(type) |> _where(_.price > THRESHOLD)
|> skip(INDEX) |> _first())
+ b |> accept(row)
if (row.price == 0) {
b->failNow()
}
@@ -27,6 +28,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let row = arr |> _where(_.price > THRESHOLD) |> element_at(INDEX)
+ b |> accept(row)
if (row.price == 0) {
b->failNow()
}
@@ -36,6 +38,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let row = _fold(each(arr)._where(_.price > THRESHOLD).element_at(INDEX))
+ b |> accept(row)
+ if (row.price == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let row = _fold(from_decs_template(type)._where(_.price > THRESHOLD).element_at(INDEX))
+ b |> accept(row)
if (row.price == 0) {
b->failNow()
}
@@ -56,3 +70,8 @@ def element_at_match_m3(b : B?) {
def element_at_match_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def element_at_match_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/first_match.das b/benchmarks/sql/first_match.das
index dee7604523..0531baa4cb 100644
--- a/benchmarks/sql/first_match.das
+++ b/benchmarks/sql/first_match.das
@@ -15,6 +15,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let row = _sql(db |> select_from(type) |> _where(_.price > THRESHOLD) |> _first())
+ b |> accept(row)
if (row.price == 0) {
b->failNow()
}
@@ -26,6 +27,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let row = arr |> _where(_.price > THRESHOLD) |> first()
+ b |> accept(row)
if (row.price == 0) {
b->failNow()
}
@@ -35,6 +37,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let row = _fold(each(arr)._where(_.price > THRESHOLD).first())
+ b |> accept(row)
+ if (row.price == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let row = _fold(from_decs_template(type)._where(_.price > THRESHOLD).first())
+ b |> accept(row)
if (row.price == 0) {
b->failNow()
}
@@ -55,3 +69,8 @@ def first_match_m3(b : B?) {
def first_match_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def first_match_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/first_or_default_match.das b/benchmarks/sql/first_or_default_match.das
index c67b3da74b..08585b560c 100644
--- a/benchmarks/sql/first_or_default_match.das
+++ b/benchmarks/sql/first_or_default_match.das
@@ -15,6 +15,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let row = _sql(db |> select_from(type) |> _where(_.price > THRESHOLD) |> _first())
+ b |> accept(row)
if (row.price == 0) {
b->failNow()
}
@@ -27,6 +28,7 @@ def run_m3(b : B?; n : int) {
let sentinel = Car(id = SENTINEL_ID, name = "none", price = 0, brand = 0, year = 0, dealer_id = 0)
b |> run("m3_array/{n}", n) {
let row = arr |> _where(_.price > THRESHOLD) |> first_or_default(sentinel)
+ b |> accept(row)
if (row.id == SENTINEL_ID) {
b->failNow()
}
@@ -37,6 +39,19 @@ def run_m3f(b : B?; n : int) {
let sentinel = Car(id = SENTINEL_ID, name = "none", price = 0, brand = 0, year = 0, dealer_id = 0)
b |> run("m3f_array_fold/{n}", n) {
let row = _fold(each(arr)._where(_.price > THRESHOLD).first_or_default(sentinel))
+ b |> accept(row)
+ if (row.id == SENTINEL_ID) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ let sentinel = (id = SENTINEL_ID, name = "none", price = 0, brand = 0, year = 0, dealer_id = 0)
+ b |> run("m4_decs_fold/{n}", n) {
+ let row = _fold(from_decs_template(type)._where(_.price > THRESHOLD).first_or_default(sentinel))
+ b |> accept(row)
if (row.id == SENTINEL_ID) {
b->failNow()
}
@@ -57,3 +72,8 @@ def first_or_default_match_m3(b : B?) {
def first_or_default_match_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def first_or_default_match_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/groupby_average.das b/benchmarks/sql/groupby_average.das
index c2dc5342db..801e168b5e 100644
--- a/benchmarks/sql/groupby_average.das
+++ b/benchmarks/sql/groupby_average.das
@@ -16,6 +16,7 @@ def run_m1(b : B?; n : int) {
|> _group_by(_.brand)
|> _select((Brand = _._0,
AvgPrice = _._1 |> select($(c : Car) => c.price) |> average())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -28,6 +29,7 @@ def run_m3(b : B?; n : int) {
b |> run("m3_array/{n}", n) {
let groups <- (arr._group_by(_.brand)._select((Brand = _._0,
AvgPrice = _._1 |> select($(c : Car) => c.price) |> average())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -41,6 +43,22 @@ def run_m3f(b : B?; n : int) {
._select((Brand = _._0,
AvgPrice = _._1 |> select($(c : Car) => c.price) |> average()))
.to_array())
+ b |> accept(groups)
+ if (empty(groups)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let groups <- _fold(from_decs_template(type)
+ ._group_by(_.brand)
+ ._select((Brand = _._0,
+ AvgPrice = _._1 |> _select(_.price) |> average()))
+ .to_array())
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -61,3 +79,8 @@ def groupby_average_m3(b : B?) {
def groupby_average_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def groupby_average_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/groupby_count.das b/benchmarks/sql/groupby_count.das
index 35179e08ff..7905028999 100644
--- a/benchmarks/sql/groupby_count.das
+++ b/benchmarks/sql/groupby_count.das
@@ -16,6 +16,7 @@ def run_m1(b : B?; n : int) {
let groups <- _sql(db |> select_from(type)
|> _group_by(_.brand)
|> _select((Brand = _._0, N = _._1 |> length)))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -27,6 +28,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let groups <- (arr._group_by(_.brand)._select((Brand = _._0, N = _._1 |> length)))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -39,6 +41,21 @@ def run_m3f(b : B?; n : int) {
._group_by(_.brand)
._select((Brand = _._0, N = _._1 |> length))
.to_array())
+ b |> accept(groups)
+ if (empty(groups)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let groups <- _fold(from_decs_template(type)
+ ._group_by(_.brand)
+ ._select((Brand = _._0, N = _._1 |> length))
+ .to_array())
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -59,3 +76,8 @@ def groupby_count_m3(b : B?) {
def groupby_count_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def groupby_count_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/groupby_first.das b/benchmarks/sql/groupby_first.das
index 217c9f80ed..22ed403c52 100644
--- a/benchmarks/sql/groupby_first.das
+++ b/benchmarks/sql/groupby_first.das
@@ -12,6 +12,7 @@ def run_m3(b : B?; n : int) {
b |> run("m3_array/{n}", n) {
let groups <- (arr._group_by(_.brand)._select((Brand = _._0,
FirstCar = _._1 |> first())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -25,6 +26,22 @@ def run_m3f(b : B?; n : int) {
._select((Brand = _._0,
FirstCar = _._1 |> first()))
.to_array())
+ b |> accept(groups)
+ if (empty(groups)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let groups <- _fold(from_decs_template(type)
+ ._group_by(_.brand)
+ ._select((Brand = _._0,
+ FirstCar = _._1 |> first()))
+ .to_array())
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -40,3 +57,8 @@ def groupby_first_m3(b : B?) {
def groupby_first_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def groupby_first_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/groupby_having_count.das b/benchmarks/sql/groupby_having_count.das
index 1a037ed27b..ae26f260ab 100644
--- a/benchmarks/sql/groupby_having_count.das
+++ b/benchmarks/sql/groupby_having_count.das
@@ -18,6 +18,7 @@ def run_m1(b : B?; n : int) {
|> _group_by(_.brand)
|> _having(_._1 |> length >= 5)
|> _select((Brand = _._0, N = _._1 |> length)))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -29,6 +30,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let groups <- (arr._group_by(_.brand)._having(_._1 |> length >= 5)._select((Brand = _._0, N = _._1 |> length)))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -42,6 +44,22 @@ def run_m3f(b : B?; n : int) {
._having(_._1 |> length >= 5)
._select((Brand = _._0, N = _._1 |> length))
.to_array())
+ b |> accept(groups)
+ if (empty(groups)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let groups <- _fold(from_decs_template(type)
+ ._group_by(_.brand)
+ ._having(_._1 |> length >= 5)
+ ._select((Brand = _._0, N = _._1 |> length))
+ .to_array())
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -62,3 +80,8 @@ def groupby_having_count_m3(b : B?) {
def groupby_having_count_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def groupby_having_count_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/groupby_having_hidden_sum.das b/benchmarks/sql/groupby_having_hidden_sum.das
index ab0e2cb696..a064247072 100644
--- a/benchmarks/sql/groupby_having_hidden_sum.das
+++ b/benchmarks/sql/groupby_having_hidden_sum.das
@@ -20,6 +20,7 @@ def run_m1(b : B?; n : int) {
|> _group_by(_.brand)
|> _having(_._1 |> select($(c : Car) => c.price) |> sum > 50000)
|> _select((Brand = _._0, N = _._1 |> length)))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -31,6 +32,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let groups <- (arr._group_by(_.brand)._having(_._1 |> select($(c : Car) => c.price) |> sum > 50000)._select((Brand = _._0, N = _._1 |> length)))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -44,6 +46,22 @@ def run_m3f(b : B?; n : int) {
._having(_._1 |> select($(c : Car) => c.price) |> sum > 50000)
._select((Brand = _._0, N = _._1 |> length))
.to_array())
+ b |> accept(groups)
+ if (empty(groups)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let groups <- _fold(from_decs_template(type)
+ ._group_by(_.brand)
+ ._having(_._1 |> _select(_.price) |> sum > 50000)
+ ._select((Brand = _._0, N = _._1 |> length))
+ .to_array())
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -64,3 +82,8 @@ def groupby_having_hidden_sum_m3(b : B?) {
def groupby_having_hidden_sum_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def groupby_having_hidden_sum_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/groupby_max.das b/benchmarks/sql/groupby_max.das
index a6faf71237..470bba5e2d 100644
--- a/benchmarks/sql/groupby_max.das
+++ b/benchmarks/sql/groupby_max.das
@@ -16,6 +16,7 @@ def run_m1(b : B?; n : int) {
|> _group_by(_.brand)
|> _select((Brand = _._0,
MaxPrice = _._1 |> select($(c : Car) => c.price) |> max())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -28,6 +29,7 @@ def run_m3(b : B?; n : int) {
b |> run("m3_array/{n}", n) {
let groups <- (arr._group_by(_.brand)._select((Brand = _._0,
MaxPrice = _._1 |> select($(c : Car) => c.price) |> max())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -41,6 +43,22 @@ def run_m3f(b : B?; n : int) {
._select((Brand = _._0,
MaxPrice = _._1 |> select($(c : Car) => c.price) |> max()))
.to_array())
+ b |> accept(groups)
+ if (empty(groups)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let groups <- _fold(from_decs_template(type)
+ ._group_by(_.brand)
+ ._select((Brand = _._0,
+ MaxPrice = _._1 |> _select(_.price) |> max()))
+ .to_array())
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -61,3 +79,8 @@ def groupby_max_m3(b : B?) {
def groupby_max_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def groupby_max_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/groupby_min.das b/benchmarks/sql/groupby_min.das
index c6e4935e80..7a12afb468 100644
--- a/benchmarks/sql/groupby_min.das
+++ b/benchmarks/sql/groupby_min.das
@@ -16,6 +16,7 @@ def run_m1(b : B?; n : int) {
|> _group_by(_.brand)
|> _select((Brand = _._0,
MinPrice = _._1 |> select($(c : Car) => c.price) |> min())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -28,6 +29,7 @@ def run_m3(b : B?; n : int) {
b |> run("m3_array/{n}", n) {
let groups <- (arr._group_by(_.brand)._select((Brand = _._0,
MinPrice = _._1 |> select($(c : Car) => c.price) |> min())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -41,6 +43,22 @@ def run_m3f(b : B?; n : int) {
._select((Brand = _._0,
MinPrice = _._1 |> select($(c : Car) => c.price) |> min()))
.to_array())
+ b |> accept(groups)
+ if (empty(groups)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let groups <- _fold(from_decs_template(type)
+ ._group_by(_.brand)
+ ._select((Brand = _._0,
+ MinPrice = _._1 |> _select(_.price) |> min()))
+ .to_array())
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -61,3 +79,8 @@ def groupby_min_m3(b : B?) {
def groupby_min_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def groupby_min_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/groupby_multi_reducer.das b/benchmarks/sql/groupby_multi_reducer.das
index 5baaa3264f..367c64f6de 100644
--- a/benchmarks/sql/groupby_multi_reducer.das
+++ b/benchmarks/sql/groupby_multi_reducer.das
@@ -18,6 +18,7 @@ def run_m1(b : B?; n : int) {
N = _._1 |> length,
TotalPrice = _._1 |> select($(c : Car) => c.price) |> sum(),
MaxPrice = _._1 |> select($(c : Car) => c.price) |> max())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -32,6 +33,7 @@ def run_m3(b : B?; n : int) {
N = _._1 |> length,
TotalPrice = _._1 |> select($(c : Car) => c.price) |> sum(),
MaxPrice = _._1 |> select($(c : Car) => c.price) |> max())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -47,6 +49,24 @@ def run_m3f(b : B?; n : int) {
TotalPrice = _._1 |> select($(c : Car) => c.price) |> sum(),
MaxPrice = _._1 |> select($(c : Car) => c.price) |> max()))
.to_array())
+ b |> accept(groups)
+ if (empty(groups)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let groups <- _fold(from_decs_template(type)
+ ._group_by(_.brand)
+ ._select((Brand = _._0,
+ N = _._1 |> length,
+ TotalPrice = _._1 |> _select(_.price) |> sum(),
+ MaxPrice = _._1 |> _select(_.price) |> max()))
+ .to_array())
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -67,3 +87,8 @@ def groupby_multi_reducer_m3(b : B?) {
def groupby_multi_reducer_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def groupby_multi_reducer_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/groupby_select_sum.das b/benchmarks/sql/groupby_select_sum.das
index 56fc3117c2..b9e49ec20d 100644
--- a/benchmarks/sql/groupby_select_sum.das
+++ b/benchmarks/sql/groupby_select_sum.das
@@ -12,6 +12,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let groups <- (arr._select(_.price)._group_by(_ % 100)._select((K = _._0, S = _._1 |> sum())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -25,6 +26,22 @@ def run_m3f(b : B?; n : int) {
._group_by(_ % 100)
._select((K = _._0, S = _._1 |> sum()))
.to_array())
+ b |> accept(groups)
+ if (empty(groups)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let groups <- _fold(from_decs_template(type)
+ ._select(_.price)
+ ._group_by(_ % 100)
+ ._select((K = _._0, S = _._1 |> sum()))
+ .to_array())
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -40,3 +57,8 @@ def groupby_select_sum_m3(b : B?) {
def groupby_select_sum_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def groupby_select_sum_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/groupby_sum.das b/benchmarks/sql/groupby_sum.das
index 52a256af29..ffacbd4d37 100644
--- a/benchmarks/sql/groupby_sum.das
+++ b/benchmarks/sql/groupby_sum.das
@@ -16,6 +16,7 @@ def run_m1(b : B?; n : int) {
|> _group_by(_.brand)
|> _select((Brand = _._0,
TotalPrice = _._1 |> select($(c : Car) => c.price) |> sum())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -28,6 +29,7 @@ def run_m3(b : B?; n : int) {
b |> run("m3_array/{n}", n) {
let groups <- (arr._group_by(_.brand)._select((Brand = _._0,
TotalPrice = _._1 |> select($(c : Car) => c.price) |> sum())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -41,6 +43,22 @@ def run_m3f(b : B?; n : int) {
._select((Brand = _._0,
TotalPrice = _._1 |> select($(c : Car) => c.price) |> sum()))
.to_array())
+ b |> accept(groups)
+ if (empty(groups)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let groups <- _fold(from_decs_template(type)
+ ._group_by(_.brand)
+ ._select((Brand = _._0,
+ TotalPrice = _._1 |> _select(_.price) |> sum()))
+ .to_array())
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -61,3 +79,8 @@ def groupby_sum_m3(b : B?) {
def groupby_sum_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def groupby_sum_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/groupby_where_count.das b/benchmarks/sql/groupby_where_count.das
index 658e5c739a..c66567a1c7 100644
--- a/benchmarks/sql/groupby_where_count.das
+++ b/benchmarks/sql/groupby_where_count.das
@@ -16,6 +16,7 @@ def run_m1(b : B?; n : int) {
|> _where(_.price > 500)
|> _group_by(_.brand)
|> _select((Brand = _._0, N = _._1 |> length)))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -27,6 +28,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let groups <- (arr._where(_.price > 500)._group_by(_.brand)._select((Brand = _._0, N = _._1 |> length)))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -40,6 +42,22 @@ def run_m3f(b : B?; n : int) {
._group_by(_.brand)
._select((Brand = _._0, N = _._1 |> length))
.to_array())
+ b |> accept(groups)
+ if (empty(groups)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let groups <- _fold(from_decs_template(type)
+ ._where(_.price > 500)
+ ._group_by(_.brand)
+ ._select((Brand = _._0, N = _._1 |> length))
+ .to_array())
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -60,3 +78,8 @@ def groupby_where_count_m3(b : B?) {
def groupby_where_count_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def groupby_where_count_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/groupby_where_sum.das b/benchmarks/sql/groupby_where_sum.das
index 07fa479258..843e68ffc2 100644
--- a/benchmarks/sql/groupby_where_sum.das
+++ b/benchmarks/sql/groupby_where_sum.das
@@ -17,6 +17,7 @@ def run_m1(b : B?; n : int) {
|> _group_by(_.brand)
|> _select((Brand = _._0,
TotalPrice = _._1 |> select($(c : Car) => c.price) |> sum())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -29,6 +30,7 @@ def run_m3(b : B?; n : int) {
b |> run("m3_array/{n}", n) {
let groups <- (arr._where(_.price > 500)._group_by(_.brand)._select((Brand = _._0,
TotalPrice = _._1 |> select($(c : Car) => c.price) |> sum())))
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -43,6 +45,23 @@ def run_m3f(b : B?; n : int) {
._select((Brand = _._0,
TotalPrice = _._1 |> select($(c : Car) => c.price) |> sum()))
.to_array())
+ b |> accept(groups)
+ if (empty(groups)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let groups <- _fold(from_decs_template(type)
+ ._where(_.price > 500)
+ ._group_by(_.brand)
+ ._select((Brand = _._0,
+ TotalPrice = _._1 |> _select(_.price) |> sum()))
+ .to_array())
+ b |> accept(groups)
if (empty(groups)) {
b->failNow()
}
@@ -63,3 +82,8 @@ def groupby_where_sum_m3(b : B?) {
def groupby_where_sum_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def groupby_where_sum_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/indexed_lookup.das b/benchmarks/sql/indexed_lookup.das
index 88aad127bf..afb4083dc9 100644
--- a/benchmarks/sql/indexed_lookup.das
+++ b/benchmarks/sql/indexed_lookup.das
@@ -15,6 +15,7 @@ def run_m1(b : B?; n : int) {
let key = n / 2
b |> run("m1_sql/{n}") {
let c = _sql(db |> select_from(type) |> _where(_.id == key) |> count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -28,6 +29,7 @@ def run_m3(b : B?; n : int) {
let key = n / 2
b |> run("m3_array/{n}") {
let c = arr |> _where(_.id == key) |> count()
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -40,6 +42,7 @@ def run_m3f(b : B?; n : int) {
let key = n / 2
b |> run("m3f_array_fold/{n}") {
let c = _fold(each(arr)._where(_.id == key).count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
diff --git a/benchmarks/sql/join_count.das b/benchmarks/sql/join_count.das
index aa4b275ac5..5c9af14664 100644
--- a/benchmarks/sql/join_count.das
+++ b/benchmarks/sql/join_count.das
@@ -17,6 +17,7 @@ def run_m3(b : B?; n : int) {
$(c : Car, d : Dealer) => c.dealer_id == d.id,
$(c : Car, d : Dealer) => (c.name, d.name))
|> count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -30,6 +31,7 @@ def run_m3f(b : B?; n : int) {
$(c : Car, d : Dealer) => c.dealer_id == d.id,
$(c : Car, d : Dealer) => (c.name, d.name))
|> count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
diff --git a/benchmarks/sql/last_match.das b/benchmarks/sql/last_match.das
index cc33f9a939..9a29f0b42f 100644
--- a/benchmarks/sql/last_match.das
+++ b/benchmarks/sql/last_match.das
@@ -15,6 +15,7 @@ def run_m1(b : B?; n : int) {
b |> run("m1_sql/{n}", n) {
let row = _sql(db |> select_from(type) |> _where(_.price > THRESHOLD)
|> _order_by_descending(_.id) |> _first())
+ b |> accept(row)
if (row.price == 0) {
b->failNow()
}
@@ -26,6 +27,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let row = arr |> _where(_.price > THRESHOLD) |> last()
+ b |> accept(row)
if (row.price == 0) {
b->failNow()
}
@@ -35,6 +37,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let row = _fold(each(arr)._where(_.price > THRESHOLD).last())
+ b |> accept(row)
+ if (row.price == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let row = _fold(from_decs_template(type)._where(_.price > THRESHOLD).last())
+ b |> accept(row)
if (row.price == 0) {
b->failNow()
}
@@ -55,3 +69,8 @@ def last_match_m3(b : B?) {
def last_match_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def last_match_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/long_count_aggregate.das b/benchmarks/sql/long_count_aggregate.das
index 3515e73835..627ca5f5f7 100644
--- a/benchmarks/sql/long_count_aggregate.das
+++ b/benchmarks/sql/long_count_aggregate.das
@@ -14,6 +14,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let c = _sql(db |> select_from(type) |> _where(_.price > THRESHOLD) |> count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -25,6 +26,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let c = arr |> _where(_.price > THRESHOLD) |> long_count()
+ b |> accept(c)
if (c == 0l) {
b->failNow()
}
@@ -34,6 +36,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let c = _fold(each(arr)._where(_.price > THRESHOLD).long_count())
+ b |> accept(c)
+ if (c == 0l) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let c = _fold(from_decs_template(type)._where(_.price > THRESHOLD).long_count())
+ b |> accept(c)
if (c == 0l) {
b->failNow()
}
@@ -54,3 +68,8 @@ def long_count_aggregate_m3(b : B?) {
def long_count_aggregate_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def long_count_aggregate_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/max_aggregate.das b/benchmarks/sql/max_aggregate.das
index ff424e0f4c..50f4049db7 100644
--- a/benchmarks/sql/max_aggregate.das
+++ b/benchmarks/sql/max_aggregate.das
@@ -11,6 +11,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let m = _sql(db |> select_from(type) |> _select(_.price) |> max())
+ b |> accept(m)
if (m == 0) {
b->failNow()
}
@@ -22,6 +23,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let m = arr |> _select(_.price) |> max()
+ b |> accept(m)
if (m == 0) {
b->failNow()
}
@@ -31,6 +33,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let m = _fold(each(arr)._select(_.price).max())
+ b |> accept(m)
+ if (m == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let m = _fold(from_decs_template(type)._select(_.price).max())
+ b |> accept(m)
if (m == 0) {
b->failNow()
}
@@ -51,3 +65,8 @@ def max_aggregate_m3(b : B?) {
def max_aggregate_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def max_aggregate_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/min_aggregate.das b/benchmarks/sql/min_aggregate.das
index f0bcbd37e9..efd61dbf90 100644
--- a/benchmarks/sql/min_aggregate.das
+++ b/benchmarks/sql/min_aggregate.das
@@ -11,6 +11,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let m = _sql(db |> select_from(type) |> _select(_.price) |> min())
+ b |> accept(m)
if (m > 999) {
b->failNow()
}
@@ -22,6 +23,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let m = arr |> _select(_.price) |> min()
+ b |> accept(m)
if (m > 999) {
b->failNow()
}
@@ -31,6 +33,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let m = _fold(each(arr)._select(_.price).min())
+ b |> accept(m)
+ if (m > 999) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let m = _fold(from_decs_template(type)._select(_.price).min())
+ b |> accept(m)
if (m > 999) {
b->failNow()
}
@@ -51,3 +65,8 @@ def min_aggregate_m3(b : B?) {
def min_aggregate_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def min_aggregate_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/order_take_desc.das b/benchmarks/sql/order_take_desc.das
index c4b61c510f..ad87ceb792 100644
--- a/benchmarks/sql/order_take_desc.das
+++ b/benchmarks/sql/order_take_desc.das
@@ -16,6 +16,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let rows <- _sql(db |> select_from(type) |> _order_by_descending(_.price) |> take(TAKE_N))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -27,6 +28,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let rows <- (arr |> _order_by_descending(_.price) |> take(TAKE_N))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -37,6 +39,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let rows <- _fold(each(arr)._order_by_descending(_.price).take(TAKE_N).to_array())
+ b |> accept(rows)
+ if (empty(rows)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let rows <- _fold(from_decs_template(type)._order_by_descending(_.price).take(TAKE_N).to_array())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -57,3 +71,8 @@ def order_take_desc_m3(b : B?) {
def order_take_desc_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def order_take_desc_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/reverse_take.das b/benchmarks/sql/reverse_take.das
index 4d8fe5eb67..79194c0822 100644
--- a/benchmarks/sql/reverse_take.das
+++ b/benchmarks/sql/reverse_take.das
@@ -19,6 +19,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let rows <- _sql(db |> select_from(type) |> _order_by_descending(_.id) |> take(TAKE_N))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -31,6 +32,7 @@ def run_m3(b : B?; n : int) {
b |> run("m3_array/{n}", n) {
unsafe {
let rows <- (each(arr) |> reverse() |> take(TAKE_N) |> to_array())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -43,6 +45,20 @@ def run_m3f(b : B?; n : int) {
b |> run("m3f_array_fold/{n}", n) {
unsafe {
let rows <- _fold(each(arr).reverse().take(TAKE_N).to_array())
+ b |> accept(rows)
+ if (empty(rows)) {
+ b->failNow()
+ }
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ unsafe {
+ let rows <- _fold(from_decs_template(type).reverse().take(TAKE_N).to_array())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -64,3 +80,8 @@ def reverse_take_m3(b : B?) {
def reverse_take_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def reverse_take_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/select_count.das b/benchmarks/sql/select_count.das
index 2291504e4a..87284405cf 100644
--- a/benchmarks/sql/select_count.das
+++ b/benchmarks/sql/select_count.das
@@ -15,6 +15,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let c = _sql(db |> select_from(type) |> count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -26,6 +27,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let c = arr |> _select(_.price * 2) |> count()
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -35,6 +37,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let c = _fold(each(arr)._select(_.price * 2).count())
+ b |> accept(c)
+ if (c == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let c = _fold(from_decs_template(type)._select(_.price * 2).count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -55,3 +69,8 @@ def select_count_m3(b : B?) {
def select_count_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def select_count_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/select_where.das b/benchmarks/sql/select_where.das
index 1aefe7dddc..4565a4e938 100644
--- a/benchmarks/sql/select_where.das
+++ b/benchmarks/sql/select_where.das
@@ -11,6 +11,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let rows <- _sql(db |> select_from(type) |> _where(_.price > THRESHOLD))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -23,6 +24,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let rows <- (arr |> _where(_.price > THRESHOLD))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -34,6 +36,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let rows <- _fold(each(arr)._where(_.price > THRESHOLD).to_array())
+ b |> accept(rows)
+ if (empty(rows)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let rows <- _fold(from_decs_template(type)._where(_.price > THRESHOLD).to_array())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -54,3 +68,8 @@ def select_where_m3(b : B?) {
def select_where_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def select_where_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/select_where_count.das b/benchmarks/sql/select_where_count.das
index 23587db9f6..7d9db90607 100644
--- a/benchmarks/sql/select_where_count.das
+++ b/benchmarks/sql/select_where_count.das
@@ -19,6 +19,7 @@ def run_m1(b : B?; n : int) {
// SQL form folds projection into the WHERE filter — the engine evaluates
// ``price * 2 > T`` per row and counts matches.
let c = _sql(db |> select_from(type) |> _where(_.price * 2 > THRESHOLD) |> count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -30,6 +31,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let c = arr |> _select(_.price * 2) |> _where(_ > THRESHOLD) |> count
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -40,6 +42,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let c = _fold(each(arr)._select(_.price * 2)._where(_ > THRESHOLD).count())
+ b |> accept(c)
+ if (c == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let c = _fold(from_decs_template(type)._select(_.price * 2)._where(_ > THRESHOLD).count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -60,3 +74,8 @@ def select_where_count_m3(b : B?) {
def select_where_count_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def select_where_count_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/select_where_order_take.das b/benchmarks/sql/select_where_order_take.das
index ccf57c0454..de68360e48 100644
--- a/benchmarks/sql/select_where_order_take.das
+++ b/benchmarks/sql/select_where_order_take.das
@@ -15,6 +15,7 @@ def run_m1(b : B?; n : int) {
|> _where(_.price > THRESHOLD)
|> _order_by(_.price)
|> take(TAKE_N))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -29,6 +30,7 @@ def run_m3(b : B?; n : int) {
let rows <- (arr |> _where(_.price > THRESHOLD)
|> _order_by(_.price)
|> take(TAKE_N))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -43,6 +45,21 @@ def run_m3f(b : B?; n : int) {
._order_by(_.price)
.take(TAKE_N)
.to_array())
+ b |> accept(rows)
+ if (empty(rows)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let rows <- _fold(from_decs_template(type)._where(_.price > THRESHOLD)
+ ._order_by(_.price)
+ .take(TAKE_N)
+ .to_array())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -63,3 +80,8 @@ def select_where_order_take_m3(b : B?) {
def select_where_order_take_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def select_where_order_take_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/select_where_sum.das b/benchmarks/sql/select_where_sum.das
index 0f693c9b2b..8bd80b73c2 100644
--- a/benchmarks/sql/select_where_sum.das
+++ b/benchmarks/sql/select_where_sum.das
@@ -26,6 +26,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let s = db |> query_scalar("SELECT SUM(price * 2) FROM Cars WHERE price * 2 > {THRESHOLD}", type)
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
@@ -37,6 +38,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let s = arr |> _select(_.price * 2) |> _where(_ > THRESHOLD) |> sum
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
@@ -47,6 +49,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let s = _fold(each(arr)._select(_.price * 2)._where(_ > THRESHOLD).sum())
+ b |> accept(s)
+ if (s == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let s = _fold(from_decs_template(type)._select(_.price * 2)._where(_ > THRESHOLD).sum())
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
@@ -67,3 +81,8 @@ def select_where_sum_m3(b : B?) {
def select_where_sum_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def select_where_sum_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/single_match.das b/benchmarks/sql/single_match.das
index 863e71adad..d14ae1b4c7 100644
--- a/benchmarks/sql/single_match.das
+++ b/benchmarks/sql/single_match.das
@@ -16,6 +16,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let row = _sql(db |> select_from(type) |> _where(_.id == TARGET_ID) |> _first())
+ b |> accept(row)
if (row.id == 0) {
b->failNow()
}
@@ -27,6 +28,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let row = arr |> _where(_.id == TARGET_ID) |> single()
+ b |> accept(row)
if (row.id == 0) {
b->failNow()
}
@@ -36,6 +38,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let row = _fold(each(arr)._where(_.id == TARGET_ID).single())
+ b |> accept(row)
+ if (row.id == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let row = _fold(from_decs_template(type)._where(_.id == TARGET_ID).single())
+ b |> accept(row)
if (row.id == 0) {
b->failNow()
}
@@ -56,3 +70,8 @@ def single_match_m3(b : B?) {
def single_match_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def single_match_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/skip_take.das b/benchmarks/sql/skip_take.das
index 52e000837c..4319781100 100644
--- a/benchmarks/sql/skip_take.das
+++ b/benchmarks/sql/skip_take.das
@@ -16,6 +16,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let rows <- _sql(db |> select_from(type) |> skip(SKIP_N) |> take(TAKE_N))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -27,6 +28,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let rows <- (arr |> skip(SKIP_N) |> take(TAKE_N))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -36,6 +38,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let rows <- _fold(each(arr).skip(SKIP_N).take(TAKE_N).to_array())
+ b |> accept(rows)
+ if (empty(rows)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let rows <- _fold(from_decs_template(type).skip(SKIP_N).take(TAKE_N).to_array())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -56,3 +70,8 @@ def skip_take_m3(b : B?) {
def skip_take_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def skip_take_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/skip_while_match.das b/benchmarks/sql/skip_while_match.das
index 32ebd1bd30..05aa398538 100644
--- a/benchmarks/sql/skip_while_match.das
+++ b/benchmarks/sql/skip_while_match.das
@@ -20,6 +20,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let total = _sql(db |> select_from(type) |> _where(_.id >= THRESHOLD) |> count())
+ b |> accept(total)
if (total == 0) {
b->failNow()
}
@@ -31,6 +32,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let total = arr |> _skip_while(_.id < THRESHOLD) |> count()
+ b |> accept(total)
if (total == 0) {
b->failNow()
}
@@ -40,6 +42,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let total = _fold(each(arr)._skip_while(_.id < THRESHOLD).count())
+ b |> accept(total)
+ if (total == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let total = _fold(from_decs_template(type)._skip_while(_.id < THRESHOLD).count())
+ b |> accept(total)
if (total == 0) {
b->failNow()
}
@@ -60,3 +74,8 @@ def skip_while_match_m3(b : B?) {
def skip_while_match_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def skip_while_match_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/sort_first.das b/benchmarks/sql/sort_first.das
index 290be82624..204c394c04 100644
--- a/benchmarks/sql/sort_first.das
+++ b/benchmarks/sql/sort_first.das
@@ -12,6 +12,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let row = _sql(db |> select_from(type) |> _order_by(_.price) |> _first())
+ b |> accept(row)
if (row.id == 0) {
b->failNow()
}
@@ -23,6 +24,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let row = arr |> _order_by(_.price) |> first()
+ b |> accept(row)
if (row.id == 0) {
b->failNow()
}
@@ -32,6 +34,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let row = _fold(each(arr)._order_by(_.price).first())
+ b |> accept(row)
+ if (row.id == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let row = _fold(from_decs_template(type)._order_by(_.price).first())
+ b |> accept(row)
if (row.id == 0) {
b->failNow()
}
@@ -52,3 +66,8 @@ def sort_first_m3(b : B?) {
def sort_first_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def sort_first_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/sort_take.das b/benchmarks/sql/sort_take.das
index 5787d519c8..2aaa81a4a1 100644
--- a/benchmarks/sql/sort_take.das
+++ b/benchmarks/sql/sort_take.das
@@ -24,6 +24,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let rows <- _sql(db |> select_from(type) |> _order_by(_.price) |> take(TAKE_N))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -35,6 +36,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let rows <- (arr |> _order_by(_.price) |> take(TAKE_N))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -45,6 +47,20 @@ def run_m3f(b : B?; n : int) {
b |> run("m3f_array_fold/{n}", n) {
unsafe {
let rows <- _fold(each(arr)._order_by(_.price).take(TAKE_N).to_array())
+ b |> accept(rows)
+ if (empty(rows)) {
+ b->failNow()
+ }
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ unsafe {
+ let rows <- _fold(from_decs_template(type)._order_by(_.price).take(TAKE_N).to_array())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -56,6 +72,7 @@ def run_m3_topn_array(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_topn_array/{n}", n) {
let rows <- top_n_by(arr, TAKE_N, @@(c : Car -&) => c.price)
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -66,6 +83,7 @@ def run_m3_topn_iter(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_topn_iter/{n}", n) {
let rows <- top_n_by(arr.to_sequence(), TAKE_N, @@(c : Car -&) => c.price)
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -96,3 +114,8 @@ def sort_take_m3_topn_array(b : B?) {
def sort_take_m3_topn_iter(b : B?) {
run_m3_topn_iter(b, 100000)
}
+
+[benchmark]
+def sort_take_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/sum_aggregate.das b/benchmarks/sql/sum_aggregate.das
index a8999e6474..e67385aaf9 100644
--- a/benchmarks/sql/sum_aggregate.das
+++ b/benchmarks/sql/sum_aggregate.das
@@ -11,6 +11,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let s = _sql(db |> select_from(type) |> _select(_.price) |> sum())
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
@@ -22,6 +23,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let s = arr |> _select(_.price) |> sum()
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
@@ -31,6 +33,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let s = _fold(each(arr)._select(_.price).sum())
+ b |> accept(s)
+ if (s == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let s = _fold(from_decs_template(type)._select(_.price).sum())
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
@@ -51,3 +65,8 @@ def sum_aggregate_m3(b : B?) {
def sum_aggregate_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def sum_aggregate_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/sum_where.das b/benchmarks/sql/sum_where.das
index 536fa47c02..638404c7ba 100644
--- a/benchmarks/sql/sum_where.das
+++ b/benchmarks/sql/sum_where.das
@@ -15,6 +15,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let s = _sql(db |> select_from(type) |> _where(_.price > THRESHOLD) |> _select(_.price) |> sum())
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
@@ -26,6 +27,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let s = (arr |> _where(_.price > THRESHOLD) |> _select(_.price) |> sum())
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
@@ -35,6 +37,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let s = _fold(each(arr)._where(_.price > THRESHOLD)._select(_.price).sum())
+ b |> accept(s)
+ if (s == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let s = _fold(from_decs_template(type)._where(_.price > THRESHOLD)._select(_.price).sum())
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
@@ -55,3 +69,8 @@ def sum_where_m3(b : B?) {
def sum_where_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def sum_where_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/take_count.das b/benchmarks/sql/take_count.das
index fdf10f352e..e7f8c5eb87 100644
--- a/benchmarks/sql/take_count.das
+++ b/benchmarks/sql/take_count.das
@@ -14,6 +14,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let rows <- _sql(db |> select_from(type) |> take(TAKE_N))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -25,6 +26,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let rows <- (arr |> take(TAKE_N))
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -34,6 +36,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let rows <- _fold(each(arr).take(TAKE_N).to_array())
+ b |> accept(rows)
+ if (empty(rows)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let rows <- _fold(from_decs_template(type).take(TAKE_N).to_array())
+ b |> accept(rows)
if (empty(rows)) {
b->failNow()
}
@@ -54,3 +68,8 @@ def take_count_m3(b : B?) {
def take_count_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def take_count_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/take_count_filtered.das b/benchmarks/sql/take_count_filtered.das
index dce9967ace..d738307279 100644
--- a/benchmarks/sql/take_count_filtered.das
+++ b/benchmarks/sql/take_count_filtered.das
@@ -14,6 +14,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let c = arr |> _where(_.price > THRESHOLD) |> take(TAKE_N) |> count()
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -23,6 +24,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let c = _fold(each(arr)._where(_.price > THRESHOLD).take(TAKE_N).count())
+ b |> accept(c)
+ if (c == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let c = _fold(from_decs_template(type)._where(_.price > THRESHOLD).take(TAKE_N).count())
+ b |> accept(c)
if (c == 0) {
b->failNow()
}
@@ -38,3 +51,8 @@ def take_count_filtered_m3(b : B?) {
def take_count_filtered_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def take_count_filtered_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/take_sum_aggregate.das b/benchmarks/sql/take_sum_aggregate.das
index 738ee4b2c8..617b66c813 100644
--- a/benchmarks/sql/take_sum_aggregate.das
+++ b/benchmarks/sql/take_sum_aggregate.das
@@ -14,6 +14,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let s = arr |> _select(_.price) |> take(TAKE_N) |> sum()
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
@@ -23,6 +24,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let s = _fold(each(arr)._select(_.price).take(TAKE_N).sum())
+ b |> accept(s)
+ if (s == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let s = _fold(from_decs_template(type)._select(_.price).take(TAKE_N).sum())
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
@@ -38,3 +51,8 @@ def take_sum_aggregate_m3(b : B?) {
def take_sum_aggregate_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def take_sum_aggregate_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/take_while_match.das b/benchmarks/sql/take_while_match.das
index b6e7da9925..86d419ed2b 100644
--- a/benchmarks/sql/take_while_match.das
+++ b/benchmarks/sql/take_while_match.das
@@ -19,6 +19,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let total = _sql(db |> select_from(type) |> _where(_.id < THRESHOLD) |> count())
+ b |> accept(total)
if (total == 0) {
b->failNow()
}
@@ -30,6 +31,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let total = arr |> _take_while(_.id < THRESHOLD) |> count()
+ b |> accept(total)
if (total == 0) {
b->failNow()
}
@@ -39,6 +41,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let total = _fold(each(arr)._take_while(_.id < THRESHOLD).count())
+ b |> accept(total)
+ if (total == 0) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let total = _fold(from_decs_template(type)._take_while(_.id < THRESHOLD).count())
+ b |> accept(total)
if (total == 0) {
b->failNow()
}
@@ -59,3 +73,8 @@ def take_while_match_m3(b : B?) {
def take_while_match_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def take_while_match_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/to_array_filter.das b/benchmarks/sql/to_array_filter.das
index d3b6a762a4..62e6b8eb6e 100644
--- a/benchmarks/sql/to_array_filter.das
+++ b/benchmarks/sql/to_array_filter.das
@@ -13,6 +13,7 @@ def run_m1(b : B?; n : int) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let prices <- _sql(db |> select_from(type) |> _where(_.price > THRESHOLD) |> _select(_.price))
+ b |> accept(prices)
if (empty(prices)) {
b->failNow()
}
@@ -24,6 +25,7 @@ def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let prices <- (arr |> _where(_.price > THRESHOLD) |> _select(_.price))
+ b |> accept(prices)
if (empty(prices)) {
b->failNow()
}
@@ -33,6 +35,18 @@ def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let prices <- _fold(each(arr)._where(_.price > THRESHOLD)._select(_.price).to_array())
+ b |> accept(prices)
+ if (empty(prices)) {
+ b->failNow()
+ }
+ }
+}
+
+def run_m4(b : B?; n : int) {
+ fixture_decs(n)
+ b |> run("m4_decs_fold/{n}", n) {
+ let prices <- _fold(from_decs_template(type)._where(_.price > THRESHOLD)._select(_.price).to_array())
+ b |> accept(prices)
if (empty(prices)) {
b->failNow()
}
@@ -53,3 +67,8 @@ def to_array_filter_m3(b : B?) {
def to_array_filter_m3f(b : B?) {
run_m3f(b, 100000)
}
+
+[benchmark]
+def to_array_filter_m4(b : B?) {
+ run_m4(b, 100000)
+}
diff --git a/benchmarks/sql/zip_dot_product.das b/benchmarks/sql/zip_dot_product.das
index b23aed7804..6d5dd33bce 100644
--- a/benchmarks/sql/zip_dot_product.das
+++ b/benchmarks/sql/zip_dot_product.das
@@ -21,6 +21,7 @@ def run_m3(b : B?; n : int) {
let ys <- make_ints(n)
b |> run("m3_array/{n}", n) {
let s = (zip(xs, ys) |> _select(_._0 * _._1) |> sum())
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
@@ -31,6 +32,7 @@ def run_m3f(b : B?; n : int) {
let ys <- make_ints(n)
b |> run("m3f_array_fold/{n}", n) {
let s = _fold(zip(xs, ys)._select(_._0 * _._1).sum())
+ b |> accept(s)
if (s == 0) {
b->failNow()
}
diff --git a/daslib/linq.das b/daslib/linq.das
index 33ecdaaa8c..e47989b446 100644
--- a/daslib/linq.das
+++ b/daslib/linq.das
@@ -854,6 +854,28 @@ def long_count(a : array) : int64 {
return int64(length(a))
}
+def long_count(var a : iterator; predicate : block<(arg : TT -&) : bool>) : int64 {
+ //! Counts elements in an iterator that satisfy a predicate, using a long integer
+ var count = 0l
+ for (it in a) {
+ if (predicate(it)) {
+ count ++
+ }
+ }
+ return count
+}
+
+def long_count(a : array; predicate : block<(arg : TT -&) : bool>) : int64 {
+ //! Counts elements in an array that satisfy a predicate, using a long integer
+ var count = 0l
+ for (it in a) {
+ if (predicate(it)) {
+ count ++
+ }
+ }
+ return count
+}
+
[unused_argument(tt)]
def private where_impl(var src; tt : auto(TT); len : int; predicate : block<(arg : TT -&) : bool>) : array {
//! Filters elements in an iterator based on a predicate
@@ -1526,7 +1548,11 @@ def average(var src : iterator) : double {
var total : double = 0lf
var count : uint64 = 0ul
for (x in src) {
- total += double(x)
+ static_if (typeinfo stripped_typename(x) == typeinfo stripped_typename(default)) {
+ total += x
+ } else {
+ total += double(x)
+ }
count ++
}
return count != 0ul ? total / double(count) : 0lf
@@ -1538,7 +1564,11 @@ def average(src : array) : double {
var total : double = 0lf
var count : uint64 = 0ul
for (x in src) {
- total += double(x)
+ static_if (typeinfo stripped_typename(x) == typeinfo stripped_typename(default)) {
+ total += x
+ } else {
+ total += double(x)
+ }
count ++
}
return count != 0ul ? total / double(count) : 0lf
diff --git a/daslib/linq_boost.das b/daslib/linq_boost.das
index ecfcb9aec4..bb7b848dc8 100644
--- a/daslib/linq_boost.das
+++ b/daslib/linq_boost.das
@@ -197,6 +197,16 @@ class private LinqCount : AstCallMacro_LinqPred2 {
override predName = "count"
}
+[call_macro(name="_long_count")]
+class private LinqLongCount : AstCallMacro_LinqPred2 {
+ //! implements _long_count(iterator, expression) shorthand notation
+ //! that expands into long_count(iterator, $(_) => expression)
+ //! for example::
+ //!
+ //! each(foo)._long_count(_ > 3)
+ override predName = "long_count"
+}
+
[call_macro(name="_unique_by")]
class private LinqUnique : AstCallMacro_LinqPred2 {
//! implements _unique_by(iterator, expression) shorthand notation
diff --git a/daslib/linq_fold.das b/daslib/linq_fold.das
index 47ec7f7e72..ded6544ee2 100644
--- a/daslib/linq_fold.das
+++ b/daslib/linq_fold.das
@@ -415,23 +415,60 @@ def private peel_each(var top : Expression?) : Expression? {
[macro_function]
def private finalize_invoke(var res : Expression?; at : LineInfo) : Expression? {
- // Post-emit cleanup: stamp loc+generated for diagnostics; set can_shadow so gensym src coexists with user scope.
+ // Post-emit cleanup: stamp loc+generated for diagnostics; set can_shadow on every block arg so gensym sources coexist with user scope. Loop handles 1-source single planners and N-source zip emission uniformly.
res.force_at(at)
res.force_generated(true)
let blk = (res as ExprInvoke).arguments[0] as ExprMakeBlock
- (blk._block as ExprBlock).arguments[0].flags.can_shadow = true
+ var blkBlock = blk._block as ExprBlock
+ for (i in 0 .. length(blkBlock.arguments)) {
+ blkBlock.arguments[i].flags.can_shadow = true
+ }
return res
}
+[macro_function]
+def private finalize_lane_emission(var topExprs : array; srcNames : array;
+ var bodyStmts : array; at : LineInfo) : Expression? {
+ // Multi-source-aware invoke wrap used by emit_accumulator_lane / emit_early_exit_lane. 1-source clones top + derives param type + emits 1-arg invoke; 2-source (zip) does both sides + emits 2-arg invoke. Caller has already pushed the for-loop into bodyStmts.
+ let nSrcs = length(srcNames)
+ if (nSrcs != 1 && nSrcs != 2) panic("finalize_lane_emission: only 1- or 2-source supported (got {nSrcs}); higher-arity zip planners must extend this branch (or build the invoke wrap directly)")
+ if (length(topExprs) != nSrcs) panic("finalize_lane_emission: topExprs length {length(topExprs)} != srcNames length {nSrcs}")
+ if (nSrcs == 1) {
+ var topExpr = clone_expression(topExprs[0])
+ topExpr.genFlags.alwaysSafe = true
+ var srcParamType = invoke_src_param_type(topExprs[0])
+ var res = qmacro(invoke($($i(srcNames[0]) : $t(srcParamType)) {
+ $b(bodyStmts)
+ }, $e(topExpr)))
+ return finalize_invoke(res, at)
+ }
+ var topAExpr = clone_expression(topExprs[0])
+ topAExpr.genFlags.alwaysSafe = true
+ var topBExpr = clone_expression(topExprs[1])
+ topBExpr.genFlags.alwaysSafe = true
+ var srcAType = invoke_src_param_type(topExprs[0])
+ var srcBType = invoke_src_param_type(topExprs[1])
+ var res = qmacro(invoke($($i(srcNames[0]) : $t(srcAType), $i(srcNames[1]) : $t(srcBType)) {
+ $b(bodyStmts)
+ }, $e(topAExpr), $e(topBExpr)))
+ return finalize_invoke(res, at)
+}
+
[macro_function]
def private emit_length_shortcut(opName : string; var top : Expression?; srcName : string; at : LineInfo) : Expression? {
- // Count-shaped shortcut: emit `length(src)` (count) or `int64(length(src))` (long_count)
+ // Count-shaped shortcut: emit `length(src)` (count, int) or `int64(length(src))` (long_count). length returns int, so the int() cast on count would be redundant — interp doesn't fold it and PERF020 fires.
var topExpr = clone_expression(top)
topExpr.genFlags.alwaysSafe = true
- let castName = opName == "long_count" ? "int64" : "int"
- var res = qmacro(invoke($($i(srcName) : typedecl($e(topExpr))) {
- return $c(castName)(length($i(srcName)))
- }, $e(topExpr)))
+ var res : Expression?
+ if (opName == "long_count") {
+ res = qmacro(invoke($($i(srcName) : typedecl($e(topExpr))) {
+ return int64(length($i(srcName)))
+ }, $e(topExpr)))
+ } else {
+ res = qmacro(invoke($($i(srcName) : typedecl($e(topExpr))) {
+ return length($i(srcName))
+ }, $e(topExpr)))
+ }
return finalize_invoke(res, at)
}
@@ -658,13 +695,14 @@ def private emit_array_lane(var top : Expression?; var expr : Expression?; var l
[macro_function]
def private emit_accumulator_lane(
opName : string;
- var top : Expression?;
+ var topExprs : array;
var projection : Expression?;
var whereCond : Expression?;
var intermediateBinds : array;
var preCondStmts : array;
var elementType : TypeDeclPtr;
- srcName, accName, itName, skipName, takeCountName, skippingName : string;
+ srcNames : array;
+ accName, itName, skipName, takeCountName, skippingName : string;
var skipExpr, takeExpr, skipWhileCond, takeWhileCond : Expression?;
at : LineInfo
) : Expression? {
@@ -730,8 +768,15 @@ def private emit_accumulator_lane(
$i(accName) += $e(valueExpr)
$i(cntName) ++
}
- returnExpr = qmacro_expr() {
- double($i(accName)) / double($i(cntName))
+ // Skip the double(accName) when accType is already double (interp doesn't fold redundant casts — would trip PERF020 and slow the inner loop tail).
+ if (accType != null && accType.baseType == Type.tDouble) {
+ returnExpr = qmacro_expr() {
+ $i(accName) / double($i(cntName))
+ }
+ } else {
+ returnExpr = qmacro_expr() {
+ double($i(accName)) / double($i(cntName))
+ }
}
} elif (opName == "min" || opName == "max") {
preludeStmts <- qmacro_block_to_array() {
@@ -766,21 +811,24 @@ def private emit_accumulator_lane(
for (s in preludeStmts) {
bodyStmts |> push(s)
}
- bodyStmts |> push <| qmacro_expr() {
- for ($i(itName) in $i(srcName)) {
- $e(loopBody)
+ // 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() {
+ for ($i(itName) in $i(srcNames[0])) {
+ $e(loopBody)
+ }
+ }
+ } else {
+ bodyStmts |> push <| qmacro_expr() {
+ for (itA, itB in $i(srcNames[0]), $i(srcNames[1])) {
+ $e(loopBody)
+ }
}
}
bodyStmts |> push <| qmacro_expr() {
return $e(returnExpr)
}
- var topExpr = clone_expression(top)
- topExpr.genFlags.alwaysSafe = true
- var srcParamType = invoke_src_param_type(top)
- var res = qmacro(invoke($($i(srcName) : $t(srcParamType)) {
- $b(bodyStmts)
- }, $e(topExpr)))
- return finalize_invoke(res, at)
+ return finalize_lane_emission(topExprs, srcNames, bodyStmts, at)
}
[macro_function]
@@ -797,14 +845,15 @@ def private emit_any_empty_shortcut(var top : Expression?; srcName : string; at
[macro_function]
def private emit_early_exit_lane(
opName : string;
- var top : Expression?;
+ var topExprs : array;
var projection : Expression?;
var whereCond : Expression?;
var intermediateBinds : array;
var preCondStmts : array;
var elementType : TypeDeclPtr;
terminatorCall : ExprCall?;
- srcName, itName, skipName, takeCountName, skippingName : string;
+ srcNames : array;
+ itName, skipName, takeCountName, skippingName : string;
var skipExpr, takeExpr, skipWhileCond, takeWhileCond : Expression?;
at : LineInfo
) : Expression? {
@@ -1122,21 +1171,24 @@ def private emit_early_exit_lane(
for (s in preludeStmts) {
bodyStmts |> push(s)
}
- bodyStmts |> push <| qmacro_expr() {
- for ($i(itName) in $i(srcName)) {
- $e(loopBody)
+ // 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() {
+ for ($i(itName) in $i(srcNames[0])) {
+ $e(loopBody)
+ }
+ }
+ } else {
+ bodyStmts |> push <| qmacro_expr() {
+ for (itA, itB in $i(srcNames[0]), $i(srcNames[1])) {
+ $e(loopBody)
+ }
}
}
for (s in tailStmts) {
bodyStmts |> push(s)
}
- var topExpr = clone_expression(top)
- topExpr.genFlags.alwaysSafe = true
- var srcParamType = invoke_src_param_type(top)
- var res = qmacro(invoke($($i(srcName) : $t(srcParamType)) {
- $b(bodyStmts)
- }, $e(topExpr)))
- return finalize_invoke(res, at)
+ return finalize_lane_emission(topExprs, srcNames, bodyStmts, at)
}
[macro_function]
@@ -1195,6 +1247,13 @@ def private order_top_n_call_name(orderName : string) : string {
return ""
}
+def private order_min_call_name(orderName : string; hasKey : bool) : string {
+ // `order + first` collapses to min/max — asc → min, desc → max. Keyed forms route to min_by/max_by.
+ let isDesc = orderName == "order_descending" || orderName == "order_by_descending"
+ if (hasKey) return isDesc ? "max_by" : "min_by"
+ return isDesc ? "max" : "min"
+}
+
[macro_function]
def private try_make_inline_cmp(orderKey : Expression?; orderName : string;
elemType : TypeDeclPtr; at : LineInfo) : Expression? {
@@ -1236,6 +1295,8 @@ def private plan_order_family(var expr : Expression?) : Expression? {
var orderKey : Expression?
var orderElemType : TypeDeclPtr
var takeExpr : Expression?
+ var firstName : string
+ var firstDefaultExpr : Expression?
var hasOrder = false
let at = calls[0]._0.at
let itName = "`it`{at.line}`{at.column}"
@@ -1253,17 +1314,28 @@ def private plan_order_family(var expr : Expression?) : Expression? {
} elif (name == "order" || name == "order_descending"
|| name == "order_by" || name == "order_by_descending") {
if (hasOrder) return null
+ // bail on `order(arr, cmp)` / `order_descending(arr, cmp)` — splice helpers (min/max/top_n) can't honor a user-supplied comparator and would silently drop it.
+ let argCount = cll._0.arguments |> length
+ if ((name == "order" || name == "order_descending") && argCount >= 2) return null
hasOrder = true
orderName = name
- if ((cll._0.arguments |> length) >= 2) {
+ if (argCount >= 2) {
orderKey = clone_expression(cll._0.arguments[1])
}
orderElemType = clone_type(cll._0._type.firstType)
} elif (name == "take") {
- if (!hasOrder || takeExpr != null) return null
+ if (!hasOrder || takeExpr != null || firstName != "") 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)
+ } elif (name == "first" || name == "first_or_default") {
+ // order + first → min/max (O(N) instead of sort + index). Must be terminal.
+ if (!hasOrder || takeExpr != null || firstName != "" || i != length(calls) - 1) return null
+ firstName = name
+ if (name == "first_or_default") {
+ if ((cll._0.arguments |> length) < 2) return null
+ firstDefaultExpr = clone_expression(cll._0.arguments[1])
+ }
} else {
return null
}
@@ -1278,12 +1350,51 @@ def private plan_order_family(var expr : Expression?) : Expression? {
if (hasKey) {
inlineCmp = try_make_inline_cmp(orderKey, orderName, orderElemType, at)
}
+ let minMaxName = order_min_call_name(orderName, hasKey)
if (whereCond == null) {
// No prefilter — direct call to daslib helper.
var topExpr = clone_expression(top)
topExpr.genFlags.alwaysSafe = true
var emission : Expression?
- if (takeExpr == null) {
+ if (firstName == "first") {
+ // order + first → preserve eager `first()` panic-on-empty. min/max return an uninitialized ref on empty, so wrap in an empty-guard for arrays (zero alloc, O(N) min scan), or use top_n*(_, 1, _) |> first() for iterators (n=1 bounded heap; first() panics on empty).
+ if (top._type.isGoodArrayType) {
+ var srcParamType = invoke_src_param_type(top)
+ let firstSrcName = "`first_src`{at.line}`{at.column}"
+ var minMaxCall : Expression?
+ if (hasKey) {
+ minMaxCall = qmacro($c(minMaxName)($i(firstSrcName), $e(orderKey)))
+ } else {
+ minMaxCall = qmacro($c(minMaxName)($i(firstSrcName)))
+ }
+ emission = qmacro(invoke($($i(firstSrcName) : $t(srcParamType)) {
+ panic("sequence contains no elements") if (empty($i(firstSrcName)))
+ return $e(minMaxCall)
+ }, $e(topExpr)))
+ emission = finalize_invoke(emission, at)
+ } else {
+ var topNCall : Expression?
+ if (inlineCmp != null) {
+ topNCall = qmacro(_::top_n_by_with_cmp($e(topExpr), 1, $e(inlineCmp)))
+ } elif (hasKey) {
+ topNCall = qmacro($c(topNName)($e(topExpr), 1, $e(orderKey)))
+ } else {
+ topNCall = qmacro($c(topNName)($e(topExpr), 1))
+ }
+ emission = qmacro(_::first($e(topNCall)))
+ }
+ } elif (firstName == "first_or_default") {
+ // No min_by_or_default exists; route through top_n*(_, 1, _) which returns an array (empty or single-element), then first_or_default supplies the default.
+ var topNCall : Expression?
+ if (inlineCmp != null) {
+ topNCall = qmacro(_::top_n_by_with_cmp($e(topExpr), 1, $e(inlineCmp)))
+ } elif (hasKey) {
+ topNCall = qmacro($c(topNName)($e(topExpr), 1, $e(orderKey)))
+ } else {
+ topNCall = qmacro($c(topNName)($e(topExpr), 1))
+ }
+ emission = qmacro(_::first_or_default($e(topNCall), $e(firstDefaultExpr)))
+ } elif (takeExpr == null) {
// Bare order family — emit the direct call. Same shape as plain LINQ, but via
if (inlineCmp != null) {
// Inlined comparator dispatches to the asc `order(src, block)` overload —
@@ -1304,7 +1415,7 @@ def private plan_order_family(var expr : Expression?) : Expression? {
}
}
// Wrap with to_sequence_move only when emission is array-shaped: take dispatches to
- let emissionIsArray = takeExpr != null || top._type.isGoodArrayType
+ let emissionIsArray = takeExpr != null || (firstName == "" && top._type.isGoodArrayType)
if (needIterWrap && emissionIsArray) {
emission = qmacro($e(emission).to_sequence_move())
}
@@ -1337,7 +1448,34 @@ def private plan_order_family(var expr : Expression?) : Expression? {
$e(loopBody)
}
}
- if (takeExpr == null) {
+ if (firstName == "first") {
+ // where + order + first → min/max on prefilter buffer. Empty buf must panic to match eager `first()` semantics; min/max return uninitialized refs on empty.
+ stmts |> push <| qmacro_expr() {
+ panic("sequence contains no elements") if (empty($i(bufName)))
+ }
+ var minMaxCall : Expression?
+ if (hasKey) {
+ minMaxCall = qmacro($c(minMaxName)($i(bufName), $e(orderKey)))
+ } else {
+ minMaxCall = qmacro($c(minMaxName)($i(bufName)))
+ }
+ stmts |> push <| qmacro_expr() {
+ return $e(minMaxCall)
+ }
+ } elif (firstName == "first_or_default") {
+ // No min_by_or_default helper exists; route through top_n*(_, 1, _) + first_or_default for the empty-buf case.
+ var topNCall : Expression?
+ if (inlineCmp != null) {
+ topNCall = qmacro(_::top_n_by_with_cmp($i(bufName), 1, $e(inlineCmp)))
+ } elif (hasKey) {
+ topNCall = qmacro($c(topNName)($i(bufName), 1, $e(orderKey)))
+ } else {
+ topNCall = qmacro($c(topNName)($i(bufName), 1))
+ }
+ stmts |> push <| qmacro_expr() {
+ return _::first_or_default($e(topNCall), $e(firstDefaultExpr))
+ }
+ } elif (takeExpr == null) {
// Sort the prefilter buffer in place and return it. order*_inplace is void
var sortCall : Expression?
if (inlineCmp != null) {
@@ -1511,10 +1649,15 @@ def private plan_loop_or_count(var expr : Expression?) : Expression? {
&& type_has_length(top._type))
return emit_length_shortcut(lastName, top, srcName, at)
// Ring 1: accumulator lane builds its own per-op loop body (typed accumulator, optional
- if (lane == LinqLane.ACCUMULATOR)
- return emit_accumulator_lane(lastName, top, projection, whereCond,
- intermediateBinds, preCondStmts, elementType, srcName, accName, itName, skipName, takeCountName,
+ if (lane == LinqLane.ACCUMULATOR) {
+ var laneTops : array
+ laneTops |> push(top)
+ var laneSrcs : array
+ laneSrcs |> push(srcName)
+ return emit_accumulator_lane(lastName, laneTops, projection, whereCond,
+ intermediateBinds, preCondStmts, elementType, laneSrcs, accName, itName, skipName, takeCountName,
skippingName, skipExpr, takeExpr, skipWhileCond, takeWhileCond, at)
+ }
// Ring 2: early-exit lane — `any` no-pred + no upstream work + no limits + length-bearing
if (lane == LinqLane.EARLY_EXIT) {
let terminatorCall = calls.back()._0
@@ -1522,8 +1665,12 @@ def private plan_loop_or_count(var expr : Expression?) : Expression? {
if (isAnyNoPred && whereCond == null && allProjectionsPure && noLimits
&& type_has_length(top._type))
return emit_any_empty_shortcut(top, srcName, at)
- return emit_early_exit_lane(lastName, top, projection, whereCond,
- intermediateBinds, preCondStmts, elementType, terminatorCall, srcName, itName, skipName,
+ var laneTops : array
+ laneTops |> push(top)
+ var laneSrcs : array
+ laneSrcs |> push(srcName)
+ return emit_early_exit_lane(lastName, laneTops, projection, whereCond,
+ intermediateBinds, preCondStmts, elementType, terminatorCall, laneSrcs, itName, skipName,
takeCountName, skippingName, skipExpr, takeExpr, skipWhileCond, takeWhileCond, at)
}
// Build the per-element loop body for COUNTER / ARRAY. Both lanes follow the same shape:
@@ -2882,6 +3029,7 @@ struct private DecsBridgeShape {
forExpr : ExpressionPtr // cloned ExprFor — inner multi-iter for-loop; body replaced when splicing
iterNames : array // bridge's iter var names (prefixed component names)
userNames : array // user-facing field names from the push tuple — feed the named-tuple bind that lets the user's `_.userName` chain access work
+ elementType : TypeDeclPtr // named-tuple type (from resVar._type.firstType) — used by to_array/first when no projection
}
[macro_function]
@@ -2973,11 +3121,122 @@ def private extract_decs_bridge(var top : Expression?) : DecsBridgeShape? {
archName := archName,
forExpr = clone_expression(forExpr),
iterNames <- iterNames,
- userNames <- userNames
+ userNames <- userNames,
+ elementType = clone_type(resVar._type.firstType)
+ )
+ return info
+}
+
+[macro_function]
+def private build_decs_tup_bind(bridge : DecsBridgeShape?; tupName : string; at : LineInfo) : Expression? {
+ // Named-tuple bind: `var tup = (n1=iter1, n2=iter2, ...)` — fold_linq_cond peels user lambdas substituting `_.userName` → `tup.userName`.
+ var mkTup = new ExprMakeTuple(at = at)
+ mkTup.recordNames |> resize(length(bridge.userNames))
+ for (i in 0 .. length(bridge.userNames)) {
+ mkTup.recordNames[i] := bridge.userNames[i]
+ mkTup.values |> emplace_new(new ExprVar(at = at, name := bridge.iterNames[i]))
+ }
+ return qmacro_expr() {
+ var $i(tupName) = $e(mkTup)
+ }
+}
+
+[macro_function]
+def private build_decs_inner_for(bridge : DecsBridgeShape?; var tupBind : Expression?; var body : Expression?; at : LineInfo) : Expression? {
+ // Clone bridge's inner multi-iter for-loop; substitute body with [tupBind, body]. Reuses bridge.archName so cloned get_ro sources stay valid.
+ var forBodyStmts <- [tupBind, body]
+ var forBody = stmts_to_expr(forBodyStmts)
+ var clonedForExpr = clone_expression(bridge.forExpr)
+ var clonedFor = clonedForExpr as ExprFor
+ var newForBody = new ExprBlock(at = at)
+ newForBody.list |> push(forBody)
+ clonedFor.body = newForBody
+ return clonedForExpr
+}
+
+struct private DecsChainInfo {
+ bindAt : array // bind name visible at each chain position
+ finalBind : string // bind name AFTER full chain — what terminator references
+ finalType : TypeDeclPtr // element type AFTER full chain (constant + ref stripped)
+ selectCount : int // number of `select` ops in chain; 0 means finalBind == tupName
+}
+
+[macro_function]
+def private compute_decs_chain_info(var calls : array>;
+ intermediateEnd : int;
+ tupName : string;
+ bridge : DecsBridgeShape?;
+ at : LineInfo) : DecsChainInfo? {
+ // Each select introduces a fresh `decs_sel{N}` bind; subsequent ops peel against it. Returns null on any non-where_/select op.
+ var info = new DecsChainInfo(
+ finalBind := tupName,
+ finalType = clone_type(bridge.elementType),
+ selectCount = 0
)
+ info.bindAt |> reserve(intermediateEnd)
+ var curBind = tupName
+ var curType : TypeDeclPtr = clone_type(bridge.elementType)
+ for (i in 0 .. intermediateEnd) {
+ info.bindAt |> push(curBind)
+ var cll & = unsafe(calls[i])
+ let opName = cll._1.name
+ if (opName == "select") {
+ info.selectCount ++
+ curBind = "`decs_sel`{at.line}`{at.column}`{info.selectCount}"
+ var peeled = fold_linq_cond(cll._0.arguments[1], info.bindAt[i])
+ if (peeled == null || peeled._type == null) return null
+ curType = clone_type(peeled._type)
+ } elif (opName != "where_") return null
+ }
+ info.finalBind := curBind
+ info.finalType = curType
+ if (info.finalType != null) {
+ info.finalType.flags.constant = false
+ info.finalType.flags.ref = false
+ }
return info
}
+[macro_function]
+def private wrap_decs_chain(var action : Expression?;
+ chainInfo : DecsChainInfo?;
+ var calls : array>;
+ intermediateEnd : int;
+ at : LineInfo) : Expression? {
+ // Reverse-walk chain, wrapping action with `if (pred) { ... }` for where_ and `let bindN+1 = proj; ...` for select.
+ var current = action
+ for (rev in 0 .. intermediateEnd) {
+ let i = intermediateEnd - 1 - rev
+ var cll & = unsafe(calls[i])
+ let opName = cll._1.name
+ let bindHere = chainInfo.bindAt[i]
+ if (opName == "where_") {
+ var pred = fold_linq_cond(cll._0.arguments[1], bindHere)
+ if (pred == null) return null
+ current = qmacro_expr() {
+ if ($e(pred)) {
+ $e(current)
+ }
+ }
+ } elif (opName == "select") {
+ var proj = fold_linq_cond(cll._0.arguments[1], bindHere)
+ if (proj == null) return null
+ let nextBind = (i + 1 < intermediateEnd) ? chainInfo.bindAt[i + 1] : chainInfo.finalBind
+ var letStmt = qmacro_expr() {
+ let $i(nextBind) = $e(proj)
+ }
+ var stmts : array
+ stmts |> reserve(2)
+ stmts |> push(letStmt)
+ stmts |> push(current)
+ current = stmts_to_expr(stmts)
+ } else {
+ return null
+ }
+ }
+ return current
+}
+
[macro_function]
def private emit_decs_count_archsize(bridge : DecsBridgeShape?; at : LineInfo) : Expression? {
// Bare count(): no chain ops, sum arch.size per archetype — skips the per-entity walk entirely.
@@ -3000,52 +3259,64 @@ def private emit_decs_count_archsize(bridge : DecsBridgeShape?; at : LineInfo) :
[macro_function]
def private emit_decs_accumulator(bridge : DecsBridgeShape?;
opName : string;
- var projection : Expression?;
- var whereCond : Expression?;
+ chainInfo : DecsChainInfo?;
+ var calls : array>;
+ intermediateEnd : int;
+ terminatorCall : ExprCall?;
var accType : TypeDeclPtr;
at : LineInfo) : Expression? {
- // Slice 2 accumulator emission: count / long_count / sum with optional _where + single _select chain ops.
+ // Slice 2/3a/4 accumulator emission: count / long_count / sum / min / max / average with chained _select + interleaved _where + optional _count(pred).
let accName = "`decs_acc`{at.line}`{at.column}"
let tupName = "`decs_tup`{at.line}`{at.column}"
- // Per-element body: acc += for sum, acc++ for count/long_count.
+ let cntName = "`decs_cnt`{at.line}`{at.column}"
+ let firstName = "`decs_first`{at.line}`{at.column}"
+ let valBindName = "`decs_val`{at.line}`{at.column}"
+ let finalBind = chainInfo.finalBind
var perElement : Expression?
if (opName == "sum") {
perElement = qmacro_expr() {
- $i(accName) += $e(projection)
+ $i(accName) += $i(finalBind)
}
- } elif (opName == "long_count") {
- perElement = qmacro_expr() {
- $i(accName) ++
+ } elif (opName == "average") {
+ perElement = qmacro_block() {
+ $i(accName) += $i(finalBind)
+ $i(cntName) ++
+ }
+ } elif (opName == "min" || opName == "max") {
+ let workhorse = (chainInfo.finalType != null && chainInfo.finalType.isWorkhorseType)
+ var compareExpr = min_max_compare(workhorse, opName, valBindName, accName)
+ perElement = qmacro_block() {
+ let $i(valBindName) = $i(finalBind)
+ if ($i(firstName)) {
+ $i(accName) := $i(valBindName)
+ $i(firstName) = false
+ } elif ($e(compareExpr)) {
+ $i(accName) := $i(valBindName)
+ }
}
} else {
+ // count, long_count
perElement = qmacro_expr() {
$i(accName) ++
}
}
- // Wrap with where filter.
- var body = wrap_with_condition(perElement, whereCond)
- // Named-tuple bind: fold_linq_cond(lambda, tupName) rebinds `_.userName` → `tup.userName`, so chain ops see a real named tuple.
- var mkTup = new ExprMakeTuple(at = at)
- mkTup.recordNames |> resize(length(bridge.userNames))
- for (i in 0 .. length(bridge.userNames)) {
- mkTup.recordNames[i] := bridge.userNames[i]
- mkTup.values |> emplace_new(new ExprVar(at = at, name := bridge.iterNames[i]))
- }
- var tupBind : Expression? = qmacro_expr() {
- var $i(tupName) = $e(mkTup)
+ // _count(pred): count/long_count with extra predicate — wrap perElement with `if (pred) {...}`. Predicate peels against finalBind so it sees the post-chain element.
+ if ((opName == "count" || opName == "long_count") && length(terminatorCall.arguments) > 1) {
+ var predExpr = fold_linq_cond(clone_expression(terminatorCall.arguments[1]), finalBind)
+ if (predExpr == null) return null
+ perElement = qmacro_expr() {
+ if ($e(predExpr)) {
+ $e(perElement)
+ }
+ }
}
- var forBodyStmts <- [tupBind, body]
- var forBody = stmts_to_expr(forBodyStmts)
- // Cloned for retains the bridge's iter vars + get_ro sources (which reference archName); reuse archName below.
- var clonedForExpr = clone_expression(bridge.forExpr)
- var clonedFor = clonedForExpr as ExprFor
- var newForBody = new ExprBlock(at = at)
- newForBody.list |> push(forBody)
- clonedFor.body = newForBody
+ var body = wrap_decs_chain(perElement, chainInfo, calls, intermediateEnd, at)
+ if (body == null) return null
+ var tupBind = build_decs_tup_bind(bridge, tupName, at)
+ var forExprNode = build_decs_inner_for(bridge, tupBind, body, at)
let archName = bridge.archName
var reqExpr = clone_expression(bridge.reqHashExpr)
var erqExpr = clone_expression(bridge.erqExpr)
- var forExprNode : Expression? = clonedForExpr
var emission : Expression?
if (opName == "long_count") {
emission = qmacro(invoke($() : int64 {
@@ -3071,9 +3342,254 @@ def private emit_decs_accumulator(bridge : DecsBridgeShape?;
})
return $i(accName)
}))
+ } elif (opName == "average") {
+ // Empty source → 0.0 / 0.0 → IEEE NaN (numerator + denominator both cast to double before division). Matches emit_accumulator_lane.
+ emission = qmacro(invoke($() : double {
+ var $i(accName) : $t(accType) = default<$t(accType)>
+ var $i(cntName) = 0
+ for_each_archetype($e(reqExpr), $e(erqExpr), $($i(archName) : Archetype) {
+ $e(forExprNode)
+ })
+ return double($i(accName)) / double($i(cntName))
+ }))
+ } elif (opName == "min" || opName == "max") {
+ // No empty-panic — matches non-decs emit_accumulator_lane (returns default-initialized acc).
+ emission = qmacro(invoke($() : $t(accType) {
+ var $i(firstName) = true
+ var $i(accName) : $t(accType)
+ for_each_archetype($e(reqExpr), $e(erqExpr), $($i(archName) : Archetype) {
+ $e(forExprNode)
+ })
+ return $i(accName)
+ }))
+ } else {
+ return null
+ }
+ emission.force_at(at)
+ emission.force_generated(true)
+ return emission
+}
+
+[macro_function]
+def private emit_decs_early_exit(bridge : DecsBridgeShape?;
+ opName : string;
+ chainInfo : DecsChainInfo?;
+ var calls : array>;
+ intermediateEnd : int;
+ terminatorCall : ExprCall?;
+ at : LineInfo) : Expression? {
+ // Slice 3b/4 early-exit: first / first_or_default / any / all / contains via for_each_archetype_find (block returns true to stop archetype walk). Supports chained _select + interleaved _where via wrap_decs_chain.
+ let tupName = "`decs_tup`{at.line}`{at.column}"
+ let foundName = "`decs_found`{at.line}`{at.column}"
+ let resultName = "`decs_result`{at.line}`{at.column}"
+ let containsValName = "`decs_cv`{at.line}`{at.column}"
+ let defaultName = "`decs_dv`{at.line}`{at.column}"
+ let finalBind = chainInfo.finalBind
+ var elemType = clone_type(chainInfo.finalType)
+ var perElement : Expression?
+ var preludeStmts : array
+ var tailStmts : array
+ if (opName == "first" || opName == "first_or_default") {
+ preludeStmts |> push <| qmacro_expr() {
+ var $i(foundName) = false
+ }
+ preludeStmts |> push <| qmacro_expr() {
+ var $i(resultName) : $t(elemType)
+ }
+ if (opName == "first_or_default") {
+ var defaultExpr = clone_expression(terminatorCall.arguments[1])
+ preludeStmts |> push <| qmacro_expr() {
+ let $i(defaultName) = $e(defaultExpr)
+ }
+ }
+ perElement = qmacro_block() {
+ $i(resultName) := $i(finalBind)
+ $i(foundName) = true
+ return true
+ }
+ if (opName == "first") {
+ tailStmts |> push <| qmacro_expr() {
+ if (!$i(foundName)) panic("sequence contains no elements")
+ }
+ tailStmts |> push <| qmacro_expr() {
+ return $i(resultName)
+ }
+ } else {
+ tailStmts |> push <| qmacro_expr() {
+ if (!$i(foundName)) return $i(defaultName)
+ }
+ tailStmts |> push <| qmacro_expr() {
+ return $i(resultName)
+ }
+ }
+ } elif (opName == "any") {
+ let argCount = length(terminatorCall.arguments)
+ if (argCount > 1) {
+ var predExpr = fold_linq_cond(clone_expression(terminatorCall.arguments[1]), finalBind)
+ if (predExpr == null) return null
+ perElement = qmacro_expr() {
+ if ($e(predExpr)) return true
+ }
+ } else {
+ perElement = qmacro_expr() {
+ return true
+ }
+ }
+ } elif (opName == "all") {
+ var predExpr = fold_linq_cond(clone_expression(terminatorCall.arguments[1]), finalBind)
+ if (predExpr == null) return null
+ perElement = qmacro_expr() {
+ if (!$e(predExpr)) return true
+ }
+ } elif (opName == "contains") {
+ var valExpr = clone_expression(terminatorCall.arguments[1])
+ preludeStmts |> push <| qmacro_expr() {
+ let $i(containsValName) = $e(valExpr)
+ }
+ perElement = qmacro_expr() {
+ if ($i(finalBind) == $i(containsValName)) return true
+ }
} else {
return null
}
+ var body = wrap_decs_chain(perElement, chainInfo, calls, intermediateEnd, at)
+ if (body == null) return null
+ var tupBind = build_decs_tup_bind(bridge, tupName, at)
+ var forExprNode = build_decs_inner_for(bridge, tupBind, body, at)
+ let archName = bridge.archName
+ var reqExpr = clone_expression(bridge.reqHashExpr)
+ var erqExpr = clone_expression(bridge.erqExpr)
+ // 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(tailStmts) + 1)
+ for (s in preludeStmts) {
+ bodyStmts |> push(s)
+ }
+ if (opName == "any" || opName == "contains") {
+ bodyStmts |> push <| qmacro_expr() {
+ return for_each_archetype_find($e(reqExpr), $e(erqExpr), $($i(archName) : Archetype) : bool {
+ $e(forExprNode)
+ return false
+ })
+ }
+ } elif (opName == "all") {
+ // `all` is "no counterexample found" — inner returns true on FAIL, so negate.
+ bodyStmts |> push <| qmacro_expr() {
+ return !for_each_archetype_find($e(reqExpr), $e(erqExpr), $($i(archName) : Archetype) : bool {
+ $e(forExprNode)
+ return false
+ })
+ }
+ } else {
+ bodyStmts |> push <| qmacro_expr() {
+ for_each_archetype_find($e(reqExpr), $e(erqExpr), $($i(archName) : Archetype) : bool {
+ $e(forExprNode)
+ return false
+ })
+ }
+ for (s in tailStmts) {
+ bodyStmts |> push(s)
+ }
+ }
+ var emission : Expression?
+ if (opName == "first" || opName == "first_or_default") {
+ emission = qmacro(invoke($() : $t(elemType) {
+ $b(bodyStmts)
+ }))
+ } else {
+ emission = qmacro(invoke($() : bool {
+ $b(bodyStmts)
+ }))
+ }
+ emission.force_at(at)
+ emission.force_generated(true)
+ return emission
+}
+
+[macro_function]
+def private emit_decs_to_array(bridge : DecsBridgeShape?;
+ chainInfo : DecsChainInfo?;
+ var calls : array>;
+ intermediateEnd : int;
+ at : LineInfo) : Expression? {
+ // Slice 3c/4 to_array: hoist `var buf` above outer for_each_archetype; per-element push_clone of post-chain value at chainInfo.finalBind.
+ let tupName = "`decs_tup`{at.line}`{at.column}"
+ let bufName = "`decs_buf`{at.line}`{at.column}"
+ let finalBind = chainInfo.finalBind
+ var elemType = clone_type(chainInfo.finalType)
+ var perElement : Expression? = qmacro_expr() {
+ $i(bufName) |> push_clone($i(finalBind))
+ }
+ var body = wrap_decs_chain(perElement, chainInfo, calls, intermediateEnd, at)
+ if (body == null) return null
+ var tupBind = build_decs_tup_bind(bridge, tupName, at)
+ var forExprNode = build_decs_inner_for(bridge, tupBind, body, at)
+ let archName = bridge.archName
+ var reqExpr = clone_expression(bridge.reqHashExpr)
+ var erqExpr = clone_expression(bridge.erqExpr)
+ var emission : Expression? = qmacro(invoke($() : array<$t(elemType)> {
+ var $i(bufName) : array<$t(elemType)>
+ for_each_archetype($e(reqExpr), $e(erqExpr), $($i(archName) : Archetype) {
+ $e(forExprNode)
+ })
+ return <- $i(bufName)
+ }))
+ emission.force_at(at)
+ emission.force_generated(true)
+ return emission
+}
+
+[macro_function]
+def private emit_decs_min_max_by(bridge : DecsBridgeShape?;
+ opName : string;
+ chainInfo : DecsChainInfo?;
+ var calls : array>;
+ intermediateEnd : int;
+ terminatorCall : ExprCall?;
+ at : LineInfo) : Expression? {
+ // Slice 4: min_by / max_by — track best (key, element) pair across archetypes. Element retained via `:=`, matches min_by_impl's empty→default semantics.
+ let tupName = "`decs_tup`{at.line}`{at.column}"
+ let firstName = "`decs_first`{at.line}`{at.column}"
+ let bestKeyName = "`decs_bkey`{at.line}`{at.column}"
+ let bestElemName = "`decs_belem`{at.line}`{at.column}"
+ let keyBindName = "`decs_key`{at.line}`{at.column}"
+ let finalBind = chainInfo.finalBind
+ var elemType = clone_type(chainInfo.finalType)
+ var keyExpr = fold_linq_cond(clone_expression(terminatorCall.arguments[1]), finalBind)
+ if (keyExpr == null || keyExpr._type == null) return null
+ var keyType = clone_type(keyExpr._type)
+ keyType.flags.constant = false
+ keyType.flags.ref = false
+ let workhorse = keyType.isWorkhorseType
+ let opCmp = (opName == "min_by") ? "min" : "max"
+ var compareExpr = min_max_compare(workhorse, opCmp, keyBindName, bestKeyName)
+ var perElement = qmacro_block() {
+ let $i(keyBindName) = $e(keyExpr)
+ if ($i(firstName)) {
+ $i(bestKeyName) := $i(keyBindName)
+ $i(bestElemName) := $i(finalBind)
+ $i(firstName) = false
+ } elif ($e(compareExpr)) {
+ $i(bestKeyName) := $i(keyBindName)
+ $i(bestElemName) := $i(finalBind)
+ }
+ }
+ var body = wrap_decs_chain(perElement, chainInfo, calls, intermediateEnd, at)
+ if (body == null) return null
+ var tupBind = build_decs_tup_bind(bridge, tupName, at)
+ var forExprNode = build_decs_inner_for(bridge, tupBind, body, at)
+ let archName = bridge.archName
+ var reqExpr = clone_expression(bridge.reqHashExpr)
+ var erqExpr = clone_expression(bridge.erqExpr)
+ var emission = qmacro(invoke($() : $t(elemType) {
+ var $i(firstName) = true
+ var $i(bestKeyName) : $t(keyType)
+ var $i(bestElemName) : $t(elemType)
+ for_each_archetype($e(reqExpr), $e(erqExpr), $($i(archName) : Archetype) {
+ $e(forExprNode)
+ })
+ return $i(bestElemName)
+ }))
emission.force_at(at)
emission.force_generated(true)
return emission
@@ -3083,70 +3599,81 @@ def private emit_decs_accumulator(bridge : DecsBridgeShape?;
def private plan_decs_unroll(var expr : Expression?) : Expression? {
var (top, calls) = flatten_linq(expr)
let bridge = extract_decs_bridge(top)
- if (bridge == null || empty(calls)) return null
- let lastName = calls.back()._1.name
+ if (bridge == null) return null
let at = expr.at
// Slice 1: bare count → arch.size shortcut.
- if (lastName == "count" && length(calls) == 1) return emit_decs_count_archsize(bridge, at)
- // Slice 2: chain-aware count/long_count/sum with _where + single _select.
- if (lastName != "count" && lastName != "long_count" && lastName != "sum") return null
+ if (length(calls) == 1 && calls.back()._1.name == "count" && length(calls.back()._0.arguments) == 1) return emit_decs_count_archsize(bridge, at)
+ // to_array is `skip = true` in linqCalls so it's already peeled — "no recognized terminator" means implicit to_array.
+ let lastName = empty(calls) ? "" : calls.back()._1.name
+ let isAccum = (lastName == "count" || lastName == "long_count" || lastName == "sum"
+ || lastName == "min" || lastName == "max" || lastName == "average")
+ let isEarlyExit = (lastName == "first" || lastName == "first_or_default"
+ || lastName == "any" || lastName == "all" || lastName == "contains")
+ let isMinMaxBy = (lastName == "min_by" || lastName == "max_by")
+ let isTerminator = isAccum || isEarlyExit || isMinMaxBy
let tupName = "`decs_tup`{at.line}`{at.column}"
- let intermediateEnd = length(calls) - 1
- var whereCond : Expression?
- var projection : Expression?
- var seenSelect = false
- for (i in 0 .. intermediateEnd) {
- var cll & = unsafe(calls[i])
- let opName = cll._1.name
- if (opName == "where_") {
- // After-select where: defer to follow-up; canonical order is where-then-select.
- if (seenSelect) return null
- var pred = fold_linq_cond(cll._0.arguments[1], tupName)
- if (pred == null) return null
- if (whereCond == null) {
- whereCond = pred
- } else {
- whereCond = qmacro($e(whereCond) && $e(pred))
- }
- } elif (opName == "select") {
- // Chained selects: defer to follow-up.
- if (seenSelect) return null
- projection = fold_linq_cond(cll._0.arguments[1], tupName)
- if (projection == null) return null
- seenSelect = true
- } else {
- return null
+ let intermediateEnd = isTerminator ? length(calls) - 1 : length(calls)
+ let chainInfo = compute_decs_chain_info(calls, intermediateEnd, tupName, bridge, at)
+ if (chainInfo == null) return null
+ if (isAccum) {
+ // sum/min/max/average need a scalar element — selectCount==0 keeps finalType = bridge.elementType (a tuple) which can't be summed/compared.
+ var accType : TypeDeclPtr
+ if (lastName == "sum" || lastName == "min" || lastName == "max" || lastName == "average") {
+ if (chainInfo.selectCount == 0 || chainInfo.finalType == null) return null
+ accType = clone_type(chainInfo.finalType)
}
+ return emit_decs_accumulator(bridge, lastName, chainInfo, calls, intermediateEnd, calls.back()._0, accType, at)
}
- // sum requires a scalar projection (tuple sources can't sum directly).
- var accType : TypeDeclPtr
- if (lastName == "sum") {
- if (projection == null || projection._type == null) return null
- accType = clone_type(projection._type)
- accType.flags.constant = false
- accType.flags.ref = false
- }
- return emit_decs_accumulator(bridge, lastName, projection, whereCond, accType, at)
+ if (isEarlyExit) return emit_decs_early_exit(bridge, lastName, chainInfo, calls, intermediateEnd, calls.back()._0, at)
+ if (isMinMaxBy) return emit_decs_min_max_by(bridge, lastName, chainInfo, calls, intermediateEnd, calls.back()._0, at)
+ // Iterator-typed chains cascade to tier-2; only fire to_array when expr is array-typed (user wrote `.to_array()`, peeled by skip=true).
+ if (expr._type == null || !expr._type.isGoodArrayType) return null
+ return emit_decs_to_array(bridge, chainInfo, calls, intermediateEnd, at)
}
[macro_function]
def private plan_zip(var expr : Expression?) : Expression? {
- // Phase 2 Z1/Z2/Z3: 2-ary lockstep zip splice. Supports bare zip (array/iterator), no-pred count/long_count, and fused where_/select/take/skip/take_while/skip_while chain ops between zip and terminator. Result-selector form (3-arg zip), accumulator terminators (sum/min/max/etc.), and chained selects bail to tier-2 cascade.
+ // 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.
var (top, calls) = flatten_linq(expr)
if (empty(calls) || calls[0]._1.name != "zip") return null
var zipCall = calls[0]._0
let zipArgCount = zipCall.arguments |> length
// Z6 bail: result-selector form (3-arg zip = 2 sources + selector) yields scalar element stream — different splice shape, defer.
if (zipArgCount != 2) return null
- // Identify recognized terminator (count/long_count). Anything else: if it's a recognized chain op, treat as no-terminator (bare); else bail.
+ // Identify recognized terminator. Counter: count/long_count. Accumulator: sum/min/max/average. Early-exit: first/first_or_default/any/all/contains. Anything else: treat as no-terminator (bare → ARRAY lane); unrecognized chain op bails inside the chain walk.
var lastName = ""
var intermediateEnd = length(calls)
if (length(calls) > 1) {
let candidateName = calls.back()._1.name
let candidateCall = calls.back()._0
+ let candidateArgs = candidateCall.arguments |> length
if (candidateName == "count" || candidateName == "long_count") {
// No-pred form only (1 arg = the source). Predicate-form bails (would change per-element work).
- if (candidateCall.arguments |> length != 1) return null
+ if (candidateArgs != 1) return null
+ lastName = candidateName
+ intermediateEnd = length(calls) - 1
+ } elif (candidateName == "sum" || candidateName == "min" || candidateName == "max" || candidateName == "average") {
+ // Accumulator: no-arg form only (1 arg = the source).
+ if (candidateArgs != 1) return null
+ lastName = candidateName
+ intermediateEnd = length(calls) - 1
+ } elif (candidateName == "first") {
+ if (candidateArgs != 1) return null
+ lastName = candidateName
+ intermediateEnd = length(calls) - 1
+ } elif (candidateName == "first_or_default" || candidateName == "contains") {
+ // first_or_default(d): 1 default arg. contains(v): 1 value arg.
+ if (candidateArgs != 2) return null
+ lastName = candidateName
+ intermediateEnd = length(calls) - 1
+ } elif (candidateName == "any") {
+ // any(): no pred. any(p): 1 predicate arg. Both forms recognized.
+ if (candidateArgs != 1 && candidateArgs != 2) return null
+ lastName = candidateName
+ intermediateEnd = length(calls) - 1
+ } elif (candidateName == "all") {
+ // all(p): must have predicate.
+ if (candidateArgs != 2) return null
lastName = candidateName
intermediateEnd = length(calls) - 1
}
@@ -3259,12 +3786,42 @@ def private plan_zip(var expr : Expression?) : Expression? {
let isCounter = lastName == "count" || lastName == "long_count"
// Semantic guard: impure `select` projection AHEAD of a range op (skip/take/skip_while/take_while). The eager `select(proj)|>skip(K)` pipeline runs proj on every element; our spliced order (skip-then-push) would drop the side effects on skipped items. Bail to tier-2 which preserves eager semantics. Plan_loop_or_count has the analogous gap (see PR #2741 review thread #r3269663361); fixing both planners uniformly is a separate follow-up.
if (seenSelect && !allProjectionsPure && !noLimits) return null
+ // Accumulator + early-exit lane dispatch: reuses the generalized emit_*_lane helpers (parallel-array form). preCondStmts threads `let it = (itA, itB)` so itName resolves inside the loop body when the where/projection/predicate/value references the tuple element. `long_count` is classified ACCUMULATOR but routes through the COUNTER path locally (existing length-shortcut + counter loop already cover it); `!isCounter` excludes it from this branch.
+ let lane = classify_terminator(lastName)
+ if (lane == LinqLane.ACCUMULATOR && !isCounter) {
+ // sum/min/max/average without projection: elementType is tuple<...>, accumulator op would not typecheck — bail to tier-2 (typer rejects anyway, but explicit bail keeps the error inside linq's normal cascade rather than the splice).
+ if (projection == null) return null
+ var preCondStmts : array
+ preCondStmts |> push <| qmacro_expr() { // nolint:STYLE012
+ let $i(itName) = (itA, itB)
+ }
+ var intermediateBinds : array
+ var laneTops <- [srcAExpr, srcBExpr]
+ let laneSrcs <- [srcAName, srcBName]
+ return emit_accumulator_lane(lastName, laneTops, projection, whereCond,
+ intermediateBinds, preCondStmts, elementType, laneSrcs, accName, itName, skipName, takeCountName,
+ skippingName, skipExpr, takeExpr, skipWhileCond, takeWhileCond, at)
+ }
+ if (lane == LinqLane.EARLY_EXIT) {
+ var preCondStmts : array
+ preCondStmts |> push <| qmacro_expr() { // nolint:STYLE012
+ let $i(itName) = (itA, itB)
+ }
+ var intermediateBinds : array
+ let terminatorCall = calls.back()._0
+ var laneTops <- [srcAExpr, srcBExpr]
+ let laneSrcs <- [srcAName, srcBName]
+ return emit_early_exit_lane(lastName, laneTops, projection, whereCond,
+ intermediateBinds, preCondStmts, elementType, terminatorCall, laneSrcs, itName, skipName,
+ takeCountName, skippingName, skipExpr, takeExpr, skipWhileCond, takeWhileCond, at)
+ }
// Length shortcut: count/long_count, no chain, both length-bearing → return min(lenA, lenB) without entering the loop.
if (noChain && bothHaveLength && isCounter) {
var bodyStmts : array
if (lastName == "count") {
+ // length returns int already; bare `length(...)` matches return type — no cast (PERF020).
bodyStmts |> push <| qmacro_expr() {
- return int(length($i(srcAName)) < length($i(srcBName)) ? length($i(srcAName)) : length($i(srcBName)))
+ return length($i(srcAName)) < length($i(srcBName)) ? length($i(srcAName)) : length($i(srcBName))
}
} else {
bodyStmts |> push <| qmacro_expr() {
diff --git a/daslib/lint.das b/daslib/lint.das
index 34c5af12db..17e5fd8458 100644
--- a/daslib/lint.das
+++ b/daslib/lint.das
@@ -57,11 +57,20 @@ class LintVisitor : AstVisitor {
// collection mode — when true, errors are appended to `errors` instead of error()
collect_errors : bool = false
errors : array
+ // Filters: disabled (denylist) and enabled (whitelist; empty == all).
+ // Caller-populated via collect overload; applied alongside // nolint suppression.
+ disabled_codes : table
+ enabled_codes : table
def LintVisitor() {
pass
}
def is_suppressed(text : string; at : LineInfo) : bool {
- return is_lint_suppressed(at, extract_lint_code(text))
+ let code = extract_lint_code(text)
+ if (!empty(code)) {
+ return true if (key_exists(disabled_codes, code)
+ || (!empty(enabled_codes) && !key_exists(enabled_codes, code)))
+ }
+ return is_lint_suppressed(at, code)
}
def lint_error(text : string; at : LineInfo) : void {
if (noLint || self->is_suppressed(text, at)) return
@@ -273,7 +282,17 @@ def public paranoid(prog : ProgramPtr; compile_time_errors : bool) {
def public paranoid_collect(prog : ProgramPtr; var errors : array) : int {
//! Runs the paranoid lint visitor and collects errors as strings.
//! Returns the number of lint issues found.
+ let empty_set : table
+ return paranoid_collect(prog, errors, empty_set, empty_set)
+}
+
+def public paranoid_collect(prog : ProgramPtr; var errors : array;
+ disabled_codes, enabled_codes : table) : int {
+ //! Filter-aware overload. `disabled_codes` is a denylist; `enabled_codes`
+ //! is a whitelist (empty == all). Codes use the bare form (e.g. "LINT002").
var astVisitor = new LintVisitor(compile_time_errors = false, collect_errors = true)
+ astVisitor.disabled_codes := disabled_codes
+ astVisitor.enabled_codes := enabled_codes
make_visitor(*astVisitor) $(adapter) {
astVisitor.astVisitorAdapter = adapter
visit(prog, astVisitor.astVisitorAdapter)
diff --git a/daslib/perf_lint.das b/daslib/perf_lint.das
index f4207822e7..d715259bd6 100644
--- a/daslib/perf_lint.das
+++ b/daslib/perf_lint.das
@@ -33,6 +33,7 @@ module perf_lint shared private
//! PERF018 — for (i in range(length(arr))) where i only indexes arr — use 'for (c in arr)'
//! 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)
require daslib/ast_boost
require strings
@@ -125,8 +126,18 @@ class PerfLintVisitor : AstVisitor {
return uint64(at.line) | (uint64(at.column) << uint64(20))
}
+ // Filters: disabled (denylist) and enabled (whitelist; empty == all).
+ // Caller-populated via collect overload; applied alongside // nolint suppression.
+ disabled_codes : table
+ enabled_codes : table
+
def is_suppressed(text : string; at : LineInfo) : bool {
- return is_lint_suppressed(at, extract_lint_code(text))
+ let code = extract_lint_code(text)
+ if (!empty(code)) {
+ return true if (key_exists(disabled_codes, code)
+ || (!empty(enabled_codes) && !key_exists(enabled_codes, code)))
+ }
+ return is_lint_suppressed(at, code)
}
def perf_warning(text : string; at : LineInfo) : void {
@@ -381,7 +392,7 @@ class PerfLintVisitor : AstVisitor {
if (inner == null || !(inner is ExprCall)) return null
let call = inner as ExprCall
if (call.func == null || empty(call.arguments)) return null
- let fname = call.func.fromGeneric != null ? string(call.func.fromGeneric.name) : string(call.func.name)
+ let fname = string(call.func.fromGeneric != null ? call.func.fromGeneric.name : call.func.name)
if (fname != "int") return null
return call.arguments[0]
}
@@ -948,7 +959,7 @@ class PerfLintVisitor : AstVisitor {
def check_perf020_redundant_cast(call : ExprCall?) : void {
if (call == null || call.func == null || length(call.arguments) != 1) return
- let fname = call.func.fromGeneric != null ? string(call.func.fromGeneric.name) : string(call.func.name)
+ let fname = string(call.func.fromGeneric != null ? call.func.fromGeneric.name : call.func.name)
let target = self->perf020_target_basetype(fname)
if (target == Type.none) return
var arg = call.arguments[0]
@@ -959,6 +970,100 @@ class PerfLintVisitor : AstVisitor {
self->perf_warning("PERF020: redundant {fname}(...) cast — argument is already {fname}", call.at)
}
+ // --- PERF021: cond ? T(a) : T(b) — hoist common workhorse cast out of ternary ---
+
+ def cast_call_workhorse_target(e : Expression?; var fname_out : string&) : Type {
+ //! If `e` is a workhorse cast call (after peeling ExprRef2Value), return
+ //! its target Type and write the cast name to `fname_out`. Otherwise
+ //! return Type.none. Accepts any argument count — `string(int)` is bound
+ //! with `value,hex,context,at` (4 daslang args), other casts use 1.
+ var ee = e
+ if (ee != null && ee is ExprRef2Value) {
+ ee = (ee as ExprRef2Value.subexpr)
+ }
+ if (ee == null || !(ee is ExprCall)) return Type.none
+ let c = ee as ExprCall
+ if (c.func == null || empty(c.arguments)) return Type.none
+ let n = string(c.func.fromGeneric != null ? c.func.fromGeneric.name : c.func.name)
+ let t = self->perf020_target_basetype(n)
+ if (t == Type.none) return Type.none
+ fname_out = n
+ return t
+ }
+
+ def cast_call_arg_basetype(e : Expression?) : Type {
+ //! Peel ExprRef2Value, take call.arguments[0], peel ExprRef2Value, return its baseType
+ //! (or Type.none if anything along the chain is null).
+ var ee = e
+ if (ee != null && ee is ExprRef2Value) {
+ ee = (ee as ExprRef2Value.subexpr)
+ }
+ if (ee == null || !(ee is ExprCall)) return Type.none
+ let c = ee as ExprCall
+ if (empty(c.arguments)) return Type.none
+ var arg = c.arguments[0]
+ if (arg is ExprRef2Value) {
+ arg = (arg as ExprRef2Value.subexpr)
+ }
+ if (arg == null || arg._type == null) return Type.none
+ return arg._type.baseType
+ }
+
+ def cast_call_tail_args_equal(le, re : Expression?) : bool {
+ //! Compare arguments[1..] structurally between two cast calls. Skip
+ //! ExprFakeContext / ExprFakeLineInfo (auto-injected by the typer for
+ //! Context*/LineInfoArg* parameters; differ at every call site by
+ //! design). The remaining tail args — e.g. `string(int)`'s `hex` flag —
+ //! must match in count and via expr_equal_struct, otherwise the hoist
+ //! would silently lose semantically-significant arguments.
+ var lee = le
+ var ree = re
+ if (lee != null && lee is ExprRef2Value) {
+ lee = (lee as ExprRef2Value.subexpr)
+ }
+ if (ree != null && ree is ExprRef2Value) {
+ ree = (ree as ExprRef2Value.subexpr)
+ }
+ if (lee == null || ree == null || !(lee is ExprCall) || !(ree is ExprCall)) return false
+ let lc = lee as ExprCall
+ let rc = ree as ExprCall
+ var li = 1
+ var ri = 1
+ let ln = length(lc.arguments)
+ let rn = length(rc.arguments)
+ while (true) {
+ while (li < ln && (lc.arguments[li] is ExprFakeContext || lc.arguments[li] is ExprFakeLineInfo)) {
+ li++
+ }
+ while (ri < rn && (rc.arguments[ri] is ExprFakeContext || rc.arguments[ri] is ExprFakeLineInfo)) {
+ ri++
+ }
+ if (li >= ln || ri >= rn) return (li >= ln && ri >= rn)
+ if (!self->expr_equal_struct(lc.arguments[li], rc.arguments[ri], false)) return false
+ li++
+ ri++
+ }
+ return true
+ }
+
+ def check_perf021_ternary_cast_hoist(expr : ExprOp3?) : void {
+ if (expr == null || expr.op != "?"
+ || expr.left == null || expr.right == null) return
+ var lname = ""
+ var rname = ""
+ let lt = self->cast_call_workhorse_target(expr.left, lname)
+ let rt = self->cast_call_workhorse_target(expr.right, rname)
+ let la = self->cast_call_arg_basetype(expr.left)
+ let ra = self->cast_call_arg_basetype(expr.right)
+ // Tail-args must match so the hoisted form preserves semantically-significant
+ // arguments (e.g. `string(int, hex)`'s `hex` flag). Auto-injected
+ // ExprFakeContext / ExprFakeLineInfo are skipped — they differ at every site.
+ if (lt == Type.none || rt == Type.none || lt != rt || lname != rname
+ || la == Type.none || la != ra
+ || !self->cast_call_tail_args_equal(expr.left, expr.right)) return
+ self->perf_warning("PERF021: redundant per-branch {lname}(...) cast in ternary — hoist as {lname}(cond ? a : b)", expr.at)
+ }
+
// --- PERF014: closed-interval char-class range checks ---
def parse_range_leg(leg : Expression?; var v_out : Expression?&; var bound_out : int&; var is_hi_out : bool&) : bool {
@@ -1119,6 +1224,9 @@ class PerfLintVisitor : AstVisitor {
// --- PERF015 / PERF016: ternary min/max/abs ---
def override preVisitExprOp3(expr : ExprOp3?) : void {
+ // PERF021: fires anywhere, including in closures — a redundant per-branch cast
+ // is redundant regardless of where the ternary lives.
+ self->check_perf021_ternary_cast_hoist(expr)
if (in_closure > 0 || expr.op != "?"
|| expr.subexpr == null || !(expr.subexpr is ExprOp2)) return
var cmp = expr.subexpr as ExprOp2
@@ -1402,7 +1510,17 @@ def public perf_lint(prog : ProgramPtr; compile_time_errors : bool) : int {
def public perf_lint_collect(prog : ProgramPtr; var warnings : array) : int {
//! Runs the performance lint visitor and collects warnings as strings.
//! Returns the number of warnings found.
+ let empty_set : table
+ return perf_lint_collect(prog, warnings, empty_set, empty_set)
+}
+
+def public perf_lint_collect(prog : ProgramPtr; var warnings : array;
+ disabled_codes, enabled_codes : table) : int {
+ //! Filter-aware overload. `disabled_codes` is a denylist; `enabled_codes`
+ //! is a whitelist (empty == all). Codes use the bare form (e.g. "PERF001").
var astVisitor = new PerfLintVisitor(compile_time_errors = false, collect_warnings = true)
+ astVisitor.disabled_codes := disabled_codes
+ astVisitor.enabled_codes := enabled_codes
make_visitor(*astVisitor) $(astVisitorAdapter) {
visit(prog, astVisitorAdapter)
}
diff --git a/daslib/style_lint.das b/daslib/style_lint.das
index dbf8df4fd7..c53a446859 100644
--- a/daslib/style_lint.das
+++ b/daslib/style_lint.das
@@ -36,12 +36,24 @@ module style_lint shared private
//! STYLE026 — nested 'unsafe { ... }' block; outer wrap already covers the scope — drop the inner
require daslib/ast_boost
+require daslib/is_local
require strings
// ---------------------------------------------------------------------------
// Visitor
// ---------------------------------------------------------------------------
+struct UnsafeFrame {
+ //! Per-expression subtree summary tracked on `StyleLintVisitor.unsafe_stack`.
+ //! `count` is the number of inherently-unsafe leaves in the subtree.
+ //! `has_non_local_let_ref` flags an enclosing `let v & = E` where `E` is
+ //! non-local-non-temporary (per `isLocalOrGlobal` in ast_infer_type.cpp:4989)
+ //! — narrowing the enclosing `unsafe { ... }` to expression-form would
+ //! leave the let-ref binding unsatisfied, so STYLE025 must stay silent.
+ count : int
+ has_non_local_let_ref : bool
+}
+
class StyleLintVisitor : AstVisitor {
compile_time_errors : bool
comment_hygiene : bool = false
@@ -58,8 +70,8 @@ class StyleLintVisitor : AstVisitor {
@do_not_delete pending_uninit_vars : array
// STYLE024/025 — tight unsafe checks.
@do_not_delete skip_userSaidItsSafe : array
- @do_not_delete unsafeExprs : table
- unsafe_stack : array
+ @do_not_delete unsafeExprs : table
+ unsafe_stack : array
unsafe_block_stack : array
def StyleLintVisitor() {
@@ -70,8 +82,18 @@ class StyleLintVisitor : AstVisitor {
return uint64(at.line) | (uint64(at.column) << uint64(20))
}
+ // Filters: disabled (denylist) and enabled (whitelist; empty == all).
+ // Caller-populated via collect overload; applied alongside // nolint suppression.
+ disabled_codes : table
+ enabled_codes : table
+
def is_suppressed(text : string; at : LineInfo) : bool {
- return is_lint_suppressed(at, extract_lint_code(text))
+ let code = extract_lint_code(text)
+ if (!empty(code)) {
+ return true if (key_exists(disabled_codes, code)
+ || (!empty(enabled_codes) && !key_exists(enabled_codes, code)))
+ }
+ return is_lint_suppressed(at, code)
}
def style_warning(text : string; at : LineInfo) : void {
@@ -1238,40 +1260,52 @@ class StyleLintVisitor : AstVisitor {
// --- STYLE024/STYLE025: redundant or over-broad `unsafe` ---
- def unsafe_count_for(expr : Expression?) : int {
- return 0 if (expr == null)
- return unsafeExprs |> key_exists(expr) ? unsafeExprs[expr] : 0
+ def unsafe_frame_for(expr : Expression?) : UnsafeFrame {
+ return UnsafeFrame() if (expr == null)
+ return unsafeExprs |> key_exists(expr) ? unsafeExprs[expr] : UnsafeFrame()
}
def mark_unsafe_in_stack() : void {
//! Add 1 to the current top of `unsafe_stack`.
let n = length(unsafe_stack)
if (n > 0) {
- unsafe_stack[n - 1] = unsafe_stack[n - 1] + 1
+ unsafe_stack[n - 1].count++
+ }
+ }
+
+ def mark_non_local_let_ref_in_stack() : void {
+ //! Flag the current top of `unsafe_stack` as containing a non-local
+ //! let-ref binding. Propagates upward through `visitExpression`.
+ let n = length(unsafe_stack)
+ if (n > 0) {
+ unsafe_stack[n - 1].has_non_local_let_ref = true
}
}
def override preVisitExpression(expr : ExpressionPtr) : void {
- //! Push a fresh 0 slot for this node.
- unsafe_stack |> push(0)
+ //! Push a fresh frame for this node.
+ unsafe_stack |> push(UnsafeFrame())
}
def override visitExpression(var expr : ExpressionPtr) : ExpressionPtr {
- //! Pop the slot for this node. The popped value is the subtree's
- //! count of inherently-unsafe leaves. Use it to:
- //! - store `unsafeExprs[expr.at] = count` for later queries
+ //! Pop the slot for this node. The popped frame is the subtree's
+ //! summary. Use it to:
+ //! - store `unsafeExprs[expr] = frame` for later queries
//! (used by `visitExprUnsafe` to decide STYLE024 vs STYLE025),
- //! - propagate count to the parent's slot (now top after our pop),
+ //! - propagate count + non-local-let-ref flag to the parent slot,
//! - fire STYLE024 if `expr` is an `unsafe(...)` wrap target
//! (`userSaidItsSafe`) and its subtree had count 0.
let n = length(unsafe_stack)
- let count = unsafe_stack[n - 1]
+ let frame = unsafe_stack[n - 1]
unsafe_stack |> pop
- if (count > 0) {
- unsafeExprs |> insert(expr, count)
+ if (frame.count > 0 || frame.has_non_local_let_ref) {
+ unsafeExprs |> insert(expr, frame)
let m = length(unsafe_stack)
if (m > 0) {
- unsafe_stack[m - 1] = unsafe_stack[m - 1] + count
+ unsafe_stack[m - 1].count = unsafe_stack[m - 1].count + frame.count
+ if (frame.has_non_local_let_ref) {
+ unsafe_stack[m - 1].has_non_local_let_ref = true
+ }
}
}
// STYLE024 expression form: parser sets `userSaidItsSafe` on the
@@ -1280,12 +1314,33 @@ class StyleLintVisitor : AstVisitor {
if (expr.genFlags.userSaidItsSafe && !expr.genFlags.generated
&& (current_function == null || current_function.fromGeneric == null)
&& !(skip_userSaidItsSafe |> has_value(expr))
- && count == 0) {
+ && frame.count == 0) {
self->style_warning("STYLE024: redundant 'unsafe(...)' wrap; the inner expression has no operation that requires unsafe — drop the wrap", expr.at)
}
return expr
}
+ def override preVisitExprLet(expr : ExprLet?) : void {
+ //! Mirror `isLocalOrGlobal` check from ast_infer_type.cpp:4989 —
+ //! `let v & = E` requires unsafe at statement level when `E` is
+ //! non-local-non-temporary. Mark the let's frame so the enclosing
+ //! `unsafe { ... }` block can detect it (STYLE025 must stay silent
+ //! when narrowing would leave the let-ref binding unsatisfied).
+ if (expr.genFlags.generated) return
+ for (v in expr.variables) {
+ continue if (
+ v._type == null ||
+ !v._type.flags.ref ||
+ v.init == null ||
+ v.init.genFlags.alwaysSafe ||
+ (v.init._type != null && v.init._type.flags.temporary) ||
+ is_local_or_global_expr(v.init)
+ )
+ self->mark_non_local_let_ref_in_stack()
+ return
+ }
+ }
+
def call_func_needs_unsafe(func : Function const?; is_for_loop_src : bool) : bool {
return (func == null
|| func.flags.unsafeOperation
@@ -1331,6 +1386,29 @@ class StyleLintVisitor : AstVisitor {
}
}
+ def override preVisitExprSafeAt(expr : ExprSafeAt?) : void {
+ // `?[]` on table<> / array<> / pointer-to-(table|array|pointer)
+ // requires unsafe — see ast_infer_type.cpp errors
+ // unsafe_table_safe_index / unsafe_array_safe_index /
+ // unsafe_pointer_safe_index. Safe-at on vector / fixed_array is OK.
+ if (expr.genFlags.generated) return
+ if (expr.subexpr == null || expr.subexpr._type == null) {
+ self->mark_unsafe_in_stack()
+ return
+ }
+ let bt = expr.subexpr._type.baseType
+ if (bt == Type.tTable || bt == Type.tArray) {
+ self->mark_unsafe_in_stack()
+ return
+ }
+ if (bt == Type.tPointer && expr.subexpr._type.firstType != null) {
+ let inner = expr.subexpr._type.firstType.baseType
+ if (inner == Type.tTable || inner == Type.tArray || inner == Type.tPointer) {
+ self->mark_unsafe_in_stack()
+ }
+ }
+ }
+
def override preVisitExprField(expr : ExprField?) : void {
// variant.field requires unsafe (write context; over-mark on read
// is safer than under-mark — biases toward keeping wraps).
@@ -1408,14 +1486,15 @@ class StyleLintVisitor : AstVisitor {
|| (current_function != null && current_function.fromGeneric != null)
|| !(expr.body is ExprBlock)) return expr
let blk = expr.body as ExprBlock
- let count = self->unsafe_count_for(blk)
- if (count == 0) {
+ let frame = self->unsafe_frame_for(blk)
+ if (frame.count == 0) {
self->style_warning("STYLE024: redundant 'unsafe \{ ... }' block; no statement requires unsafe — drop the wrap", expr.at)
- } elif (count == 1 &&
+ } elif (frame.count == 1 &&
!(blk.list[0] is ExprYield ||
blk.list[0] is ExprDelete ||
blk.list[0] is ExprReturn ||
- blk.list[0] is ExprNew)) {
+ blk.list[0] is ExprNew)
+ && !frame.has_non_local_let_ref) {
self->style_warning("STYLE025: 'unsafe \{ ... }' block scope is too broad; only one operation requires unsafe — narrow to 'unsafe()' wrapping that operation", expr.at)
}
return expr
@@ -1450,7 +1529,18 @@ def public style_lint_collect(prog : ProgramPtr; var warnings : array; c
//! Runs the style lint visitor and collects warnings as strings.
//! Returns the number of warnings found.
//! Pass ``comment_hygiene = true`` to enable STYLE014/STYLE015 checks.
+ let empty_set : table
+ return style_lint_collect(prog, warnings, empty_set, empty_set, comment_hygiene)
+}
+
+def public style_lint_collect(prog : ProgramPtr; var warnings : array;
+ disabled_codes, enabled_codes : table;
+ comment_hygiene : bool = false) : int {
+ //! Filter-aware overload. `disabled_codes` is a denylist; `enabled_codes`
+ //! is a whitelist (empty == all). Codes use the bare form (e.g. "STYLE024").
var astVisitor = new StyleLintVisitor(compile_time_errors = false, collect_warnings = true, comment_hygiene = comment_hygiene)
+ astVisitor.disabled_codes := disabled_codes
+ astVisitor.enabled_codes := enabled_codes
make_visitor(*astVisitor) $(astVisitorAdapter) {
visit_with_generics(prog, astVisitorAdapter)
}
diff --git a/doc/source/reference/language/lint.rst b/doc/source/reference/language/lint.rst
index 6a25019d10..0df9bab30c 100644
--- a/doc/source/reference/language/lint.rst
+++ b/doc/source/reference/language/lint.rst
@@ -722,6 +722,56 @@ The rule deliberately does NOT cover:
Cross-type casts (widening, narrowing, signedness change, float ↔ int)
are genuine work and do NOT fire.
+PERF021 — hoist common workhorse cast out of ternary
+======================================================
+
+``cond ? T(a) : T(b)`` where both branches apply the **same** workhorse
+cast ``T`` emits two ``ExprCall`` nodes that do identical work regardless
+of which branch is taken. Hoisting the cast outside the ternary collapses
+them to one: ``T(cond ? a : b)``.
+
+Uses the same 15-name workhorse cast set as PERF020. The rule fires only
+when:
+
+- Both ternary branches are calls to the same workhorse cast name (after
+ peeling ``ExprRef2Value``).
+- Both calls share the same target ``Type``.
+- Both arguments share the same ``baseType`` — so the hoisted
+ ``T(cond ? a : b)`` typechecks without an intermediate cast.
+
+If the argument base types differ (e.g. ``cond ? string(intV) :
+string(int64V)``), the rule does NOT fire; the rewrite would need a
+manual widen on one branch and that is left to the author.
+
+.. code-block:: das
+
+ // Bad
+ def to_str(c : bool; a, b : int) : string {
+ return c ? string(a) : string(b) // PERF021
+ }
+
+ def widen(c : bool; a, b : int) : int64 {
+ return c ? int64(a) : int64(b) // PERF021
+ }
+
+ // Good
+ def to_str(c : bool; a, b : int) : string {
+ return string(c ? a : b)
+ }
+
+ def widen(c : bool; a, b : int) : int64 {
+ return int64(c ? a : b)
+ }
+
+The rewrite is unconditionally safe: the original ternary evaluates
+exactly one of ``a`` / ``b``, and so does the hoisted form — argument
+evaluation count is unchanged. Only the per-branch cast dispatch is
+eliminated.
+
+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.
+
.. _style_lint:
-----------
diff --git a/include/daScript/misc/memory_model.h b/include/daScript/misc/memory_model.h
index d95af0d29f..82311f754b 100644
--- a/include/daScript/misc/memory_model.h
+++ b/include/daScript/misc/memory_model.h
@@ -302,7 +302,10 @@ namespace das {
void setTrackAllocations ( bool on );
__forceinline bool isTrackingAllocations() const { return trackAllocations; }
CustomGrowFunction customGrow;
- uint32_t alignMask;
+ // Mask must be uint64 — `(size + alignMask) & ~alignMask` in allocate/free/reallocate
+ // would otherwise zero-extend `~alignMask` from uint32 to uint64 as 0x00000000FFFFFFF0,
+ // silently truncating any allocation ≥ 4 GB to its low 32 bits.
+ uint64_t alignMask;
uint64_t totalAllocated;
uint64_t maxAllocated;
uint64_t initialSize = 0;
@@ -411,7 +414,10 @@ namespace das {
CustomGrowFunction customGrow;
uint64_t unadjustedInitialSize = 0;
uint64_t initialSize = 0;
- uint32_t alignMask = 15;
+ // uint64 — see MemoryModel::alignMask. `~alignMask` must be uint64 so the
+ // `DAS_VERIFYF(s <= UINT32_MAX)` cap check in allocate() actually fires on
+ // >4 GB requests instead of seeing a silently-truncated low-32-bit size.
+ uint64_t alignMask = 15;
HeapChunk * chunk = nullptr;
};
diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp
index c38c099bf7..95e01f70de 100644
--- a/src/ast/ast_infer_type.cpp
+++ b/src/ast/ast_infer_type.cpp
@@ -2273,6 +2273,12 @@ namespace das {
} else if (expr->trait == "is_numeric") {
reportAstChanged();
return new ExprConstBool(expr->at, expr->typeexpr->isNumeric());
+ } else if (expr->trait == "is_int") {
+ reportAstChanged();
+ return new ExprConstBool(expr->at, expr->typeexpr->baseType == Type::tInt && expr->typeexpr->dim.size() == 0);
+ } else if (expr->trait == "is_int64") {
+ reportAstChanged();
+ return new ExprConstBool(expr->at, expr->typeexpr->baseType == Type::tInt64 && expr->typeexpr->dim.size() == 0);
} else if (expr->trait == "is_numeric_comparable") {
reportAstChanged();
return new ExprConstBool(expr->at, expr->typeexpr->isNumericComparable());
diff --git a/src/ast/ast_infer_type_op.cpp b/src/ast/ast_infer_type_op.cpp
index 9bddbfe66c..21442f29c0 100644
--- a/src/ast/ast_infer_type_op.cpp
+++ b/src/ast/ast_infer_type_op.cpp
@@ -221,6 +221,9 @@ namespace das {
}
} else if (expr->left->rtti_isAt()) {
ExprAt *eat = (ExprAt *)(expr->left);
+ if ( !eat->subexpr->type || eat->subexpr->type->isExprType() ) {
+ return nullptr;
+ }
auto complexName = "[]" + expr->name;
if (auto atComplex = inferGenericOperator3(complexName, eat->at, eat->subexpr, eat->index, expr->right)) {
atComplex->alwaysSafe = eat->alwaysSafe | expr->alwaysSafe;
diff --git a/src/simulate/simulate_fusion_at_array.cpp b/src/simulate/simulate_fusion_at_array.cpp
index f879b3ad47..7cda4d0e1a 100644
--- a/src/simulate/simulate_fusion_at_array.cpp
+++ b/src/simulate/simulate_fusion_at_array.cpp
@@ -35,6 +35,12 @@ namespace das {
uint32_t stride, offset;
};
+ // int64-indexed parallel base for fused arr[i64] access
+ struct SimNode_Op2ArrayAt_I64 : SimNode_Op2ArrayAt {};
+
+ // uint64-indexed parallel base for fused arr[u64] access
+ struct SimNode_Op2ArrayAt_U64 : SimNode_Op2ArrayAt {};
+
/* ArrayAtR2V SCALAR */
#define IMPLEMENT_OP2_SET_NODE_ANY(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL) \
@@ -151,10 +157,258 @@ namespace das {
IMPLEMENT_ANY_SETOP(__forceinline, ArrayAt, Ptr, StringPtr, StringPtr);
+/* ArrayAtR2V_I64 SCALAR */
+
+#undef IMPLEMENT_OP2_SET_NODE_ANY
+#define IMPLEMENT_OP2_SET_NODE_ANY(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL) \
+ struct SimNode_##OPNAME##_##COMPUTEL##_Any : SimNode_Op2ArrayAt_I64 { \
+ INLINE auto compute ( Context & context ) { \
+ DAS_PROFILE_NODE \
+ auto pl = (Array *) l.compute##COMPUTEL(context); \
+ int64_t rr = r.subexpr->evalInt64(context); \
+ if ( rr<0 || uint64_t(rr) >= pl->size ) context.throw_error_at(debugInfo,"array index out of range, %lld of %llu", (long long)rr, (unsigned long long)pl->size); \
+ return *((CTYPE *)(pl->data + uint64_t(rr)*uint64_t(stride) + offset)); \
+ } \
+ DAS_NODE(TYPE,CTYPE); \
+ };
+
+#undef IMPLEMENT_OP2_SET_NODE
+#define IMPLEMENT_OP2_SET_NODE(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL,COMPUTER) \
+ struct SimNode_##OPNAME##_##COMPUTEL##_##COMPUTER : SimNode_Op2ArrayAt_I64 { \
+ INLINE auto compute ( Context & context ) { \
+ DAS_PROFILE_NODE \
+ auto pl = (Array *) l.compute##COMPUTEL(context); \
+ int64_t rr = *((int64_t *)r.compute##COMPUTER(context)); \
+ if ( rr<0 || uint64_t(rr) >= pl->size ) context.throw_error_at(debugInfo,"array index out of range, %lld of %llu", (long long)rr, (unsigned long long)pl->size); \
+ return *((CTYPE *)(pl->data + uint64_t(rr)*uint64_t(stride) + offset)); \
+ } \
+ DAS_NODE(TYPE,CTYPE); \
+ };
+
+#undef IMPLEMENT_OP2_SET_SETUP_NODE
+#define IMPLEMENT_OP2_SET_SETUP_NODE(result,node) \
+ auto rn = (SimNode_Op2ArrayAt_I64 *)result; \
+ auto sn = (SimNode_ArrayAt_I64 *)node; \
+ rn->stride = sn->stride; \
+ rn->offset = sn->offset;
+
+#undef FUSION_OP2_SUBEXPR_LEFT
+#undef FUSION_OP2_SUBEXPR_RIGHT
+#define FUSION_OP2_SUBEXPR_LEFT(CTYPE,node) ((static_cast(node))->l)
+#define FUSION_OP2_SUBEXPR_RIGHT(CTYPE,node) ((static_cast(node))->r)
+
+#include "daScript/simulate/simulate_fusion_op2_set_impl.h"
+#include "daScript/simulate/simulate_fusion_op2_set_perm.h"
+
+ IMPLEMENT_SETOP_SCALAR(ArrayAtR2V_I64);
+
+/* ArrayAtR2V_I64 VECTOR */
+
+#undef IMPLEMENT_OP2_SET_NODE_ANY
+#define IMPLEMENT_OP2_SET_NODE_ANY(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL) \
+ struct SimNode_##OPNAME##_##COMPUTEL##_Any : SimNode_Op2ArrayAt_I64 { \
+ DAS_EVAL_ABI virtual vec4f eval ( Context & context ) override { \
+ DAS_PROFILE_NODE \
+ auto pl = (Array *) l.compute##COMPUTEL(context); \
+ int64_t rr = r.subexpr->evalInt64(context); \
+ if ( rr<0 || uint64_t(rr) >= pl->size ) context.throw_error_at(debugInfo,"array index out of range, %lld of %llu", (long long)rr, (unsigned long long)pl->size); \
+ vec4f __r; \
+ DAS_LDU_WORKHORSE(__r, pl->data + uint64_t(rr)*uint64_t(stride) + offset, CTYPE); \
+ return __r; \
+ } \
+ };
+
+#undef IMPLEMENT_OP2_SET_NODE
+#define IMPLEMENT_OP2_SET_NODE(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL,COMPUTER) \
+ struct SimNode_##OPNAME##_##COMPUTEL##_##COMPUTER : SimNode_Op2ArrayAt_I64 { \
+ DAS_EVAL_ABI virtual vec4f eval ( Context & context ) override { \
+ DAS_PROFILE_NODE \
+ auto pl = (Array *) l.compute##COMPUTEL(context); \
+ int64_t rr = *((int64_t *)r.compute##COMPUTER(context)); \
+ if ( rr<0 || uint64_t(rr) >= pl->size ) context.throw_error_at(debugInfo,"array index out of range, %lld of %llu", (long long)rr, (unsigned long long)pl->size); \
+ vec4f __r; \
+ DAS_LDU_WORKHORSE(__r, pl->data + uint64_t(rr)*uint64_t(stride) + offset, CTYPE); \
+ return __r; \
+ } \
+ };
+
+#include "daScript/simulate/simulate_fusion_op2_set_impl.h"
+#include "daScript/simulate/simulate_fusion_op2_set_perm.h"
+
+ IMPLEMENT_SETOP_NUMERIC_VEC(ArrayAtR2V_I64);
+
+/* ArrayAt_I64 */
+
+#undef IMPLEMENT_OP2_SET_NODE_ANY
+#define IMPLEMENT_OP2_SET_NODE_ANY(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL) \
+ struct SimNode_##OPNAME##_##COMPUTEL##_Any : SimNode_Op2ArrayAt_I64 { \
+ INLINE auto compute ( Context & context ) { \
+ DAS_PROFILE_NODE \
+ auto pl = (Array *) l.compute##COMPUTEL(context); \
+ int64_t rr = r.subexpr->evalInt64(context); \
+ if ( rr<0 || uint64_t(rr) >= pl->size ) context.throw_error_at(debugInfo,"array index out of range, %lld of %llu", (long long)rr, (unsigned long long)pl->size); \
+ return pl->data + uint64_t(rr)*uint64_t(stride) + offset; \
+ } \
+ DAS_PTR_NODE; \
+ };
+
+#undef IMPLEMENT_OP2_SET_NODE
+#define IMPLEMENT_OP2_SET_NODE(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL,COMPUTER) \
+ struct SimNode_##OPNAME##_##COMPUTEL##_##COMPUTER : SimNode_Op2ArrayAt_I64 { \
+ INLINE auto compute ( Context & context ) { \
+ DAS_PROFILE_NODE \
+ auto pl = (Array *) l.compute##COMPUTEL(context); \
+ int64_t rr = *((int64_t *)r.compute##COMPUTER(context)); \
+ if ( rr<0 || uint64_t(rr) >= pl->size ) context.throw_error_at(debugInfo,"array index out of range, %lld of %llu", (long long)rr, (unsigned long long)pl->size); \
+ return pl->data + uint64_t(rr)*uint64_t(stride) + offset; \
+ } \
+ DAS_PTR_NODE; \
+ };
+
+#undef IMPLEMENT_OP2_SET_SETUP_NODE
+#define IMPLEMENT_OP2_SET_SETUP_NODE(result,node) \
+ auto rn = (SimNode_Op2ArrayAt_I64 *)result; \
+ auto sn = (SimNode_ArrayAt_I64 *)node; \
+ rn->stride = sn->stride; \
+ rn->offset = sn->offset; \
+ rn->baseType = Type::none;
+
+#include "daScript/simulate/simulate_fusion_op2_set_impl.h"
+#include "daScript/simulate/simulate_fusion_op2_set_perm.h"
+
+ IMPLEMENT_ANY_SETOP(__forceinline, ArrayAt_I64, Ptr, StringPtr, StringPtr);
+
+/* ArrayAtR2V_U64 SCALAR */
+
+#undef IMPLEMENT_OP2_SET_NODE_ANY
+#define IMPLEMENT_OP2_SET_NODE_ANY(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL) \
+ struct SimNode_##OPNAME##_##COMPUTEL##_Any : SimNode_Op2ArrayAt_U64 { \
+ INLINE auto compute ( Context & context ) { \
+ DAS_PROFILE_NODE \
+ auto pl = (Array *) l.compute##COMPUTEL(context); \
+ uint64_t rr = r.subexpr->evalUInt64(context); \
+ if ( rr >= pl->size ) context.throw_error_at(debugInfo,"array index out of range, %llu of %llu", (unsigned long long)rr, (unsigned long long)pl->size); \
+ return *((CTYPE *)(pl->data + rr*uint64_t(stride) + offset)); \
+ } \
+ DAS_NODE(TYPE,CTYPE); \
+ };
+
+#undef IMPLEMENT_OP2_SET_NODE
+#define IMPLEMENT_OP2_SET_NODE(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL,COMPUTER) \
+ struct SimNode_##OPNAME##_##COMPUTEL##_##COMPUTER : SimNode_Op2ArrayAt_U64 { \
+ INLINE auto compute ( Context & context ) { \
+ DAS_PROFILE_NODE \
+ auto pl = (Array *) l.compute##COMPUTEL(context); \
+ uint64_t rr = *((uint64_t *)r.compute##COMPUTER(context)); \
+ if ( rr >= pl->size ) context.throw_error_at(debugInfo,"array index out of range, %llu of %llu", (unsigned long long)rr, (unsigned long long)pl->size); \
+ return *((CTYPE *)(pl->data + rr*uint64_t(stride) + offset)); \
+ } \
+ DAS_NODE(TYPE,CTYPE); \
+ };
+
+#undef IMPLEMENT_OP2_SET_SETUP_NODE
+#define IMPLEMENT_OP2_SET_SETUP_NODE(result,node) \
+ auto rn = (SimNode_Op2ArrayAt_U64 *)result; \
+ auto sn = (SimNode_ArrayAt_U64 *)node; \
+ rn->stride = sn->stride; \
+ rn->offset = sn->offset;
+
+#undef FUSION_OP2_SUBEXPR_LEFT
+#undef FUSION_OP2_SUBEXPR_RIGHT
+#define FUSION_OP2_SUBEXPR_LEFT(CTYPE,node) ((static_cast(node))->l)
+#define FUSION_OP2_SUBEXPR_RIGHT(CTYPE,node) ((static_cast(node))->r)
+
+#include "daScript/simulate/simulate_fusion_op2_set_impl.h"
+#include "daScript/simulate/simulate_fusion_op2_set_perm.h"
+
+ IMPLEMENT_SETOP_SCALAR(ArrayAtR2V_U64);
+
+/* ArrayAtR2V_U64 VECTOR */
+
+#undef IMPLEMENT_OP2_SET_NODE_ANY
+#define IMPLEMENT_OP2_SET_NODE_ANY(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL) \
+ struct SimNode_##OPNAME##_##COMPUTEL##_Any : SimNode_Op2ArrayAt_U64 { \
+ DAS_EVAL_ABI virtual vec4f eval ( Context & context ) override { \
+ DAS_PROFILE_NODE \
+ auto pl = (Array *) l.compute##COMPUTEL(context); \
+ uint64_t rr = r.subexpr->evalUInt64(context); \
+ if ( rr >= pl->size ) context.throw_error_at(debugInfo,"array index out of range, %llu of %llu", (unsigned long long)rr, (unsigned long long)pl->size); \
+ vec4f __r; \
+ DAS_LDU_WORKHORSE(__r, pl->data + rr*uint64_t(stride) + offset, CTYPE); \
+ return __r; \
+ } \
+ };
+
+#undef IMPLEMENT_OP2_SET_NODE
+#define IMPLEMENT_OP2_SET_NODE(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL,COMPUTER) \
+ struct SimNode_##OPNAME##_##COMPUTEL##_##COMPUTER : SimNode_Op2ArrayAt_U64 { \
+ DAS_EVAL_ABI virtual vec4f eval ( Context & context ) override { \
+ DAS_PROFILE_NODE \
+ auto pl = (Array *) l.compute##COMPUTEL(context); \
+ uint64_t rr = *((uint64_t *)r.compute##COMPUTER(context)); \
+ if ( rr >= pl->size ) context.throw_error_at(debugInfo,"array index out of range, %llu of %llu", (unsigned long long)rr, (unsigned long long)pl->size); \
+ vec4f __r; \
+ DAS_LDU_WORKHORSE(__r, pl->data + rr*uint64_t(stride) + offset, CTYPE); \
+ return __r; \
+ } \
+ };
+
+#include "daScript/simulate/simulate_fusion_op2_set_impl.h"
+#include "daScript/simulate/simulate_fusion_op2_set_perm.h"
+
+ IMPLEMENT_SETOP_NUMERIC_VEC(ArrayAtR2V_U64);
+
+/* ArrayAt_U64 */
+
+#undef IMPLEMENT_OP2_SET_NODE_ANY
+#define IMPLEMENT_OP2_SET_NODE_ANY(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL) \
+ struct SimNode_##OPNAME##_##COMPUTEL##_Any : SimNode_Op2ArrayAt_U64 { \
+ INLINE auto compute ( Context & context ) { \
+ DAS_PROFILE_NODE \
+ auto pl = (Array *) l.compute##COMPUTEL(context); \
+ uint64_t rr = r.subexpr->evalUInt64(context); \
+ if ( rr >= pl->size ) context.throw_error_at(debugInfo,"array index out of range, %llu of %llu", (unsigned long long)rr, (unsigned long long)pl->size); \
+ return pl->data + rr*uint64_t(stride) + offset; \
+ } \
+ DAS_PTR_NODE; \
+ };
+
+#undef IMPLEMENT_OP2_SET_NODE
+#define IMPLEMENT_OP2_SET_NODE(INLINE,OPNAME,TYPE,CTYPE,COMPUTEL,COMPUTER) \
+ struct SimNode_##OPNAME##_##COMPUTEL##_##COMPUTER : SimNode_Op2ArrayAt_U64 { \
+ INLINE auto compute ( Context & context ) { \
+ DAS_PROFILE_NODE \
+ auto pl = (Array *) l.compute##COMPUTEL(context); \
+ uint64_t rr = *((uint64_t *)r.compute##COMPUTER(context)); \
+ if ( rr >= pl->size ) context.throw_error_at(debugInfo,"array index out of range, %llu of %llu", (unsigned long long)rr, (unsigned long long)pl->size); \
+ return pl->data + rr*uint64_t(stride) + offset; \
+ } \
+ DAS_PTR_NODE; \
+ };
+
+#undef IMPLEMENT_OP2_SET_SETUP_NODE
+#define IMPLEMENT_OP2_SET_SETUP_NODE(result,node) \
+ auto rn = (SimNode_Op2ArrayAt_U64 *)result; \
+ auto sn = (SimNode_ArrayAt_U64 *)node; \
+ rn->stride = sn->stride; \
+ rn->offset = sn->offset; \
+ rn->baseType = Type::none;
+
+#include "daScript/simulate/simulate_fusion_op2_set_impl.h"
+#include "daScript/simulate/simulate_fusion_op2_set_perm.h"
+
+ IMPLEMENT_ANY_SETOP(__forceinline, ArrayAt_U64, Ptr, StringPtr, StringPtr);
+
void createFusionEngine_at_array() {
REGISTER_SETOP_SCALAR(ArrayAtR2V);
REGISTER_SETOP_NUMERIC_VEC(ArrayAtR2V);
(*getFusionEngine())["ArrayAt"].emplace_back(new FusionPoint_Set_ArrayAt_StringPtr());
+ REGISTER_SETOP_SCALAR(ArrayAtR2V_I64);
+ REGISTER_SETOP_NUMERIC_VEC(ArrayAtR2V_I64);
+ (*getFusionEngine())["ArrayAt_I64"].emplace_back(new FusionPoint_Set_ArrayAt_I64_StringPtr());
+ REGISTER_SETOP_SCALAR(ArrayAtR2V_U64);
+ REGISTER_SETOP_NUMERIC_VEC(ArrayAtR2V_U64);
+ (*getFusionEngine())["ArrayAt_U64"].emplace_back(new FusionPoint_Set_ArrayAt_U64_StringPtr());
}
}
diff --git a/tests-cpp/small/test_heap_64bit.cpp b/tests-cpp/small/test_heap_64bit.cpp
index b011ef87bf..87d4fcc93a 100644
--- a/tests-cpp/small/test_heap_64bit.cpp
+++ b/tests-cpp/small/test_heap_64bit.cpp
@@ -13,6 +13,7 @@
#include "daScript/daScript.h"
#include "daScript/daScriptC.h"
+#include "daScript/simulate/aot_builtin.h" // heap_bytes_allocated
#include
#include
@@ -116,6 +117,53 @@ TEST_CASE("legacy uint32_t C-API still works after heap widening") {
cleanup_inline_ctx(ic);
}
+TEST_CASE("alignMask uint32 truncation guard: 4 GB allocation reports correct bytesAllocated (gated)") {
+ // Repro for the alignMask uint32_t truncation in MemoryModel::allocate
+ // (`size = (size + alignMask) & ~alignMask` — `~alignMask` is uint32 and
+ // zero-extends to 0x00000000FFFFFFF0 when ANDed with uint64 size). Sizes
+ // ≥ 4 GB lose their high 32 bits, the function takes the shoe path with
+ // size=0, computes `si = (0>>4) - 1 = 0xFFFFFFFF`, and dereferences
+ // `chunks[0xFFFFFFFF]` — a wild-address read that crashes the process.
+ //
+ // On master with the bug: das_context_allocate_i64(ctx, 4GB) crashes
+ // inside MemoryModel::allocate via shoe.chunks OOB.
+ // After widening alignMask to uint64_t: the AND no longer truncates,
+ // the allocation lands in the bigStuff path, and bytesAllocated() grows
+ // by ≥ 4 GB. The CHECK below catches a regression where the mask flips
+ // back to uint32 (bytesAllocated grows by ~16 instead of ≥ 4 GB).
+ if constexpr ( sizeof(void*) < 8 ) {
+ WARN("DASLANG_HUGE_HEAP_TESTS: 32-bit build, skipping");
+ return;
+ }
+ const char * env = getenv("DASLANG_HUGE_HEAP_TESTS");
+ if ( !env || env[0] != '1' ) {
+ WARN("DASLANG_HUGE_HEAP_TESTS=1 not set, skipping 4 GB alignMask probe");
+ return;
+ }
+
+ // persistent_heap routes through PersistentHeapAllocator (MemoryModel/bigStuff).
+ // The default LinearHeapAllocator is uint32-bounded per the policy at
+ // memory_model.h:415-422 — >4GB allocations through it should panic with a
+ // clear message rather than silently truncate. PR-A's widening of
+ // LinearChunkAllocator::alignMask also enables that policy check to fire.
+ static const char * SRC =
+ "options gen2\n"
+ "options persistent_heap = true\n"
+ "[export] def main {}\n";
+ InlineCtx ic = compile_inline(SRC);
+ REQUIRE(ic.ctx != nullptr);
+
+ const uint64_t HUGE_BYTES = uint64_t(4) * 1024 * 1024 * 1024; // exactly 4 GB
+ const uint64_t before = heap_bytes_allocated(reinterpret_cast(ic.ctx));
+ void * p = das_context_allocate_i64(ic.ctx, HUGE_BYTES);
+ REQUIRE(p != nullptr);
+ const uint64_t after = heap_bytes_allocated(reinterpret_cast(ic.ctx));
+ CHECK(after - before >= HUGE_BYTES);
+ das_context_free_i64(ic.ctx, p, HUGE_BYTES);
+
+ cleanup_inline_ctx(ic);
+}
+
TEST_CASE("uint64 size accepts values larger than UINT32_MAX (gated)") {
// Only runs when DASLANG_HUGE_HEAP_TESTS=1 — a 5GB allocation isn't free
// even on big runners. Compile-time disabled on 32-bit builds where
@@ -130,7 +178,13 @@ TEST_CASE("uint64 size accepts values larger than UINT32_MAX (gated)") {
return;
}
- static const char * SRC = "options gen2\n[export] def main {}\n";
+ // persistent_heap required: default LinearHeapAllocator is uint32-bounded
+ // (per memory_model.h:415-422). >4GB allocations need PersistentHeapAllocator
+ // / MemoryModel::bigStuff path.
+ static const char * SRC =
+ "options gen2\n"
+ "options persistent_heap = true\n"
+ "[export] def main {}\n";
InlineCtx ic = compile_inline(SRC);
REQUIRE(ic.ctx != nullptr);
diff --git a/tests/linq/test_linq_fold.das b/tests/linq/test_linq_fold.das
index 07a18b8fcb..a9b0636a95 100644
--- a/tests/linq/test_linq_fold.das
+++ b/tests/linq/test_linq_fold.das
@@ -2776,3 +2776,70 @@ def test_take_while_skip_while_cascade_bails(t : T?) {
tt |> equal(0, length(got))
}
}
+
+[test]
+def test_fold_order_by_first(t : T?) {
+ t |> run("order_by + first → min_by") @(tt : T?) {
+ let v = _fold(each([10, 20, 5, 8, 30, 15, 2, 25])._order_by(_).first())
+ tt |> equal(2, v, "minimum element after order_by")
+ }
+ t |> run("order_by_descending + first → max_by") @(tt : T?) {
+ let v = _fold(each([10, 20, 5, 8, 30, 15, 2, 25])._order_by_descending(_).first())
+ tt |> equal(30, v, "maximum element after order_by_descending")
+ }
+ t |> run("where + order_by + first → min on prefilter buf") @(tt : T?) {
+ let v = _fold(each([10, 20, 5, 8, 30, 15, 2, 25])._where(_ > 5)._order_by(_).first())
+ tt |> equal(8, v, "min of (10,20,8,30,15,25) = 8")
+ }
+ t |> run("order_by + first_or_default — empty source returns default") @(tt : T?) {
+ let empty : array
+ let v = _fold(each(empty)._order_by(_).first_or_default(-1))
+ tt |> equal(-1, v, "empty source → default")
+ }
+ t |> run("order_by + first_or_default — non-empty returns min") @(tt : T?) {
+ let v = _fold(each([10, 5, 20])._order_by(_).first_or_default(-1))
+ tt |> equal(5, v, "non-empty source → min element")
+ }
+ t |> run("where + order_by + first_or_default — empty after filter") @(tt : T?) {
+ let v = _fold(each([1, 2, 3])._where(_ > 100)._order_by(_).first_or_default(-1))
+ tt |> equal(-1, v, "filter excludes all → default")
+ }
+ t |> run("order_by + first on empty array panics") @(tt : T?) {
+ let empty : array
+ var didPanic = false
+ try {
+ let v = _fold(each(empty)._order_by(_).first())
+ tt |> success(false, "expected panic; got {v}")
+ } recover {
+ didPanic = true
+ }
+ tt |> success(didPanic, "first() on empty array panicked")
+ }
+ t |> run("where + order_by + first filtered-empty panics") @(tt : T?) {
+ var didPanic = false
+ try {
+ let v = _fold(each([1, 2, 3])._where(_ > 100)._order_by(_).first())
+ tt |> success(false, "expected panic; got {v}")
+ } recover {
+ didPanic = true
+ }
+ tt |> success(didPanic, "first() on filtered-empty array panicked")
+ }
+ // ── order(arr, cmp) + first — splice MUST honor the custom comparator (was emitting bare min/max, ignoring cmp)
+ t |> run("order(arr, cmp) + first preserves custom comparator (descending cmp → max)") @(tt : T?) {
+ let v = _fold(each([10, 5, 20, 3]) |> order($(a : int, b : int) => a > b) |> first())
+ tt |> equal(20, v, "cmp orders descending → first returns max")
+ }
+ t |> run("order_descending(arr, cmp) + first preserves custom comparator") @(tt : T?) {
+ // order_descending swaps cmp args, so cmp `a > b` becomes effectively `b > a` (ascending) → first → min
+ let v = _fold(each([10, 5, 20, 3]) |> order_descending($(a : int, b : int) => a > b) |> first())
+ tt |> equal(3, v, "order_descending negates cmp → first is min")
+ }
+ t |> run("order(arr, cmp) + take preserves custom comparator (pre-existing recognizer hole)") @(tt : T?) {
+ // cmp orders descending → take(2) returns top 2
+ let result <- _fold(each([10, 5, 20, 3, 25, 8]) |> order($(a : int, b : int) => a > b) |> take(2) |> to_array())
+ tt |> equal(2, length(result), "take 2 of descending order")
+ tt |> equal(25, result[0], "first is max (25)")
+ tt |> equal(20, result[1], "second is next-max (20)")
+ }
+}
diff --git a/tests/linq/test_linq_fold_ast.das b/tests/linq/test_linq_fold_ast.das
index 86e3153bd3..d80344431e 100644
--- a/tests/linq/test_linq_fold_ast.das
+++ b/tests/linq/test_linq_fold_ast.das
@@ -160,6 +160,103 @@ def target_zip_impure_select_skip_bails() : array {
return <- [10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> select($(t : tuple) => side_effect_zip_proj(t._0)) |> skip(2) |> _fold()
}
+// ── PR Phase 2B+ — accumulator (sum/min/max/average) terminators on zip ──
+// sum/min/max/average require a `select` projection to scalarize the tuple element (accumulator ops aren't defined on tuples). `long_count` is the exception: it routes through the COUNTER path locally (length shortcut + counter loop) since the accumulator is just `int64 acc++` regardless of element type.
+[export, marker(no_coverage)]
+def target_zip_sum_proj_fold() : int {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> select($(t : tuple) => t._0 + t._1) |> sum())
+}
+
+[export, marker(no_coverage)]
+def target_zip_min_proj_fold() : int {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> select($(t : tuple) => t._0 - t._1) |> min())
+}
+
+[export, marker(no_coverage)]
+def target_zip_max_proj_fold() : int {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> select($(t : tuple) => t._0 * t._1) |> max())
+}
+
+[export, marker(no_coverage)]
+def target_zip_average_proj_fold() : double {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> select($(t : tuple) => t._0 + t._1) |> average())
+}
+
+[export, marker(no_coverage)]
+def target_zip_where_sum_proj_fold() : int {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> where_($(t : tuple) => t._0 > 20) |> select($(t : tuple) => t._0 + t._1) |> sum())
+}
+
+[export, marker(no_coverage)]
+def target_zip_where_long_count_fold() : int64 {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> where_($(t : tuple) => t._0 > 20) |> long_count())
+}
+
+// ── PR Phase 2B+ — early-exit (first/first_or_default/any/all/contains) terminators on zip ──
+[export, marker(no_coverage)]
+def target_zip_first_no_proj_fold() : tuple {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> first())
+}
+
+[export, marker(no_coverage)]
+def target_zip_first_proj_fold() : int {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> select($(t : tuple) => t._0 + t._1) |> first())
+}
+
+[export, marker(no_coverage)]
+def target_zip_where_first_fold() : tuple {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> where_($(t : tuple) => t._0 > 20) |> first())
+}
+
+[export, marker(no_coverage)]
+def target_zip_first_or_default_proj_fold() : int {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> select($(t : tuple) => t._0 + t._1) |> where_($(v : int) => v > 1000) |> first_or_default(-1))
+}
+
+[export, marker(no_coverage)]
+def target_zip_any_no_pred_fold() : bool {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> any())
+}
+
+[export, marker(no_coverage)]
+def target_zip_any_no_pred_empty_fold() : bool {
+ var emptyA : array
+ return _fold(emptyA.zip([1, 2, 3]) |> any())
+}
+
+[export, marker(no_coverage)]
+def target_zip_any_pred_fold() : bool {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> any($(t : tuple) => t._0 > 30))
+}
+
+[export, marker(no_coverage)]
+def target_zip_all_pred_true_fold() : bool {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> all($(t : tuple) => t._0 > 0))
+}
+
+[export, marker(no_coverage)]
+def target_zip_all_pred_false_fold() : bool {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> all($(t : tuple) => t._0 > 30))
+}
+
+[export, marker(no_coverage)]
+def target_zip_contains_proj_fold() : bool {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> select($(t : tuple) => t._0 + t._1) |> contains(33))
+}
+
+[export, marker(no_coverage)]
+def target_zip_contains_proj_miss_fold() : bool {
+ return _fold([10, 20, 30, 40, 50].zip([1, 2, 3, 4, 5]) |> select($(t : tuple) => t._0 + t._1) |> contains(999))
+}
+
+// PR #2742 Copilot review #r3270337476 — DEFERRED: emit_accumulator_lane.average semantics divergence from linq.das. Pre-existing in helper (affects plan_loop_or_count's single-source path too): accumulates in accType (often int → overflow risk) and returns NaN on empty cnt, while linq.das average accumulates in double and returns 0.0lf on empty. Follow-up PR must fix the helper uniformly AND update the existing fold test "average: empty → NaN" in test_linq_fold.das to expect 0.0lf.
+[export, marker(no_coverage)]
+def target_zip_average_empty_fold() : double {
+ var emptyA : array
+ var emptyB : array
+ return _fold(emptyA.zip(emptyB) |> select($(t : tuple) => t._0 + t._1) |> average())
+}
+
[export, marker(no_coverage)]
def target_zip3_fold() : array> {
return <- [1, 2, 3]._select(_ * 2).zip([10, 20, 30]._select(_ + 1), [100, 200, 300]._select(_ / 10))._fold()
@@ -339,6 +436,9 @@ def test_zip_long_count_uses_length_shortcut(t : T?) {
t |> success(r.matched && body_expr is ExprInvoke, "expected splice invoke wrapper")
let n = count_inner_for_loops(body_expr)
t |> equal(n, 0, "zip(arr,arr).long_count() must use length shortcut (no for-loop)")
+ // Distinguish length-shortcut splice from tier-2 cascade by elimination: for_loops==0 rules out the counter-loop splice path; long_count==0 rules out tier-2 (which leaves the long_count call in place). Both together ⇒ length shortcut. (Can't grep for length() directly: count_call doesn't recurse into ExprOp3 ternary where length() lives in the min(...) expression.)
+ let longCountCalls = count_call(body_expr, "long_count")
+ t |> equal(longCountCalls, 0, "long_count must be inlined into the splice (no runtime call)")
}
}
@@ -569,6 +669,213 @@ def test_zip_select_count_pure_skips_it_bind(t : T?) {
}
}
+// ── PR Phase 2B+ accumulator (sum/min/max/average) terminators on zip ──
+// Source: arrA=[10,20,30,40,50], arrB=[1,2,3,4,5]; pairs are (10,1)(20,2)(30,3)(40,4)(50,5).
+
+[test]
+def test_zip_sum_proj_fold_result(t : T?) {
+ t |> run("zip+select+sum returns scalar accumulator") @(t : T?) {
+ // (10+1)+(20+2)+(30+3)+(40+4)+(50+5) = 11+22+33+44+55 = 165
+ t |> equal(target_zip_sum_proj_fold(), 165)
+ }
+}
+
+[test]
+def test_zip_min_proj_fold_result(t : T?) {
+ t |> run("zip+select+min picks smallest projected value") @(t : T?) {
+ // diffs: 9,18,27,36,45 → min 9
+ t |> equal(target_zip_min_proj_fold(), 9)
+ }
+}
+
+[test]
+def test_zip_max_proj_fold_result(t : T?) {
+ t |> run("zip+select+max picks largest projected value") @(t : T?) {
+ // products: 10,40,90,160,250 → max 250
+ t |> equal(target_zip_max_proj_fold(), 250)
+ }
+}
+
+[test]
+def test_zip_average_proj_fold_result(t : T?) {
+ t |> run("zip+select+average returns double") @(t : T?) {
+ // (11+22+33+44+55)/5 = 33.0
+ t |> equal(target_zip_average_proj_fold(), 33.0lf)
+ }
+}
+
+[test]
+def test_zip_where_sum_proj_fold_result(t : T?) {
+ t |> run("zip+where+select+sum filters then sums") @(t : T?) {
+ // survivors where t._0>20: (30,3)(40,4)(50,5) → 33+44+55 = 132
+ t |> equal(target_zip_where_sum_proj_fold(), 132)
+ }
+}
+
+[test]
+def test_zip_where_long_count_fold_result(t : T?) {
+ t |> run("zip+where+long_count returns int64 survivor count") @(t : T?) {
+ // 3 survivors
+ t |> equal(target_zip_where_long_count_fold(), 3l)
+ }
+}
+
+// long_count + chain op (where) must emit a single multi-iter counter for-loop, not cascade to tier-2. Regression guard against routing long_count through the ACCUMULATOR projection-required branch (PR #2742 Copilot review #r3270242609).
+[test]
+def test_zip_where_long_count_emits_counter_loop(t : T?) {
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_zip_where_long_count_fold)
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return $e(body_expr)
+ }
+ t |> success(r.matched && body_expr is ExprInvoke, "expected splice invoke wrapper")
+ let nfor = count_inner_for_loops(body_expr)
+ t |> equal(nfor, 1, "zip+where+long_count must emit one multi-iter counter for-loop")
+ let zipCalls = count_call(body_expr, "zip")
+ t |> equal(zipCalls, 0, "zip call must be inlined into the splice")
+ let longCountCalls = count_call(body_expr, "long_count")
+ t |> equal(longCountCalls, 0, "long_count terminator must be fused (no runtime call)")
+ }
+}
+
+// ── PR Phase 2B+ early-exit (first/first_or_default/any/all/contains) on zip ──
+
+[test]
+def test_zip_first_no_proj_fold_result(t : T?) {
+ t |> run("zip+first (no projection) returns first tuple") @(t : T?) {
+ let result = target_zip_first_no_proj_fold()
+ t |> equal(result._0, 10)
+ t |> equal(result._1, 1)
+ }
+}
+
+[test]
+def test_zip_first_proj_fold_result(t : T?) {
+ t |> run("zip+select+first returns first projected scalar") @(t : T?) {
+ // first pair (10,1) projected → 11
+ t |> equal(target_zip_first_proj_fold(), 11)
+ }
+}
+
+[test]
+def test_zip_where_first_fold_result(t : T?) {
+ t |> run("zip+where+first returns first survivor tuple") @(t : T?) {
+ // first pair where t._0>20: (30,3)
+ let result = target_zip_where_first_fold()
+ t |> equal(result._0, 30)
+ t |> equal(result._1, 3)
+ }
+}
+
+[test]
+def test_zip_first_or_default_proj_fold_result(t : T?) {
+ t |> run("zip+select+where+first_or_default returns default when no survivor") @(t : T?) {
+ // no projected sum > 1000, so default -1 fires
+ t |> equal(target_zip_first_or_default_proj_fold(), -1)
+ }
+}
+
+[test]
+def test_zip_any_no_pred_fold_result(t : T?) {
+ t |> run("zip+any (no pred) on non-empty pair → true") @(t : T?) {
+ t |> equal(target_zip_any_no_pred_fold(), true)
+ }
+}
+
+[test]
+def test_zip_any_no_pred_empty_fold_result(t : T?) {
+ t |> run("zip+any (no pred) on empty source → false") @(t : T?) {
+ // emptyA.zip(...) → min(0,3) = 0 iters → any returns false
+ t |> equal(target_zip_any_no_pred_empty_fold(), false)
+ }
+}
+
+[test]
+def test_zip_any_pred_fold_result(t : T?) {
+ t |> run("zip+any(pred) returns true if any pair matches") @(t : T?) {
+ // (40,4) and (50,5) satisfy t._0>30 → true
+ t |> equal(target_zip_any_pred_fold(), true)
+ }
+}
+
+[test]
+def test_zip_all_pred_true_fold_result(t : T?) {
+ t |> run("zip+all(pred) returns true when all match") @(t : T?) {
+ t |> equal(target_zip_all_pred_true_fold(), true)
+ }
+}
+
+[test]
+def test_zip_all_pred_false_fold_result(t : T?) {
+ t |> run("zip+all(pred) returns false when any fails") @(t : T?) {
+ // 10, 20, 30 fail t._0>30
+ t |> equal(target_zip_all_pred_false_fold(), false)
+ }
+}
+
+[test]
+def test_zip_contains_proj_fold_result(t : T?) {
+ t |> run("zip+select+contains returns true on hit") @(t : T?) {
+ // 30+3 = 33 matches
+ t |> equal(target_zip_contains_proj_fold(), true)
+ }
+}
+
+[test]
+def test_zip_contains_proj_miss_fold_result(t : T?) {
+ t |> run("zip+select+contains returns false on miss") @(t : T?) {
+ t |> equal(target_zip_contains_proj_miss_fold(), false)
+ }
+}
+
+// PR #2742 Copilot review #r3270337476 — DEFERRED-fix tracking test. Documents the desired post-fix behavior (zip+select+average on empty source should return 0.0lf per linq.das semantics). Currently emit_accumulator_lane returns NaN, so this test is skipped until the helper is aligned (uniform fix across plan_loop_or_count + plan_zip; also requires updating "average: empty → NaN" in test_linq_fold.das to expect 0.0lf).
+[test]
+def test_zip_average_empty_returns_zero_when_fixed(t : T?) {
+ t->skip("DEFERRED: zip+select+average on empty source should return 0.0lf per linq.das semantics. Blocked on follow-up PR aligning emit_accumulator_lane.average with linq.das (double accumulator + cnt==0 guard); pre-existing helper divergence — affects single-source plan_loop_or_count too. Un-skip + assert `equal(target_zip_average_empty_fold(), 0.0lf)` when fix lands.")
+}
+
+// AST-shape: zip+accumulator splices into a single for-loop with no surviving runtime `sum`/`zip` calls.
+[test]
+def test_zip_sum_proj_emits_inline_accumulator(t : T?) {
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_zip_sum_proj_fold)
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return $e(body_expr)
+ }
+ t |> success(r.matched && body_expr is ExprInvoke, "expected splice invoke wrapper")
+ let nfor = count_inner_for_loops(body_expr)
+ t |> equal(nfor, 1, "zip+select+sum must emit exactly one multi-iter for-loop")
+ let zipCalls = count_call(body_expr, "zip")
+ t |> equal(zipCalls, 0, "zip call must be inlined into the splice")
+ let sumCalls = count_call(body_expr, "sum")
+ t |> equal(sumCalls, 0, "sum terminator must be fused (no runtime sum call)")
+ }
+}
+
+// AST-shape: zip+early-exit emits the same single-for shape with no surviving terminator call.
+[test]
+def test_zip_first_proj_emits_early_return(t : T?) {
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_zip_first_proj_fold)
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return $e(body_expr)
+ }
+ t |> success(r.matched && body_expr is ExprInvoke, "expected splice invoke wrapper")
+ let nfor = count_inner_for_loops(body_expr)
+ t |> equal(nfor, 1, "zip+select+first must emit exactly one multi-iter for-loop")
+ let zipCalls = count_call(body_expr, "zip")
+ t |> equal(zipCalls, 0, "zip call must be inlined")
+ let firstCalls = count_call(body_expr, "first")
+ t |> equal(firstCalls, 0, "first terminator must be fused (no runtime first call)")
+ }
+}
+
// ── Targets for `_fold` Phase-2A loop planner ──────────────────────────
[export, marker(no_coverage)]
@@ -3531,3 +3838,130 @@ def test_select_then_take_while_cascades_to_tier2(t : T?) {
}
}
+// ── order_by + first → min_by / max_by splice arm ───────────────────────
+
+[export, marker(no_coverage)]
+def target_order_by_first_splices_min_by() : int {
+ return _fold(each([10, 20, 5, 8, 30, 15, 2, 25])._order_by(_).first())
+}
+
+[export, marker(no_coverage)]
+def target_order_by_descending_first_splices_max_by() : int {
+ return _fold(each([10, 20, 5, 8, 30, 15, 2, 25])._order_by_descending(_).first())
+}
+
+[export, marker(no_coverage)]
+def target_where_order_by_first_splices_min_by_on_buf() : int {
+ return _fold(each([10, 20, 5, 8, 30, 15, 2, 25])._where(_ > 5)._order_by(_).first())
+}
+
+[export, marker(no_coverage)]
+def target_order_by_first_or_default_splices_top_n_by() : int {
+ return _fold(each([10, 20, 5, 8, 30])._order_by(_).first_or_default(-1))
+}
+
+[export, marker(no_coverage)]
+def target_order_with_cmp_first_bails_to_tier2() : int {
+ // order(arr, cmp) with a custom comparator — splice must bail (min/max can't honor arbitrary cmp).
+ return _fold(each([10, 5, 20, 3]) |> order($(a : int, b : int) => a > b) |> first())
+}
+
+[test]
+def test_order_by_first_emits_min_by(t : T?) {
+ // order_by + first → min_by(top, key) directly. No sort, no take, no order_by/order helpers.
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_order_by_first_splices_min_by)
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return $e(body_expr)
+ }
+ t |> success(r.matched, "should have return expression")
+ t |> success(count_call(body_expr, "min_by") >= 1, "should emit a min_by call")
+ t |> equal(0, count_call(body_expr, "max_by"), "should NOT emit max_by (ascending)")
+ t |> equal(0, count_call(body_expr, "order_by"), "should NOT emit order_by")
+ t |> equal(0, count_call(body_expr, "first"), "should NOT emit first (splice absorbs)")
+ }
+}
+
+[test]
+def test_order_by_descending_first_emits_max_by(t : T?) {
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_order_by_descending_first_splices_max_by)
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return $e(body_expr)
+ }
+ t |> success(r.matched, "should have return expression")
+ t |> success(count_call(body_expr, "max_by") >= 1, "should emit a max_by call")
+ t |> equal(0, count_call(body_expr, "min_by"), "should NOT emit min_by (descending)")
+ t |> equal(0, count_call(body_expr, "order_by_descending"), "should NOT emit order_by_descending")
+ t |> equal(0, count_call(body_expr, "first"), "should NOT emit first (splice absorbs)")
+ }
+}
+
+[test]
+def test_where_order_by_first_emits_min_by_on_prefilter(t : T?) {
+ // where + order_by + first → fused prefilter loop + min_by on buf. Same shape as
+ // where + order_by + take, but min_by instead of top_n_by.
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_where_order_by_first_splices_min_by_on_buf)
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return $e(body_expr)
+ }
+ t |> success(r.matched, "should have return expression")
+ t |> success(count_call(body_expr, "min_by") >= 1, "should emit min_by on prefilter buf")
+ t |> equal(0, count_call(body_expr, "order_by"), "should NOT emit order_by")
+ t |> equal(0, count_call(body_expr, "first"), "should NOT emit first (splice absorbs)")
+ }
+}
+
+[test]
+def test_order_by_first_or_default_emits_top_n_by_first_or_default(t : T?) {
+ // order_by + first_or_default → top_n_by(top, 1, key) |> first_or_default(d) — empty
+ // array handles the default fallback; no min_by_or_default helper exists.
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_order_by_first_or_default_splices_top_n_by)
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return $e(body_expr)
+ }
+ t |> success(r.matched, "should have return expression")
+ t |> success(count_call(body_expr, "top_n_by_with_cmp") + count_call(body_expr, "top_n_by") >= 1,
+ "should emit top_n_by (or top_n_by_with_cmp for inline key)")
+ t |> success(count_call(body_expr, "first_or_default") >= 1,
+ "should emit first_or_default on the 1-elem array")
+ t |> equal(0, count_call(body_expr, "order_by"), "should NOT emit order_by")
+ }
+}
+
+[test]
+def test_order_with_cmp_first_bails_no_min_max(t : T?) {
+ // `order(arr, cmp)` with a custom comparator block: splice helpers can't honor an arbitrary cmp,
+ // so plan_order_family must bail. fold_linq_default takes over and rewrites to `order_to_array` + `first`.
+ // The key invariant: NO min/max splice (which would silently drop the cmp).
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_order_with_cmp_first_bails_to_tier2)
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return $e(body_expr)
+ }
+ t |> success(r.matched, "should have return expression")
+ t |> equal(0, count_call(body_expr, "min_by"), "must NOT emit min_by (cmp can't be honored)")
+ t |> equal(0, count_call(body_expr, "max_by"), "must NOT emit max_by")
+ t |> equal(0, count_call(body_expr, "min"), "must NOT emit min")
+ t |> equal(0, count_call(body_expr, "max"), "must NOT emit max")
+ t |> equal(0, count_call(body_expr, "top_n"), "must NOT emit top_n (bare top_n would drop cmp)")
+ t |> equal(0, count_call(body_expr, "top_n_by"), "must NOT emit top_n_by")
+ // fold_linq_default rewrites the bailed chain to order_to_array + first. Confirm a sort step survives.
+ t |> success(count_call(body_expr, "order_to_array") + count_call(body_expr, "order") >= 1,
+ "sort step must survive (order_to_array or order)")
+ t |> success(count_call(body_expr, "first") >= 1, "first call must survive")
+ }
+}
+
diff --git a/tests/linq/test_linq_from_decs.das b/tests/linq/test_linq_from_decs.das
index 46fdc7aab9..eae9057428 100644
--- a/tests/linq/test_linq_from_decs.das
+++ b/tests/linq/test_linq_from_decs.das
@@ -321,3 +321,364 @@ def test_unroll_select_sum_splice_shape(t : T?) {
t |> equal(describe_count(body_expr, "for_each_archetype"), 1, "exactly one for_each_archetype")
}
}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Slice 3a: min/max/average accumulator extensions
+// ─────────────────────────────────────────────────────────────────────────────
+
+[export, marker(no_coverage)]
+def target_unroll_min_fold() : int {
+ return _fold(from_decs_template(type)._select(_.val).min())
+}
+
+[export, marker(no_coverage)]
+def target_unroll_max_fold() : int {
+ return _fold(from_decs_template(type)._select(_.val).max())
+}
+
+[export, marker(no_coverage)]
+def target_unroll_average_fold() : double {
+ return _fold(from_decs_template(type)._select(_.val).average())
+}
+
+[export, marker(no_coverage)]
+def target_unroll_where_max_fold() : int {
+ return _fold(from_decs_template(type)._where(_.flag == 1)._select(_.val).max())
+}
+
+[test]
+def test_unroll_min_parity(t : T?) {
+ fixture_unroll2(5)
+ // vals 0,1,2,3,4 → min = 0
+ t |> equal(target_unroll_min_fold(), 0, "min splice parity")
+}
+
+[test]
+def test_unroll_max_parity(t : T?) {
+ fixture_unroll2(5)
+ // vals 0,1,2,3,4 → max = 4
+ t |> equal(target_unroll_max_fold(), 4, "max splice parity")
+}
+
+[test]
+def test_unroll_average_parity(t : T?) {
+ fixture_unroll2(5)
+ // vals 0,1,2,3,4 → avg = 10/5 = 2.0
+ t |> equal(target_unroll_average_fold(), 2.0lf, "average splice parity")
+}
+
+[test]
+def test_unroll_where_max_parity(t : T?) {
+ fixture_unroll2(5)
+ // flag==1 rows: vals 1,3 → max 3
+ t |> equal(target_unroll_where_max_fold(), 3, "where+select+max splice parity")
+}
+
+[test]
+def test_unroll_max_splice_shape(t : T?) {
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_unroll_max_fold)
+ t |> success(func != null, "RTTI must resolve target_unroll_max_fold")
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return <- $e(body_expr)
+ }
+ t |> success(r.matched && body_expr is ExprInvoke, "expected splice invoke wrapper")
+ t |> equal(describe_count(body_expr, "to_sequence"), 0, "max splice must NOT call to_sequence")
+ t |> equal(describe_count(body_expr, "for_each_archetype"), 1, "exactly one for_each_archetype")
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Slice 3b: early-exit terminators (first/first_or_default/any/all/contains)
+// ─────────────────────────────────────────────────────────────────────────────
+
+[export, marker(no_coverage)]
+def target_unroll_any_bare_fold() : bool {
+ return _fold(from_decs_template(type).any())
+}
+
+[export, marker(no_coverage)]
+def target_unroll_any_pred_fold() : bool {
+ return _fold(from_decs_template(type)._select(_.val)._any(_ > 100))
+}
+
+[export, marker(no_coverage)]
+def target_unroll_all_fold() : bool {
+ return _fold(from_decs_template(type)._select(_.val)._all(_ >= 0))
+}
+
+[export, marker(no_coverage)]
+def target_unroll_contains_fold() : bool {
+ return _fold(from_decs_template(type)._select(_.val).contains(3))
+}
+
+[export, marker(no_coverage)]
+def target_unroll_first_fold() : int {
+ return _fold(from_decs_template(type)._where(_.flag == 1)._select(_.val).first())
+}
+
+[export, marker(no_coverage)]
+def target_unroll_first_or_default_fold() : int {
+ return _fold(from_decs_template(type)._where(_.val > 1000)._select(_.val).first_or_default(-7))
+}
+
+[test]
+def test_unroll_any_bare_parity(t : T?) {
+ fixture_unroll2(3)
+ t |> equal(target_unroll_any_bare_fold(), true, "bare any with entities")
+ restart()
+ commit()
+ t |> equal(target_unroll_any_bare_fold(), false, "bare any with no entities")
+}
+
+[test]
+def test_unroll_any_pred_parity(t : T?) {
+ fixture_unroll2(5)
+ // vals 0..4, none > 100
+ t |> equal(target_unroll_any_pred_fold(), false, "any(pred) — no match")
+ fixture_unroll2(150)
+ // vals 0..149, some > 100
+ t |> equal(target_unroll_any_pred_fold(), true, "any(pred) — has match")
+}
+
+[test]
+def test_unroll_all_parity(t : T?) {
+ fixture_unroll2(5)
+ t |> equal(target_unroll_all_fold(), true, "all(>=0) on 0..4")
+}
+
+[test]
+def test_unroll_contains_parity(t : T?) {
+ fixture_unroll2(5)
+ t |> equal(target_unroll_contains_fold(), true, "contains(3) in 0..4")
+ fixture_unroll2(2)
+ t |> equal(target_unroll_contains_fold(), false, "contains(3) in 0..1")
+}
+
+[test]
+def test_unroll_first_parity(t : T?) {
+ fixture_unroll2(5)
+ // flag==1 first: i=1 → val=1
+ t |> equal(target_unroll_first_fold(), 1, "first(where flag==1)")
+}
+
+[test]
+def test_unroll_first_or_default_parity(t : T?) {
+ fixture_unroll2(5)
+ // val>1000 → none match → default -7
+ t |> equal(target_unroll_first_or_default_fold(), -7, "first_or_default empty match")
+}
+
+[test]
+def test_unroll_first_splice_shape(t : T?) {
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_unroll_first_fold)
+ t |> success(func != null, "RTTI must resolve target_unroll_first_fold")
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return <- $e(body_expr)
+ }
+ t |> success(r.matched && body_expr is ExprInvoke, "expected splice invoke wrapper")
+ t |> equal(describe_count(body_expr, "to_sequence"), 0, "first splice must NOT call to_sequence")
+ t |> equal(describe_count(body_expr, "for_each_archetype_find"), 1, "first splice uses for_each_archetype_find")
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Slice 3c: to_array buffer terminator
+// ─────────────────────────────────────────────────────────────────────────────
+
+[export, marker(no_coverage)]
+def target_unroll_to_array_fold() : array {
+ return <- _fold(from_decs_template(type)._where(_.flag == 1)._select(_.val).to_array())
+}
+
+[test]
+def test_unroll_to_array_parity(t : T?) {
+ fixture_unroll2(6)
+ // flag==1 rows: i=1,3,5 → vals 1,3,5
+ let got <- target_unroll_to_array_fold()
+ t |> equal(got |> length, 3, "to_array length")
+ if (length(got) == 3) {
+ t |> equal(got[0], 1, "to_array[0]")
+ t |> equal(got[1], 3, "to_array[1]")
+ t |> equal(got[2], 5, "to_array[2]")
+ }
+}
+
+[test]
+def test_unroll_to_array_splice_shape(t : T?) {
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_unroll_to_array_fold)
+ t |> success(func != null, "RTTI must resolve target_unroll_to_array_fold")
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return <- $e(body_expr)
+ }
+ t |> success(r.matched && body_expr is ExprInvoke, "expected splice invoke wrapper")
+ t |> equal(describe_count(body_expr, "to_sequence"), 0, "to_array splice must NOT call to_sequence")
+ t |> equal(describe_count(body_expr, "for_each_archetype"), 1, "exactly one for_each_archetype")
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Slice 4: chained _select + interleaved _where + _count(pred) + _min_by / _max_by
+// ─────────────────────────────────────────────────────────────────────────────
+
+[export, marker(no_coverage)]
+def target_unroll_chained_select_sum_fold() : int {
+ return _fold(from_decs_template(type)._select(_.val)._select(_ * 10).sum())
+}
+
+[export, marker(no_coverage)]
+def target_unroll_select_then_where_sum_fold() : int {
+ return _fold(from_decs_template(type)._select(_.val)._where(_ > 1).sum())
+}
+
+[export, marker(no_coverage)]
+def target_unroll_where_select_where_sum_fold() : int {
+ return _fold(from_decs_template(type)._where(_.flag == 1)._select(_.val)._where(_ > 1).sum())
+}
+
+[export, marker(no_coverage)]
+def target_unroll_count_pred_fold() : int {
+ return _fold(from_decs_template(type)._count(_.val > 2))
+}
+
+[export, marker(no_coverage)]
+def target_unroll_long_count_pred_fold() : int64 {
+ return _fold(from_decs_template(type)._long_count(_.val >= 3))
+}
+
+[export, marker(no_coverage)]
+def target_unroll_chained_select_to_array_fold() : array {
+ return <- _fold(from_decs_template(type)._select(_.val)._select(_ + 100).to_array())
+}
+
+[export, marker(no_coverage)]
+def target_unroll_min_by_fold() : int {
+ return _fold(from_decs_template(type)._select(_.val)._min_by(-_))
+}
+
+[export, marker(no_coverage)]
+def target_unroll_max_by_fold() : int {
+ return _fold(from_decs_template(type)._select(_.val)._max_by(-_))
+}
+
+[test]
+def test_unroll_chained_select_sum_parity(t : T?) {
+ fixture_unroll2(5)
+ // (0+1+2+3+4)*10 = 100
+ t |> equal(target_unroll_chained_select_sum_fold(), 100, "chained select+sum splice parity")
+}
+
+[test]
+def test_unroll_select_then_where_sum_parity(t : T?) {
+ fixture_unroll2(5)
+ // select val → 0,1,2,3,4 → where >1 → 2,3,4 → sum 9
+ t |> equal(target_unroll_select_then_where_sum_fold(), 9, "select→where→sum splice parity")
+}
+
+[test]
+def test_unroll_where_select_where_sum_parity(t : T?) {
+ fixture_unroll2(6)
+ // flag==1 rows: i=1,3,5 (vals 1,3,5) → select val → 1,3,5 → where >1 → 3,5 → sum 8
+ t |> equal(target_unroll_where_select_where_sum_parity_compute(), 8, "where→select→where→sum splice parity")
+}
+
+def private target_unroll_where_select_where_sum_parity_compute() : int {
+ return target_unroll_where_select_where_sum_fold()
+}
+
+[test]
+def test_unroll_count_pred_parity(t : T?) {
+ fixture_unroll2(6)
+ // vals 0..5, pred val>2 → 3,4,5 → 3 hits
+ t |> equal(target_unroll_count_pred_fold(), 3, "_count(pred) splice parity")
+}
+
+[test]
+def test_unroll_long_count_pred_parity(t : T?) {
+ fixture_unroll2(6)
+ // vals 0..5, pred val>=3 → 3,4,5 → 3 hits
+ t |> equal(target_unroll_long_count_pred_fold(), 3l, "_long_count(pred) splice parity")
+}
+
+[test]
+def test_unroll_chained_select_to_array_parity(t : T?) {
+ fixture_unroll2(3)
+ // select val → 0,1,2 → select +100 → 100,101,102
+ let got <- target_unroll_chained_select_to_array_fold()
+ t |> equal(got |> length, 3, "chained to_array length")
+ if (length(got) == 3) {
+ t |> equal(got[0], 100, "[0]")
+ t |> equal(got[1], 101, "[1]")
+ t |> equal(got[2], 102, "[2]")
+ }
+}
+
+[test]
+def test_unroll_min_by_parity(t : T?) {
+ fixture_unroll2(5)
+ // select val → 0,1,2,3,4; min by -val → max val → 4
+ t |> equal(target_unroll_min_by_fold(), 4, "_min_by splice parity")
+}
+
+[test]
+def test_unroll_max_by_parity(t : T?) {
+ fixture_unroll2(5)
+ // select val → 0,1,2,3,4; max by -val → min val → 0
+ t |> equal(target_unroll_max_by_fold(), 0, "_max_by splice parity")
+}
+
+[test]
+def test_unroll_chained_select_splice_shape(t : T?) {
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_unroll_chained_select_sum_fold)
+ t |> success(func != null, "RTTI must resolve target_unroll_chained_select_sum_fold")
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return <- $e(body_expr)
+ }
+ t |> success(r.matched && body_expr is ExprInvoke, "expected splice invoke wrapper")
+ t |> equal(describe_count(body_expr, "to_sequence"), 0, "chained-select sum splice must NOT call to_sequence")
+ t |> equal(describe_count(body_expr, "for_each_archetype"), 1, "exactly one for_each_archetype")
+ }
+}
+
+[test]
+def test_unroll_count_pred_splice_shape(t : T?) {
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_unroll_count_pred_fold)
+ t |> success(func != null, "RTTI must resolve target_unroll_count_pred_fold")
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return <- $e(body_expr)
+ }
+ t |> success(r.matched && body_expr is ExprInvoke, "expected splice invoke wrapper")
+ t |> equal(describe_count(body_expr, "to_sequence"), 0, "_count(pred) splice must NOT call to_sequence")
+ t |> equal(describe_count(body_expr, "for_each_archetype"), 1, "exactly one for_each_archetype")
+ }
+}
+
+[test]
+def test_unroll_min_by_splice_shape(t : T?) {
+ ast_gc_guard() {
+ var func = find_module_function_via_rtti(compiling_module(), @@target_unroll_min_by_fold)
+ t |> success(func != null, "RTTI must resolve target_unroll_min_by_fold")
+ if (func == null) return
+ var body_expr : ExpressionPtr
+ let r = qmatch_function(func) $() {
+ return <- $e(body_expr)
+ }
+ t |> success(r.matched && body_expr is ExprInvoke, "expected splice invoke wrapper")
+ t |> equal(describe_count(body_expr, "to_sequence"), 0, "_min_by splice must NOT call to_sequence")
+ t |> equal(describe_count(body_expr, "for_each_archetype"), 1, "exactly one for_each_archetype")
+ }
+}
diff --git a/tests/long_array_table/test_fusion_arr_i64.das b/tests/long_array_table/test_fusion_arr_i64.das
new file mode 100644
index 0000000000..69b4aa73b3
--- /dev/null
+++ b/tests/long_array_table/test_fusion_arr_i64.das
@@ -0,0 +1,98 @@
+options gen2
+require dastest/testing_boost public
+
+// Slice A coverage: fused path for arr[i64] / arr[u64] across the
+// compute-mode cross-product (Local_Const / Local_Local / Argument).
+// Correctness only; fusion-vs-unfused parity is a benchmark concern.
+
+[test]
+def test_arr_i64_const_idx(t : T?) {
+ var arr : array
+ arr |> resize(10)
+ for (i in iter_range(arr)) {
+ arr[i] = i * 7
+ }
+ // i64 const literal indices — _I64_Local_Const fusion variant
+ t |> equal(arr[0_l], 0)
+ t |> equal(arr[3_l], 21)
+ t |> equal(arr[9_l], 63)
+}
+
+[test]
+def test_arr_u64_const_idx(t : T?) {
+ var arr : array
+ arr |> resize(10)
+ for (i in iter_range(arr)) {
+ arr[i] = i * 11
+ }
+ // u64 const literal indices — _U64_Local_Const fusion variant
+ t |> equal(arr[0_ul], 0)
+ t |> equal(arr[4_ul], 44)
+ t |> equal(arr[9_ul], 99)
+}
+
+[test]
+def test_arr_i64_var_write_read(t : T?) {
+ var arr : array
+ arr |> resize(5)
+ let i : int64 = 2_l
+ let j : int64 = 4_l
+ arr[i] = 100
+ arr[j] = 200
+ t |> equal(arr[i], 100)
+ t |> equal(arr[j], 200)
+}
+
+[test]
+def test_arr_i64_via_argument(t : T?) {
+ var arr : array
+ arr |> resize(8)
+ for (i in iter_range(arr)) {
+ arr[i] = i + 1000
+ }
+ t |> equal(read_at(arr, 0_l), 1000)
+ t |> equal(read_at(arr, 5_l), 1005)
+ t |> equal(read_at(arr, 7_l), 1007)
+}
+
+def read_at(arr : array; idx : int64) : int {
+ return arr[idx]
+}
+
+[test]
+def test_arr_i64_float_value_type(t : T?) {
+ var arr : array
+ arr |> resize(4)
+ arr[0_l] = 1.5f
+ arr[1_l] = 2.5f
+ arr[2_l] = 3.5f
+ arr[3_l] = 4.5f
+ t |> equal(arr[0_l], 1.5f)
+ t |> equal(arr[3_l], 4.5f)
+}
+
+[test]
+def test_arr_i64_lvalue_write_via_fusion(t : T?) {
+ // Exercises ArrayAt_I64 (DAS_PTR_NODE) — writes through the fused ptr return.
+ var arr : array
+ arr |> resize(4)
+ arr[0_l] = 10
+ arr[1_l] = 20
+ arr[2_l] = 30
+ arr[3_l] = 40
+ var total = 0
+ for (v in arr) {
+ total += v
+ }
+ t |> equal(total, 100)
+}
+
+[test]
+def test_arr_u64_lvalue_write_via_fusion(t : T?) {
+ var arr : array
+ arr |> resize(3)
+ arr[0_ul] = 1.0f
+ arr[1_ul] = 2.0f
+ arr[2_ul] = 3.0f
+ t |> equal(arr[0_ul] + arr[1_ul] + arr[2_ul], 6.0f)
+}
diff --git a/tests/long_array_table/test_fusion_table_i64.das b/tests/long_array_table/test_fusion_table_i64.das
new file mode 100644
index 0000000000..a317ab807c
--- /dev/null
+++ b/tests/long_array_table/test_fusion_table_i64.das
@@ -0,0 +1,64 @@
+options gen2
+require dastest/testing_boost public
+
+// Slice B audit: int64/uint64-keyed table indexing already has fusion variants
+// registered via IMPLEMENT_SETOP_NUMERIC(TableIndex) at
+// src/simulate/simulate_fusion_tableindex.cpp:81. These tests verify the
+// fused path produces correct values for int64/uint64 keys.
+
+[test]
+def test_table_i64_key_index_write_read(t : T?) {
+ var tab : table
+ tab[1_l] = 10
+ tab[1000000000000_l] = 20
+ tab[-1_l] = 30
+ t |> equal(tab[1_l], 10)
+ t |> equal(tab[1000000000000_l], 20)
+ t |> equal(tab[-1_l], 30)
+}
+
+[test]
+def test_table_i64_key_overwrite(t : T?) {
+ var tab : table
+ tab[42_l] = "first"
+ tab[42_l] = "second"
+ t |> equal(length(tab), 1)
+ t |> equal(tab[42_l], "second")
+}
+
+[test]
+def test_table_u64_key_index(t : T?) {
+ var tab : table
+ tab[1_ul] = 100
+ tab[18446744073709551615_ul] = 200
+ t |> equal(tab[1_ul], 100)
+ t |> equal(tab[18446744073709551615_ul], 200)
+}
+
+[test]
+def test_table_i64_key_via_local(t : T?) {
+ var tab : table
+ let k1 : int64 = 5_l
+ let k2 : int64 = 100_l
+ tab[k1] = 50
+ tab[k2] = 1000
+ t |> equal(tab[k1], 50)
+ t |> equal(tab[k2], 1000)
+}
+
+[test]
+def test_table_i64_key_via_arg(t : T?) {
+ var tab : table
+ fill_table(tab, 7_l, 77)
+ fill_table(tab, 13_l, 133)
+ t |> equal(read_table(tab, 7_l), 77)
+ t |> equal(read_table(tab, 13_l), 133)
+}
+
+def fill_table(var tab : table; k : int64; v : int) {
+ tab[k] = v
+}
+
+def read_table(var tab : table; k : int64) : int {
+ return tab[k]
+}
diff --git a/tests/long_array_table/test_huge_array_index_offset.das b/tests/long_array_table/test_huge_array_index_offset.das
new file mode 100644
index 0000000000..e98178d59f
--- /dev/null
+++ b/tests/long_array_table/test_huge_array_index_offset.das
@@ -0,0 +1,56 @@
+options gen2
+options persistent_heap = true // ~4.4 GB array needs PersistentHeapAllocator
+
+require dastest/testing_boost public
+require daslib/fio
+
+// Memory-gated probe targeting the SimNode_ArrayAt offset-math widening.
+// `array` with stride=4 and ~1.1G elements puts `idx * stride` above
+// UINT32_MAX, so the address computation MUST be `uint64_t(idx) *
+// uint64_t(stride) + offset` (per Phase 2/runtime_array.h). A uint32
+// regression would wrap and read the wrong cache line — usually a segfault,
+// occasionally a silently-wrong value.
+//
+// ~4.3 GB allocation. Skipped silently unless DASLANG_HUGE_HEAP_TESTS=1 on
+// a 64-bit build.
+
+def huge_enabled() : bool {
+ static_if (typeinfo sizeof(type) < 8) {
+ return false
+ }
+ return has_env_variable("DASLANG_HUGE_HEAP_TESTS") && get_env_variable("DASLANG_HUGE_HEAP_TESTS") == "1"
+}
+
+// (UINT32_MAX / 4) + ~32M slack = 1.1G elements; idx*stride > 4 GB at the tail.
+let STRIDE_OVERFLOW_N = 1_100_000_000_l
+
+[test]
+def test_int_array_high_index_int64(t : T?) {
+ if (!huge_enabled()) return
+ var arr : array
+ arr |> resize(STRIDE_OVERFLOW_N)
+ t |> equal(long_length(arr), STRIDE_OVERFLOW_N)
+ // Write to a position where idx*4 > UINT32_MAX, then read it back.
+ // If the address math is uint32, we either segfault or read garbage.
+ let high = STRIDE_OVERFLOW_N - 1_l
+ arr[high] = int(0x12345678)
+ arr[0_l] = int(0x0BADC0DE)
+ t |> equal(arr[high], int(0x12345678))
+ t |> equal(arr[0_l], int(0x0BADC0DE))
+ delete arr
+}
+
+[test]
+def test_int_array_high_index_uint64(t : T?) {
+ if (!huge_enabled()) return
+ var arr : array
+ arr |> resize(STRIDE_OVERFLOW_N)
+ // Same test, uint64 index instead of int64 — exercises the U64 SimNode
+ // variant's offset math.
+ let high : uint64 = uint64(STRIDE_OVERFLOW_N) - 1_ul
+ arr[high] = int(0x12345678)
+ arr[0_ul] = int(0x0BADC0DE)
+ t |> equal(arr[high], int(0x12345678))
+ t |> equal(arr[0_ul], int(0x0BADC0DE))
+ delete arr
+}
diff --git a/tests/long_array_table/test_huge_array_iterate.das b/tests/long_array_table/test_huge_array_iterate.das
new file mode 100644
index 0000000000..812b5315ea
--- /dev/null
+++ b/tests/long_array_table/test_huge_array_iterate.das
@@ -0,0 +1,84 @@
+options gen2
+options persistent_heap = true // >2 GB arrays need PersistentHeapAllocator
+
+require dastest/testing_boost public
+require daslib/fio
+require daslib/functional
+
+// Memory-gated probe: iterate a > INT_MAX-element array four different ways
+// and assert each visits exactly long_length(arr) elements. If any iteration
+// shape uses an int counter internally, the count check fails (or wraps).
+//
+// Auto-discovered by dastest; inline `huge_enabled()` gate silent-returns when
+// the env var is unset, so CI sees a no-op pass. Manual run actually allocates.
+
+def huge_enabled() : bool {
+ static_if (typeinfo sizeof(type) < 8) {
+ return false
+ }
+ return has_env_variable("DASLANG_HUGE_HEAP_TESTS") && get_env_variable("DASLANG_HUGE_HEAP_TESTS") == "1"
+}
+
+// 2.2 GB elements of uint8 — exceeds INT_MAX (2.147 GB), stays under
+// UINT32_MAX (4 GB) for faster runs.
+let HUGE_N = 2200_l * 1024_l * 1024_l
+
+[test]
+def test_iterate_via_range64_long_length(t : T?) {
+ if (!huge_enabled()) return
+ var arr : array
+ arr |> resize(HUGE_N)
+ var count = 0_l
+ for (_i in range64(0_l, long_length(arr))) {
+ count++
+ }
+ t |> equal(count, HUGE_N)
+ delete arr
+}
+
+[test]
+def test_iterate_via_long_iter_range(t : T?) {
+ if (!huge_enabled()) return
+ var arr : array
+ arr |> resize(HUGE_N)
+ var count = 0_l
+ for (_i in long_iter_range(arr)) {
+ count++
+ }
+ t |> equal(count, HUGE_N)
+ delete arr
+}
+
+[test]
+def test_iterate_via_index_read(t : T?) {
+ if (!huge_enabled()) return
+ var arr : array
+ arr |> resize(HUGE_N)
+ // Write a sentinel at the last index, iterate with int64 counter and
+ // assert we read it back via arr[i] indexing (stride math).
+ arr[HUGE_N - 1_l] = uint8(0x77)
+ var sum = 0_l
+ for (i in range64(0_l, long_length(arr))) {
+ sum += int64(arr[i])
+ }
+ t |> equal(sum, 119_l) // only one non-zero byte at the very end (0x77)
+ delete arr
+}
+
+[test]
+def test_iterate_via_long_enumerate(t : T?) {
+ if (!huge_enabled()) return
+ var arr : array
+ arr |> resize(HUGE_N)
+ var last_index = -1_l
+ var count = 0_l
+ unsafe {
+ for ((i, _v) in long_enumerate(each(arr))) {
+ last_index = i
+ count++
+ }
+ }
+ t |> equal(count, HUGE_N)
+ t |> equal(last_index, HUGE_N - 1_l)
+ delete arr
+}
diff --git a/tests/long_array_table/test_huge_array_push_emplace_clone.das b/tests/long_array_table/test_huge_array_push_emplace_clone.das
new file mode 100644
index 0000000000..5d694325cc
--- /dev/null
+++ b/tests/long_array_table/test_huge_array_push_emplace_clone.das
@@ -0,0 +1,65 @@
+options gen2
+options persistent_heap = true // >2 GB arrays need PersistentHeapAllocator
+
+require dastest/testing_boost public
+require daslib/fio
+
+// Memory-gated probe: resize an array past INT_MAX, then push/emplace/
+// push_clone one element and assert the new tail reads back correctly.
+//
+// The plan flagged `tests/long_array_table/test_push_returns_int64.das` as
+// owed once Phase 4b lands `arr[i64]` (now landed). The daslib `push` wrapper
+// doesn't surface the underlying `__builtin_array_push_back` int64 position,
+// so we verify indirectly: long_length grows by 1, and arr[before] reads
+// back the pushed value via int64 indexing (which only works if the position
+// the wrapper used internally was the correct int64).
+//
+// Skipped silently unless DASLANG_HUGE_HEAP_TESTS=1 on a 64-bit build.
+
+def huge_enabled() : bool {
+ static_if (typeinfo sizeof(type) < 8) {
+ return false
+ }
+ return has_env_variable("DASLANG_HUGE_HEAP_TESTS") && get_env_variable("DASLANG_HUGE_HEAP_TESTS") == "1"
+}
+
+// 2.2 GB elements of uint8 — exceeds INT_MAX (2.147 GB).
+let HUGE_N = 2200_l * 1024_l * 1024_l
+
+[test]
+def test_push_past_int_max(t : T?) {
+ if (!huge_enabled()) return
+ var arr : array
+ arr |> resize(HUGE_N)
+ let before = long_length(arr)
+ arr |> push(uint8(0xAB))
+ t |> equal(long_length(arr), before + 1_l)
+ t |> equal(arr[before], uint8(0xAB))
+ delete arr
+}
+
+[test]
+def test_emplace_past_int_max(t : T?) {
+ if (!huge_enabled()) return
+ var arr : array
+ arr |> resize(HUGE_N)
+ let before = long_length(arr)
+ var v : uint8 = uint8(0xCD)
+ arr |> emplace(v)
+ t |> equal(long_length(arr), before + 1_l)
+ t |> equal(arr[before], uint8(0xCD))
+ delete arr
+}
+
+[test]
+def test_push_clone_past_int_max(t : T?) {
+ if (!huge_enabled()) return
+ var arr : array
+ arr |> resize(HUGE_N)
+ let before = long_length(arr)
+ let v : uint8 = uint8(0xEF)
+ arr |> push_clone(v)
+ t |> equal(long_length(arr), before + 1_l)
+ t |> equal(arr[before], uint8(0xEF))
+ delete arr
+}
diff --git a/tests/long_array_table/test_huge_array_resize_index.das b/tests/long_array_table/test_huge_array_resize_index.das
new file mode 100644
index 0000000000..4746cc6460
--- /dev/null
+++ b/tests/long_array_table/test_huge_array_resize_index.das
@@ -0,0 +1,56 @@
+options gen2
+options persistent_heap = true // >4 GB arrays need PersistentHeapAllocator
+
+require dastest/testing_boost public
+require daslib/fio
+
+// Memory-gated probe. Allocates a 5 GB `array` and exercises
+// int64 indexing at index 0 and index N-1 (a high position that requires
+// `uint64_t(idx)*uint64_t(stride) + offset` for the address math).
+//
+// Skipped silently unless:
+// * running a 64-bit daslang (static_if on pointer size)
+// * DASLANG_HUGE_HEAP_TESTS=1 in the environment
+//
+// dastest auto-discovers this file (filename starts with `test_`), but the
+// inline `huge_enabled()` gate silent-returns when the env var is unset, so
+// CI sees a no-op pass. Run manually with the env var to actually allocate:
+// $env:DASLANG_HUGE_HEAP_TESTS = "1"
+// bin/Release/daslang.exe dastest/dastest.das -- --test tests/long_array_table/test_huge_array_resize_index.das
+
+def huge_enabled() : bool {
+ static_if (typeinfo sizeof(type) < 8) {
+ return false
+ }
+ return has_env_variable("DASLANG_HUGE_HEAP_TESTS") && get_env_variable("DASLANG_HUGE_HEAP_TESTS") == "1"
+}
+
+[test]
+def test_huge_array_resize_round_trip(t : T?) {
+ if (!huge_enabled()) return
+ let N = 5_l * 1024_l * 1024_l * 1024_l // 5 GB elements of uint8
+ var arr : array
+ arr |> resize(N)
+ t |> equal(long_length(arr), N)
+ arr[0_l] = uint8(0xC0)
+ arr[N - 1_l] = uint8(0xDE)
+ t |> equal(arr[0_l], uint8(0xC0))
+ t |> equal(arr[N - 1_l], uint8(0xDE))
+ delete arr
+}
+
+[test]
+def test_huge_array_length_panics_long_length_ok(t : T?) {
+ if (!huge_enabled()) return
+ // 2.2 GB > INT_MAX (2.147 GB). long_length is safe; length() should panic
+ // per the surface contract added in PR #2746.
+ let N = 2200_l * 1024_l * 1024_l
+ var arr : array
+ arr |> resize(N)
+ t |> equal(long_length(arr), N)
+ // length(arr) here should panic — but try/recover is banned for panic UX testing
+ // (feedback_no_try_recover_for_soft_fail), so we don't assert the panic here.
+ // Reproduce manually by uncommenting:
+ // let _l = length(arr)
+ delete arr
+}
diff --git a/tests/long_array_table/test_int_int64_disjunction.das b/tests/long_array_table/test_int_int64_disjunction.das
new file mode 100644
index 0000000000..ba8c6e8d0d
--- /dev/null
+++ b/tests/long_array_table/test_int_int64_disjunction.das
@@ -0,0 +1,44 @@
+options gen2
+
+require dastest/testing_boost public
+
+// Phase 8b spike: can a function accept `int | int64` as a single parameter
+// signature and fork inside the body via `static_if`? If yes, PR-D (linq
+// surface widening) can use one signature per function instead of doubling
+// overloads. If no, PR-D falls back to separate `: int` / `: int64` pairs.
+//
+// Reference for the disjunction-parameter shape: tests/language/option_type.das.
+// `typeinfo is_int` / `typeinfo is_int64` are added in the same PR — string-
+// compare `stripped_typename(x) == "int"` was the fallback while landing this.
+
+def take_or(x : int | int64) : int64 {
+ static_if (typeinfo is_int(x)) {
+ return int64(x) + 1_l
+ } else {
+ return x + 1_l
+ }
+}
+
+[test]
+def test_int_branch(t : T?) {
+ let r = take_or(40)
+ t |> equal(r, 41_l)
+}
+
+[test]
+def test_int64_branch(t : T?) {
+ let r = take_or(40_l)
+ t |> equal(r, 41_l)
+}
+
+// Type-trait contract: locks `is_int` / `is_int64` against silent reverts.
+[test]
+def test_typeinfo_is_int_traits(t : T?) {
+ static_assert(typeinfo is_int(type), "is_int(int) must be true")
+ static_assert(!typeinfo is_int(type), "is_int(int64) must be false")
+ static_assert(!typeinfo is_int(type