Skip to content

linq_fold: route ExprRef2Value peels through qm_peel_ref2value + adopt H3 in plan_zip (PR 2a)#2796

Merged
borisbat merged 1 commit into
masterfrom
bbatkin/linq-fold-qmatch-adoption
May 21, 2026
Merged

linq_fold: route ExprRef2Value peels through qm_peel_ref2value + adopt H3 in plan_zip (PR 2a)#2796
borisbat merged 1 commit into
masterfrom
bbatkin/linq-fold-qmatch-adoption

Conversation

@borisbat
Copy link
Copy Markdown
Collaborator

Summary

Slice 2a of the linq_fold dedup ladder (PR 2 reshaped after grounding). Two small dedups in daslib/linq_fold.das, no ast_match.das changes:

  • extend_specs_for_missing_having_reducers + rewrite_having_pred — hand-rolled while (expr is ExprRef2Value) { expr = (expr as ExprRef2Value).subexpr } loops replaced with calls to qm_peel_ref2value from daslib/ast_match, the same helper qmatch emits at every generate_match dispatch. Single source of truth for ExprRef2Value peel semantics across the macro layer.
  • plan_zip projection elementType extraction — 11-line manual ExprMakeBlockExprBlockExprReturnsubexpr unwrap collapsed to one call to peel_lambda_single_return (H3, already shared via PR 1a).

Net: -15 LOC (5 ins / 20 del). Lint clean.

Plan deviation: qmatch adoption scope shrank

The plan envisioned qmatch conversion of peel_tuple_field_read internals + HAVING-clause probes + extract_decs_bridge cascades + plan_zip projection. Reality after grounding:

  • peel_tuple_field_read lives in ast_match.das which defines qmatch — circular reference prevents qmatch usage in the same file.
  • HAVING-clause probes already route the tuple-field check through peel_tuple_field_read (H9 from PR 1c), which is shorter than the qmatch equivalent (qmatch(c.args[0], $e(rec)._1).matched && rec is ExprVar && (rec as ExprVar).name == hbName).
  • The outer extend_specs_for_missing_having_reducers recursive descent is generic RTTI dispatch on ExprCall/ExprField/ExprOp1/ExprOp2/ExprOp3 — qmatch is for shape matching, not for "is this any ExprOp2" recursion.
  • extract_decs_bridge is block-shape matching with semantic cross-statement constraints (3 statements with specific types, push target var name matches res declared in stmt 0, recordNames count matches for.sources count) — beyond qmatch's pattern grammar.

The genuine wins from PR 2 reduce to (a) routing the two linq_fold while-peels through qm_peel_ref2value for single source of truth, and (b) plan_zip projection extraction via the existing H3 helper.

Item 5 (ExprRef2Value while→if in qm_peel_ref2value itself) is intentionally deferred. tests/ast_match/test_ref2value_skip.das:test_nested_wrappers asserts triple-wrap peel as a future-proofing guard, and src/ast/ast_block_folding.cpp:44 could synthesize a nested wrapper (it rewraps after moving through ExprCast, and the inner subexpr could itself be an ExprRef2Value). The "never nests" claim in the plan is unverified for the synthetic case — keep the loop, route consumers through it.

Test plan

  • daslib/linq_fold.das lint clean (MCP).
  • Interp lane: tests/ast_match 371/371 · tests/linq 1332/1332 · tests/decs 245/245 · tests/dasSQLITE 782/782.
  • AOT lane: tests/ast_match 371/371 · tests/linq 1332/1332 · tests/decs 245/245 · tests/dasSQLITE 782/782. (test_extract_const_string.das.cpp was missing from baseline → regenerated, committed via cmake test_aot_ast_match rebuild — all other AOT stubs "Content is same", confirming no codegen drift.)
  • JIT lane: tests/ast_match 371/371 (modulo test_capture_cfb.das pre-existing JIT failure on master, verified by stash+revert) · tests/linq 1332/1332 · tests/decs 245/245.
  • Bench smoke (7 representative × 1 rep, 100K rows): count_aggregate m3f 4 ns/op · sum_aggregate m3f 2 · average_aggregate m3f 5-6 · aggregate_match m3f 6 · groupby_count m3f 36 · zip_dot_product m3f 7 · distinct_count m3f 15. All within rounding noise of PR linq_fold: migrate to shared ast_match helpers (PR 1c) #2795 baselines. zip_dot_product specifically exercises the plan_zip projection path — invariant codegen confirmed.

Next: PR 3 (mechanical wins — qname sweep, push_block_list adoption, finalize_emission collapse, inline-collapse of $b(localVec) sites).

🤖 Generated with Claude Code

… H3 in plan_zip (PR 2a)

Slice 2a of the linq_fold dedup ladder (PR 2 reshaped):
- extend_specs_for_missing_having_reducers + rewrite_having_pred: hand-rolled while-peel loops replaced with calls to ast_match's qm_peel_ref2value, the same helper qmatch emits at every generate_match dispatch. Single source of truth for ExprRef2Value peel semantics across the macro layer.
- plan_zip projection elementType extraction: 11-line manual ExprMakeBlock → ExprBlock → ExprReturn → subexpr unwrap collapsed to one call to peel_lambda_single_return (H3, already shared via PR 1a).

The plan's "qmatch adoption" goal (items 1-3 in PR 2) reality-checked into noise after PR 1c — the shared helpers (peel_tuple_field_read, peel_lambda_single_return) are already shorter than the qmatch equivalents for the specific shapes linq_fold matches, and the recursive RTTI descents (extend_specs_for_missing_having_reducers walking ExprCall/Field/Op1/Op2/Op3) aren't a qmatch shape at all. Item 5 (ExprRef2Value while→if in qm_peel_ref2value itself) is deferred: tests/ast_match/test_ref2value_skip.das:test_nested_wrappers asserts triple-wrap peel as a future-proofing guard, and block-folding in ast_block_folding.cpp:44 could synthesize a nested wrapper. Keep the loop, route linq_fold through it.

Net: -15 LOC. Lint clean.

Validation:
- ast_match 371/371, linq 1332/1332, decs 245/245, dasSQLITE 782/782 (interp).
- ast_match 371/371, linq 1332/1332, decs 245/245, dasSQLITE 782/782 (AOT).
- ast_match 371/371, linq 1332/1332, decs 245/245 (JIT). test_capture_cfb.das pre-existing JIT failure on master (unrelated to this PR).
- 7 representative SQL benches (count/sum/average/aggregate_match/groupby_count/zip_dot_product/distinct_count): all m3f within rounding noise of PR #2795 baselines.

Next: PR 3 (mechanical wins — qname sweep, push_block_list adoption, finalize_emission collapse, inline-collapse of $b(localVec) sites).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 21, 2026 18:55
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 continues the linq_fold deduplication work by replacing ad-hoc AST unwrapping logic with shared helpers from daslib/ast_match, reducing duplicated “peel” semantics and keeping shape-handling consistent across the macro layer.

Changes:

  • Replaced local ExprRef2Value peeling loops with qm_peel_ref2value in HAVING-related walkers/rewrites.
  • Simplified plan_zip projection element type extraction by using peel_lambda_single_return instead of manual block/return unwrapping.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread daslib/linq_fold.das
Comment on lines +4683 to +4685
var projRet = peel_lambda_single_return(cll._0.arguments[1])
if (projRet == null || projRet._type == null) return null
var projElemType = clone_type(projRet._type)
@borisbat borisbat merged commit 1106189 into master May 21, 2026
33 checks passed
pull Bot pushed a commit to forksnd/daScript that referenced this pull request May 22, 2026
…inalize_decs_emission extract, qn sweep

- A1: collapse `finalize_emission` into `finalize_emission_stmts`. Single
  caller (`plan_reverse`) now extracts its qmacro_block body via
  `push_block_list` before delegating. -8 LOC.

- A2: extract `finalize_decs_emission(emission, at, wrapToIter)` helper.
  Three callers (`plan_decs_order_family`, `plan_decs_reverse`,
  `plan_decs_distinct`) consolidate the `force_at + force_generated +
  conditional iter wrap` tail into a single call. The two wrap conditions
  (`needIterWrap && returnsArray` for order_family, `needIterWrap &&
  needBuffer` for distinct) merge into a pre-multiplied `wrapToIter`
  boolean at the call site. -9 LOC net.

- qname sweep: 124 of 132 `"`{prefix}`{at.line}`{at.column}"` sites
  collapse to `qn("prefix", at)`. The 8 remaining sites carry an extra
  backtick-segment after `{at.column}` (e.g. `{length(preCondStmts)}`,
  `{spec.slot}`) that doesn't fit `qn`'s signature; left as-is.

- Category A inline-collapse / Category B push_block_list adoption:
  SKIPPED after fresh audit. The plan's targeted sites have shifted to
  conditional-push shapes (after prior slices) that no longer fit clean
  inline-collapse. Defer to a future cleanup if it surfaces again.

Codegen invariant: `qn()` returns the byte-identical string as the inline
form, so all AOT baselines pass without refresh. Bench smoke 7 reps × 7
benches: m3f figures byte-identical to PR GaijinEntertainment#2796 baselines (count 4 / sum 2
/ average 5 / aggregate_match 6 / groupby_count 36 / zip_dot_product 7 /
distinct_count 15 ns/op).

Validation matrix (9 lanes):
- Interp: linq 1332/1332, decs 245/245, ast_match 371/371, dasSQLITE 782/782
- AOT:    same
- JIT:    same modulo test_capture_cfb.das pre-existing failure on master

Net: -16 LOC (139 ins / 155 del). MCP lint + CI lint + format all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@borisbat borisbat deleted the bbatkin/linq-fold-qmatch-adoption branch May 30, 2026 15:21
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