Skip to content

linq_fold: plan_decs_eager_bridge — count + to_array splice past from_decs(_template)#2748

Closed
borisbat wants to merge 2 commits into
masterfrom
bbatkin/linq-fold-plan-decs-eager-bridge
Closed

linq_fold: plan_decs_eager_bridge — count + to_array splice past from_decs(_template)#2748
borisbat wants to merge 2 commits into
masterfrom
bbatkin/linq-fold-plan-decs-eager-bridge

Conversation

@borisbat
Copy link
Copy Markdown
Collaborator

@borisbat borisbat commented May 20, 2026

Summary

Phase 4 sub-PR 4a of the LINQ-to-DECS plan. New plan_decs_eager_bridge planner that recognizes the post-expansion eager-bridge shape (produced by both from_decs(...) and from_decs_template(type<Foo>)) and splices past it for count and to_array terminators — bypassing array<tuple> materialization.

[decs_template(prefix="particle_")]
struct Particle { val : int }

// Before: eager bridge allocates array<tuple<val:int>>, walks every entity,
// returns to_sequence, then count walks the iterator.
// After: arch.size shortcut — sums per-archetype entity counts, no
// allocation, no inner for-loop.
let n = _fold(from_decs_template(type<Particle>).count())

How it works

_fold sees the FULLY-EXPANDED AST — both from_decs_template AND query have lowered before the planner runs. What _fold receives:

invoke($() : iterator<tuple<val:int>> {
    var res : array<tuple<val:int>>
    for_each_archetype(req_hash, erq_factory, $(arch) {
        for (val in get_ro(arch, "particle_val", type<int>)) {
            res |> push(tuple(val))
        }
    })
    return res.to_sequence()
})

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.size shortcut, since no chain ops):

invoke($() : int {
    var acc = 0
    for_each_archetype(req_hash, erq_factory, $(arch) {
        acc += arch.size
    })
    return acc
})

to_array splice (skip the to_sequence iterator roundtrip):

invoke($() : array<tuple<val:int>> {
    var buf : array<tuple<val:int>>
    for_each_archetype(req_hash, erq_factory, $(arch) {
        for (val in get_ro(arch, "particle_val", type<int>)) {
            buf |> push(tuple(val))
        }
    })
    return <- buf
})

(req_hash and erq_factory are the same ExprConstUInt64 + ExprAddr from the original bridge, reused via clone_expression — no rebuild of EcsRequest needed.)

Architecture notes

  • No decs_boost changes. The shape is the marker; pure pattern match in the planner. linq_fold stays decs-unaware at the dependency level.
  • Splice fires on BOTH from_decs($(...){}) AND from_decs_template(type<Foo>) — same post-expansion shape.
  • Cascade slot: between plan_group_by and plan_zip (bridge shape is more specific than zip's generic source).
  • Safe degradation everywhere. Any shape mismatch returns null; tier-2 (eager bridge) runs unchanged. No way to break existing behavior.

Out of scope for 4a (lands in follow-ups)

  • 4b: accumulator (sum / min / max / average / long_count) + early-exit (first / first_or_default / any / all / contains / take) terminators
  • 4c: chain-op fusion (where_ / select / take / skip / take_while / skip_while between source and terminator)
  • 4d: state-table terminators (distinct / group_by / reverse / order_by) — hoist state above outer

Files

  • daslib/linq_fold.das (+139) — new plan_decs_eager_bridge planner + extract_eager_bridge + emit_decs_count_splice + emit_decs_to_array_splice + get_call_short_name helper
  • tests/linq/test_linq_from_decs.das (+158) — 4 functional parity tests + 3 AST-shape gates
  • benchmarks/sql/LINQ_TO_DECS.md (+204) — design doc updates: Phase 1-3 status, spike findings (macro expansion order, marker round-trip, for_each_archetype_find block-return), Phase 4 sub-PR breakdown

Test plan

  • mcp__daslang__lint — clean on touched files
  • mcp__daslang__format_file — clean
  • mcp__daslang__run_test tests/linq/test_linq_from_decs.das — 14/14 pass (7 existing + 7 new splice tests)
  • Interpret sweep tests/linq — 1145/1145 green
  • AOT regen + rebuild + sweep tests/linq — 1145/1145 green
  • Decs regression tests/decs — 239/239 green
  • CI

🤖 Generated with Claude Code

…_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>
Copilot AI review requested due to automatic review settings May 20, 2026 04:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_bridge to daslib/linq_fold.das, including eager-bridge extraction and specialized emitters for count and to_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_rtti fails, which can make the test pass without asserting anything (e.g., when RTTI is disabled). Please assert func != 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_rtti fails, which can make the test pass without asserting anything (e.g., when RTTI is disabled). Please assert func != 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.

Comment thread daslib/linq_fold.das
Comment thread daslib/linq_fold.das Outdated
Comment thread tests/linq/test_linq_from_decs.das
Comment thread tests/linq/test_linq_from_decs.das
Comment thread benchmarks/sql/LINQ_TO_DECS.md Outdated
- 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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_meta annotation and a plan_from_decs_template planner, 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:

Comment on lines +130 to +147
// 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.
@borisbat
Copy link
Copy Markdown
Collaborator Author

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.

@borisbat borisbat closed this May 20, 2026
@borisbat
Copy link
Copy Markdown
Collaborator Author

Replacement for the earlier mangled close-comment (shell ate backticks):

Superseded by #2750. The new PR uses a cleaner architecture — named-tuple bind + fold_linq_cond peel instead of renameVariable on the cloned for-body — that covers the same count case (now via arch.size shortcut) plus chain-aware sum / long_count / _where / _select. The to_array terminator from this PR is deferred to a Slice 3 follow-up on the same architecture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants