linq_fold: plan_decs_eager_bridge — count + to_array splice past from_decs(_template)#2748
linq_fold: plan_decs_eager_bridge — count + to_array splice past from_decs(_template)#2748borisbat wants to merge 2 commits into
Conversation
…_decs(_template)
First slice of Phase 4 in the LINQ-to-DECS plan. Recognizes the
post-expansion eager-bridge shape that both FromDecsMacro and
FromDecsTemplateMacro produce (after query() macro expansion):
invoke($() : iterator<tuple<...>> {
var res : array<tuple<...>>
for_each_archetype(req_hash, erq_factory, $(arch) {
for (a, b in get_ro(arch, "name_a", type<A>),
get_ro(arch, "name_b", type<B>)) {
res |> push(tuple(a, b))
}
})
return res.to_sequence()
})
When this shape is wrapped by `_fold(... .count())` or `_fold(... .to_array())`,
the new plan_decs_eager_bridge planner recognizes it via pure pattern match
(no marker annotation needed; the eager-bridge shape IS the marker) and
emits a terminator-specific shell that bypasses the array<tuple>
materialization.
For count: uses the arch.size shortcut — no inner for-loop, just sums
per-archetype entity counts. Direct measurable win over the eager bridge.
For to_array: same shape as eager bridge but returns the buffer directly
(skips the to_sequence() iterator roundtrip).
The splice fires on BOTH from_decs(...) AND from_decs_template(type<Foo>)
since they emit identical post-expansion shapes. Bonus: any user code
matching the same shape (the for_each_archetype materialization pattern)
also benefits.
Bails to tier-2 (eager bridge runs unchanged) on any shape mismatch or
unsupported chain (where_/select/take/skip/etc.) — those are sub-PR 4c
scope. Likewise, accumulator (sum/min/max/etc.) and early-exit
(first/any/take) terminators are sub-PR 4b scope.
Cascade slot: between plan_group_by and plan_zip (the bridge shape is
more specific than zip's generic source recognition).
Tests:
- 4 functional parity: count/to_array splice over both template + block forms,
plus empty-archetype.
- 3 AST-shape gates via describe_count: verify count uses arch.size shortcut
(no inner for, no push, no to_sequence), to_array skips to_sequence (one
inner for), and unsupported chains (_select before count) cascade to
tier-2 (to_sequence call still present).
Verification:
- lint clean, format clean
- interpret tests/linq: 1145/1145 green
- AOT regen + rebuild + sweep tests/linq: 1145/1145 green
- decs regression tests/decs: 239/239 green
Design notes + spike findings + Phase 4 sub-PR breakdown captured in
benchmarks/sql/LINQ_TO_DECS.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a new LINQ _fold planner optimization that recognizes the expanded “DECS eager-bridge” shape emitted by from_decs(...) / from_decs_template(type<...>) and splices through it for count and to_array, avoiding intermediate array<tuple> materialization and iterator round-trips.
Changes:
- Add
plan_decs_eager_bridgetodaslib/linq_fold.das, including eager-bridge extraction and specialized emitters forcountandto_array. - Add functional parity tests plus AST-shape “splice fired” gates for the new planner behavior.
- Update LINQ→DECS design notes in
benchmarks/sql/LINQ_TO_DECS.md.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
daslib/linq_fold.das |
Introduces the eager-bridge recognizer and emits optimized splices for count/to_array, wired into the _fold planner cascade. |
tests/linq/test_linq_from_decs.das |
Adds new splice parity tests and AST-shape gates intended to ensure the optimization triggers (and cascades when unsupported). |
benchmarks/sql/LINQ_TO_DECS.md |
Updates the Phase 4 design writeup/status, but currently describes a marker-based approach that conflicts with this PR’s implementation. |
Comments suppressed due to low confidence (2)
tests/linq/test_linq_from_decs.das:252
- The AST gate test silently returns when
find_module_function_via_rttifails, which can make the test pass without asserting anything (e.g., when RTTI is disabled). Please assertfunc != null(e.g.,t |> success(func != null, ...)) before early-returning so this test can’t become a no-op.
var func = find_module_function_via_rtti(compiling_module(), @@target_splice_to_array_template_fold)
if (func == null) return
var body_expr : ExpressionPtr
let r = qmatch_function(func) $() {
return <- $e(body_expr)
}
tests/linq/test_linq_from_decs.das:269
- The AST gate test silently returns when
find_module_function_via_rttifails, which can make the test pass without asserting anything (e.g., when RTTI is disabled). Please assertfunc != null(e.g.,t |> success(func != null, ...)) before early-returning so this test can’t become a no-op.
var func = find_module_function_via_rtti(compiling_module(), @@target_splice_cascade_select_count_fold)
if (func == null) return
var body_expr : ExpressionPtr
let r = qmatch_function(func) $() {
return <- $e(body_expr)
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- extract_eager_bridge now captures the bridge's `res` variable name and verifies the to_sequence target references the same variable — closes false-positive surface on user-written invoke blocks that happen to end in `something_else.to_sequence()`. - emit_decs_to_array_splice consumes the captured name via DecsBridgeInfo instead of hard-coding "res" — survives a future rename of the eager bridge's gensym. - test_linq_from_decs.das: add `options rtti` so AST gates don't silently no-op when RTTI is disabled at the program level; assert `func != null` at the 3 RTTI lookup sites so a future symbol rename can't turn the gates into passing no-ops. - LINQ_TO_DECS.md: rewrite the EcsRequest-construction options block and the sub-PR 4a description to reflect the implemented Option C (pure pattern match on the post-expansion eager bridge); demote the marker plan to a considered-then-rejected alternative. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
benchmarks/sql/LINQ_TO_DECS.md:14
- In the status table, piece 4 is described as “plan_from_decs_template splice + Stage B marker”, but later in this doc Phase 4a is explicitly “Option C (chosen)… pure pattern-match on the post-expansion eager-bridge AST (no marker)”. Please update this row to match the current Phase 4 approach (or clearly label the marker plan as a superseded/alternative path).
| 3. `from_decs_template(type<Foo>)` macro — eager bridge (Stage A) | 🟡 in flight | #2745 |
| 4. `plan_from_decs_template` splice in `linq_fold` + Stage B marker (the real payoff) | ⬜ next | — |
| 5. sqlite_linq N-ary zip recognition | ⬜ deferred (no concrete use case) | — |
benchmarks/sql/LINQ_TO_DECS.md:467
- The “Updated Phase 4 plan (post-validation)” section proposes adding a
from_decs_template_metaannotation and aplan_from_decs_templateplanner, which contradicts the earlier “Decision: Option C (chosen, implemented in PR #2748): Pure pattern-match…” and the implementation in this PR (no annotation, no decs_boost changes). This section should be removed, reworded as a rejected/superseded spike note, or updated to reflect the Option C implementation so the design doc remains internally consistent.
## Updated Phase 4 plan (post-validation)
The work is now:
1. **Add `[block_macro(name="from_decs_template_meta")]` to `daslib/decs_boost.das`.** Class extends `AstBlockAnnotation`. `apply` is a no-op (return `true`). Just a tag.
2. **Modify FromDecsTemplateMacro** to attach the tag annotation to the lambda body block as part of the eager-bridge emission. Annotation arg: `prefix = "<resolved prefix>"`. No other behavior change.
3. **Add `plan_from_decs_template` planner** in `daslib/linq_fold.das`. Slot between `plan_group_by` and `plan_zip`. Walks `flatten_linq(expr)` for ExprInvoke whose body block has `from_decs_template_meta` annotation. When found:
| // AST-gate helpers — describe-substring counting. Stable for shape assertions: | ||
| // the splice's emission is deterministic and we just need to spot known call | ||
| // patterns (e.g., `to_sequence`, `for_each_archetype`, `for (`). | ||
| def private describe_count(expr : Expression?; needle : string) : int { | ||
| if (expr == null) return 0 | ||
| let s = describe(expr) | ||
| var n = 0 | ||
| var i = 0 | ||
| let nl = length(needle) | ||
| while (true) { | ||
| let p = s |> find(needle, i) | ||
| if (p < 0) break | ||
| n ++ | ||
| i = p + nl | ||
| } | ||
| return n | ||
| } | ||
|
|
|
|
||
| Status: **discussion draft** — no PR open, no plan committed. Edited in-place | ||
| as the design firms up. | ||
| **Status (2026-05-20):** Pieces 1-3 landed. Piece 4 is the active design surface. |
|
Superseded by #2750. The new PR uses a cleaner architecture (named-tuple bind + fold_linq_cond peel) that covers the same case plus chain-aware 0 0///. The terminator from this PR is deferred to a Slice 3 follow-up on the same architecture. |
|
Replacement for the earlier mangled close-comment (shell ate backticks): Superseded by #2750. The new PR uses a cleaner architecture — named-tuple bind + |
Summary
Phase 4 sub-PR 4a of the LINQ-to-DECS plan. New
plan_decs_eager_bridgeplanner that recognizes the post-expansion eager-bridge shape (produced by bothfrom_decs(...)andfrom_decs_template(type<Foo>)) and splices past it forcountandto_arrayterminators — bypassingarray<tuple>materialization.How it works
_foldsees the FULLY-EXPANDED AST — bothfrom_decs_templateANDqueryhave lowered before the planner runs. What_foldreceives:The planner pattern-matches this shape (no marker annotation needed — the shape itself is deterministic and unique to FromDecs(Template)Macro). When matched + a supported terminator is found:
count splice (with
arch.sizeshortcut, since no chain ops):to_array splice (skip the to_sequence iterator roundtrip):
(
req_hashanderq_factoryare the sameExprConstUInt64+ExprAddrfrom the original bridge, reused viaclone_expression— no rebuild ofEcsRequestneeded.)Architecture notes
decs_boostchanges. The shape is the marker; pure pattern match in the planner.linq_foldstays decs-unaware at the dependency level.from_decs($(...){})ANDfrom_decs_template(type<Foo>)— same post-expansion shape.plan_group_byandplan_zip(bridge shape is more specific than zip's generic source).Out of scope for 4a (lands in follow-ups)
sum/min/max/average/long_count) + early-exit (first/first_or_default/any/all/contains/take) terminatorswhere_/select/take/skip/take_while/skip_whilebetween source and terminator)distinct/group_by/reverse/order_by) — hoist state above outerFiles
plan_decs_eager_bridgeplanner +extract_eager_bridge+emit_decs_count_splice+emit_decs_to_array_splice+get_call_short_namehelperfor_each_archetype_findblock-return), Phase 4 sub-PR breakdownTest plan
mcp__daslang__lint— clean on touched filesmcp__daslang__format_file— cleanmcp__daslang__run_test tests/linq/test_linq_from_decs.das— 14/14 pass (7 existing + 7 new splice tests)tests/linq— 1145/1145 greentests/linq— 1145/1145 greentests/decs— 239/239 green🤖 Generated with Claude Code