skills/das_macros: document AST-match helpers, qmatch idiom, push cluster consolidation#2800
Conversation
…ush cluster consolidation Codifies the patterns established by PRs #2793-#2799 so future macro work follows the documented forms instead of rediscovering them. Three new sections: - "Shared AST-match helpers" — table of 11 public helpers in daslib/ast_match.das + daslib/templates_boost.das (match_call_in_module, match_call_in_linq, peel_lambda_*, peel_tuple_field_read, extract_const_string, qn, qm_peel_ref2value, push_block_list) with signatures + when-to-reach-for-each. Includes a "when patterns apply vs don't" note: introspection-heavy files (linq_fold, sqlite_linq, ast_match) benefit; emit-only files (decs_boost, the emitter half of templates_boost) don't. - "qmatch — predicate-style pattern matching" — anti-pattern (hand-rolled is X / as X cascades) vs preferred predicate form with $e/$f/$v/$i tags bound to PRE-DECLARED outer variables (not result-struct fields). Documents the QMatchResult shape, points to sqlite_linq for 37+ adoption sites + tests/ast_match for grammar exercises. - "Push cluster consolidation" (new subsection under "qmacro vs quote") — consecutive `arr |> push <| qmacro_expr() { ... }` runs into the same array collapse into a single emission via either Form A (push_from + qmacro_block_to_array, preferred, no clone) or Form B (push_block_list + qmacro_block, clones, use when the source block stays alive). Includes "when NOT to collapse" guard. One section updated: - "Peel ExprRef2Value before qmatch" now routes through qm_peel_ref2value (single source of truth) instead of showing the manual if-peel snippet. Adds note on why the helper still uses while-peel (conservative until ast_block_folding.cpp synthesis paths are audited). PR 6 (decs_boost migration from the original ladder plan) intentionally skipped: audit confirmed decs_boost has zero hand-rolled is_*_call helpers, zero qname construction, zero ExprRef2Value while-loops, zero push qmacro_expr clusters, and zero peel_lambda candidates. The file is already lean — the migration would manufacture work. +100 / -15 LOC. Doc-only; no code changes.
There was a problem hiding this comment.
Pull request overview
This PR updates skills/das_macros.md to document established macro/AST-introspection patterns, including shared AST-match helpers, the qmatch idiom, and how to consolidate consecutive qmacro push clusters.
Changes:
- Added a “Shared AST-match helpers” section describing public helpers from
daslib/ast_match.dasanddaslib/templates_boost.das. - Added a “
qmatch— predicate-style pattern matching” section with recommended usage and tag explanations. - Added “Push cluster consolidation” guidance for collapsing consecutive
qmacro_exprpushes, and updated theExprRef2Valuepeeling section to referenceqm_peel_ref2value.
Comments suppressed due to low confidence (1)
skills/das_macros.md:340
- This subsection states that
qmatchcan't match throughExprRef2Valueand that auto-peel insideqmatchis still TODO, but the matcher already peelsExprRef2Valueon both source and pattern sides viaqm_peel_ref2valueemitted bygenerate_match(and has dedicated transparency tests). Please update this section to avoid sending readers toward unnecessary (or contradictory) manual peeling, and refocus it on cases whereqm_peel_ref2valueis needed outsideqmatch(e.g., hand-writtenis/asprobes).
Post-Mode-2-expansion AST walking will see field reads wrapped in `ExprRef2Value`. `qmatch` is RTTI-strict — it matches `ExprField` but not `ExprRef2Value(ExprField(...))`. **Route through `qm_peel_ref2value`** (the single source of truth in `daslib/ast_match.das`) instead of hand-rolling either a `while`-peel or an `if`-peel:
```das
require daslib/ast_match
qm_peel_ref2value(node)
if (node == null) {
macro_error(prog, at, "_where: ExprRef2Value with null subexpr")
return ""
}
// now `qmatch(node, _.$f(name))` etc. work as expected
qm_peel_ref2value currently uses while (e is ExprRef2Value) rather than single-if-peeling. The conservative loop is intentional until block-folding is fully audited — tests/ast_match/test_ref2value_skip.das exercises a triple-wrap shape, and src/ast/ast_block_folding.cpp synthesis paths could theoretically produce a nested wrapper. Once that audit lands, the helper switches to single-if peel in one place and every consumer follows automatically.
Auto-peel inside qmatch itself remains a TODO documented in daslib/ast_match.das. Until then, every analyzer entry point that takes an expression coming out of post-expansion (predicate body, projection body, classifier helpers like is_const_or_captured_var) needs the qm_peel_ref2value call at the top.
</details>
---
💡 <a href="/GaijinEntertainment/daScript/new/master?filename=.github/instructions/*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
| | `qm_peel_ref2value` | `(var e : Expression?&) → void` | Single source of truth for `ExprRef2Value` peeling. Always call this instead of hand-rolling `while (... is ExprRef2Value)` or `if`-peel — see ["Peel ExprRef2Value before qmatch"](#peel-exprref2value-before-qmatch). | | ||
| | `push_block_list` | `(var stmts, var blockExpr)` in `daslib/templates_boost.das` | Splices every statement from a `qmacro_block(...)` result into `stmts`, cloning each. See ["Push cluster consolidation"](#push-cluster-consolidation). | | ||
|
|
||
| **When the patterns apply (and when they don't).** These helpers earn their keep in files that **probe AST shape** to route macro emission — `linq_fold`, `sqlite_linq`, `ast_match` itself. Files that only **emit code** without introspecting it — `decs_boost`, the emitter half of `templates_boost` — won't find adoption sites. Audit before mechanically searching: if a file has zero hand-rolled `is X / as X` call-cascades and zero qname construction, the patterns don't apply there. | ||
|
|
||
| ## `qmatch` — predicate-style pattern matching | ||
|
|
||
| Prefer `qmatch(expr, <pattern>).matched` over hand-rolled `is X / as X` cascades when matching structural AST shapes. `qmatch` is RTTI-strict and won't traverse `ExprRef2Value` — peel first via `qm_peel_ref2value`. |
| - `_` — anonymous wildcard (no bind) | ||
| - Concrete operators (`&&`, `||`, `+`, `==`, `<`, dot-field, function-call) and literals match literally | ||
|
|
||
| Result is `QMatchResult` with `.matched : bool` and `.error : QMatchError` — captured bindings live in the pre-declared outer variables, NOT on the result struct. On match failure the bindings are left untouched. |
| | `peel_lambda_rename_2vars` | `(expr, a, b) → Expression?` | 2-arg form for `aggregate`-style `block<(acc, x) : AGG>` lambdas. Returns `null` on shape mismatch — caller decides fallback. | | ||
| | `peel_tuple_field_read` | `(expr, bindName, fieldIndex) → bool` | `true` when `expr` matches `<bindName>._<fieldIndex>` — tuple-slot read on a named bind. Single-level `ExprRef2Value` peel on each side. | | ||
| | `extract_const_string` | `(e) → tuple<bool; string>` | For `ExprConstString` returns `(true, value)`, else `(false, "")`. Use to consume compile-time string literals threaded through macro args. | | ||
| | `qn` | `(prefix, at) → string` | Synthesizes ``` `<prefix>`<at.line>`<at.column> ``` — qualified-name helper for macro-emitted locals. Deterministic per `(prefix, at)`; synthesized `LineInfo()` (line=0, col=0) WILL collide across distinct synth sites with the same prefix — build a synth-specific name if it matters. | |
Summary
Codifies the patterns established by PRs #2793-#2799 (the linq_fold / sqlite_linq / ast_match harvest ladder) so future macro work follows the documented forms instead of rediscovering them.
Three new sections:
"Shared AST-match helpers" — table of 11 public helpers in
daslib/ast_match.das+daslib/templates_boost.das(match_call_in_module,match_call_in_linq,peel_lambda_*4 forms,peel_tuple_field_read,extract_const_string,qn,qm_peel_ref2value,push_block_list) with signatures + when-to-reach-for-each. Includes a "when patterns apply vs don't" note: introspection-heavy files (linq_fold, sqlite_linq, ast_match) benefit; emit-only files (decs_boost, the emitter half of templates_boost) don't."qmatch — predicate-style pattern matching" — anti-pattern (hand-rolled
is X / as Xcascades) vs preferred predicate form. Documents that$e/$f/$v/$itags bind to pre-declared outer variables, not result-struct fields (a subtle but easy-to-get-wrong API shape). Points tosqlite_linq.dasfor 37+ adoption sites +tests/ast_match/test_qmatch_*.dasfor grammar exercises."Push cluster consolidation" (new subsection under "qmacro vs quote") — consecutive
arr |> push <| qmacro_expr() { ... }runs into the same array collapse into one emission via either Form A (push_from+qmacro_block_to_array, preferred, no clone) or Form B (push_block_list+qmacro_block, clones, use when the source block stays alive). Includes "when NOT to collapse" guard.One section updated:
qm_peel_ref2value(single source of truth) instead of showing the manual if-peel snippet. Adds note on why the helper still useswhile-peel (conservative untilast_block_folding.cppsynthesis paths are audited).PR 6 (decs_boost migration from the original plan) intentionally skipped:
Audit confirmed decs_boost has zero hand-rolled
is_*_callhelpers, zero qname construction, zeroExprRef2Valuewhile-loops, zeropush qmacro_exprclusters, and zero peel_lambda candidates. The file is mostly code-generation plumbing, not AST introspection — the migration would manufacture work. The "when patterns apply" note in the new helpers section codifies this finding so future audits start with the question instead of mechanical search.Test plan
daslib/ast_match.das:2199-2332verbatim, qmatch example uses the actualvar lhs, rhs : ExpressionPtr; qmatch(...).matchedshape verified againsttests/ast_match/test_capture_e.das#peel-exprref2value-before-qmatchand#push-cluster-consolidationare GitHub-flavored markdown auto-anchors that match the new section headingsvar lhs, rhs : ExpressionPtrexists in 5+ codebase sites incl.daslib/random.das:85)🤖 Generated with Claude Code