Skip to content

[pull] master from GaijinEntertainment:master#1037

Merged
pull[bot] merged 40 commits into
forksnd:masterfrom
GaijinEntertainment:master
May 26, 2026
Merged

[pull] master from GaijinEntertainment:master#1037
pull[bot] merged 40 commits into
forksnd:masterfrom
GaijinEntertainment:master

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented May 26, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

profelis and others added 30 commits May 25, 2026 19:34
…verse + plan_distinct

Introduce a two-layer split for the `_fold` splice planner:

  1. Pattern recognition — declared via `splice_patterns` data table.
     Each row is a `SplicePattern { name, chain : array<Slot>, requires
     : array<RequiresPredicate>, emit : EmitFn }`. Slots are
     `(SlotMatcher, SlotCardinality, capture_name, arity)` variants.
     One generic `match_pattern(p, calls, top) : MatchResult` walker
     replaces the 8–30 tracking-flag + if/elif walls in each plan_*.

  2. Code generation — reusable emit archetypes parameterized by a
     `SourceAdapter` (Array stub in PR A; widens to Decs/DecsFind/Zip
     /DecsJoin in PR C/D). One archetype per recognized chain shape.

`plan_reverse` migrates to 5 pattern rows + 5 emit archetypes (Ra
counter, Rb walk-overwrite-last scalar, R6 backward-index walk, R-2a
backward walk with dset gate, R1-R4 buffer + reverse_inplace catch-all).
`plan_distinct` migrates to 2 pattern rows + 1 emit archetype with
internal terminator-shape dispatch. Both stubs delete the imperative
bodies and dispatch through their per-plan pattern tables.

The full 4-PR roadmap and design decisions live in `daslib/linq_fold.md`
(masterplan, living doc). Subsequent PRs B/C/D migrate the remaining
11 plan_* functions and collapse the per-plan tables into one flat
`splice_patterns`.

Behavior unchanged — pure refactor. 600+ tests across 14 linq test
files pass; per-archetype + walker integrity tests added in
`tests/linq/test_linq_fold_pattern_walker.das`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…(no capture needed)

PR A's emit archetypes and predicates are pure — none capture any state.
Lambda was preemptive coverage for hypothetical PR B-D closure factories
(make_arity_eq, make_emit_terminal_select_wrapped, etc.) but forces every
[init] row to wrap a real function in a useless lambda:

  emit = @(var c, var top, src, at) => emit_x(c, top, src, at)

vs. with function<> typedef:

  emit = @@<EmitFn> emit_x

Same readability, half the LOC per row, no heap allocation for the
function pointer itself (8-byte workhorse value). Predicates likewise
switch from `@(...) {...}` to `@@(...) {...}` and declare as `let`
(workhorse-immutable storage).

When PR B-D actually hits a closure-bearing predicate or emit wrapper,
options are: (a) sibling `EmitLambda = lambda<>` typedef + dispatch on
which kind, (b) variant-RequiresEntry sum type with closure cases.
Both are zero-cost-for-PR-A-shape and reachable from function<>.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three real bugs + one doc-drift fix on the iterator-wrap path that the
imperative bodies had wired up via `expr._type.isIterator` and the refactor
hard-coded to `false`. PR C was the wrong place to defer it — return-shape
is a fold-level concern, not a source-adapter one.

- Kernel: introduce `EmitCtx { top; src; expr_is_iterator }` and thread it
  through `EmitFn`. Stubs build it per pattern attempt, pre-cloning `top`
  (replacing the `[clone(top)]` annotation which was load-bearing under
  direct call but never fired through `invoke()`).
- Bug fix: `emit_reverse_buffer_inplace` and `emit_hashtable_dedup` now
  read `ctx.expr_is_iterator` and pass it to `buffer_return`, so
  `_fold(seq |> reverse())` / `_fold(seq.distinct())` (no terminator)
  return iterator as the chain does instead of array.
- Predicate library: convert the three module-level
  `let X : RequiresPredicate = @@(...) { ... }` inline lambdas to named
  `def` functions wrapped with `@@<RequiresPredicate>` at use sites. The
  inline form produced `_localfunction_*` symbols that the LLVM JIT pass
  couldn't resolve — this was breaking every CI build lane.
- Regression test: `tests/linq/test_linq_fold_iterator_wrap.das` —
  3 tests / 6 sub-runs asserting `typeinfo typename(got) == "iterator<int>"`
  on reverse / distinct / distinct_by chains with no terminator. Verified to
  fail when buffer_return wrap is reverted.
- Masterplan `daslib/linq_fold.md`: kernel snippet refreshed to match shipped
  code — Captures, MatchResult, EmitCtx, named-predicate idiom. The four
  Copilot inline comments on lines 2564 / 2581 / 2881 (linq_fold.das) and
  95 (linq_fold.md) are all addressed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The new public `match_pattern` walker has `array<tuple<ExprCall?; LinqCall?>>`
in its signature. With `LinqCall` private, the Sphinx doc generator emits an
undefined-label warning ('struct-linq_fold-linqcall') in the auto-generated
RST, and CI's `Build latex files` step fails with `-W` (warnings-as-errors).

Drop `private` on the struct itself; the `linqCalls` table that holds the
records stays private. Closes the only Sphinx warning surfaced by PR A.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI's `daslang -exe utils/aot/main.das` was failing with `Internal jit error.
Failed to get IR for functions ... emit_reverse_*, emit_hashtable_dedup, ...`.

Root cause: `populate_plan_*_patterns` was `[init]` (program startup), making
it runtime-reachable. The pattern rows hold `@@<EmitFn>` addresses of
`[macro_function]` emit fns whose bodies contain quote() expressions the LLVM
JIT cannot lower. The whole transitive closure of macro-time helpers got
dragged onto the JIT graph and failed.

Switching to `[_macro]` runs the populator at compile time only (before any
user `_fold(...)` expands) and keeps the function pointer references off the
runtime JIT graph entirely. Add an idempotency guard so re-entry is a no-op.

Tests: drop the four runtime-table inspection tests — `plan_reverse_patterns`
/ `plan_distinct_patterns` are now macro-time-only state, empty at runtime by
design. Kept the alias_table test (runtime-literal-initialized) and the
chain_prefix_of / synthetic-shadow tests (kernel logic exercised directly).
End-to-end pattern selection stays covered by the existing test_linq_fold_*.das
suite — each user chain exercises emit fns through `_fold`.

Local verification:
- `bin/daslang -exe utils/aot/main.das` builds clean (was the failing CI step)
- `bin/daslang -jit dastest/dastest.das -- --test ...` (JIT-mode dastest):
  6/6 iterator-wrap + 10/10 walker + 36/36 theme8 + 385/385 core fold pass

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- audio_boost.das set3D: revert channel-converter input to 2u (HRTF always
  emits stereo; uint(source.channels) corrupts mono 3D sources).
- audio_boost.das end_batch: restore panic("no batch"); drop the redundant
  inner `for it; delete it` loop (outer `delete *global_batch` already
  finalizes nested arrays).
- audio_boost.das begin_batch/end_batch: mark deprecated, recommend
  `batch(cb)`.
- audio_boost.das play_*_from_pcm_stream: unify to sid : SID = INVALID_SID
  + ternary so generate_sound_sid() is only called when caller didn't pass
  a SID.
- strudel_player.das + strudel_midi_player.das: add `options persistent_heap`
  + `options gc` (linear allocator leaks on the audio/sequencer threads).
- Lint sweep on the two strudel files surfaced by the options addition:
  drop redundant float()/string() casts (PERF020) and unsafe() wraps on
  lock_box_create/job_status_create (STYLE024).
Three more fixes flagged by Copilot on the latest commits:

- emit_reverse_backward_index_walk (R6, `reverse + take`) — was hard-coding
  `buffer_return(bufName, false)`. Same root cause as R1's
  emit_reverse_buffer_inplace / emit_hashtable_dedup fixes: my earlier
  analysis incorrectly claimed `implicit_to_array` predicate forces array
  context. It does the OPPOSITE — true when no terminator is captured,
  i.e. the chain ends bare (could be iterator-typed in outer context).
  Use `ctx.expr_is_iterator`.
- emit_reverse_backward_walk_dset_gate (R-2a, `reverse + distinct[_by]` on
  array source) — same fix.
- chain_prefix_of — was returning true for equal-length chains (the guard
  used `>` not `>=`). Two patterns with identical chains but different
  `requires` predicates are both legitimately reachable (walker falls
  through on requires-failure), so equal-length is not a shadowing
  relation. Enforce strict prefix (`length(a) >= length(b)` → false).

Regression tests added to test_linq_fold_iterator_wrap.das:
- test_iterwrap_reverse_distinct_array_source — covers R-2a path
- test_iterwrap_reverse_take_array_source — covers R6 path
Both verified to fail when their buffer_return wrap is reverted.

Local verification:
- 10/10 iterator_wrap, 10/10 walker, 36/36 theme8, 385/385 core fold pass
- `bin/daslang -exe utils/aot/main.das` builds clean (no JIT regression)
- lint + format clean

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four small fixes flagged on top of R2:

- `take_arg_is_int` — guard `take.arguments[1]._type != null` before reading
  `baseType`. Matches the defensive pattern used elsewhere in the file; avoids
  a potential macro-time null deref on partially-typed expressions.
- `emit_reverse_backward_index_walk` — the takeN clamp ternary spliced
  `c["take"].arguments[1]` three times. With a side-effecting `take(...)` arg
  that fires three times instead of once. Bind to a `rtakeLim` local and reuse
  (mirrors `emit_hashtable_dedup`'s `takeLim` pattern).
- `emit_reverse_buffer_inplace` — same 3x splice in the resize clamp. Same
  fix: bind to a `rev_takeLim` local via `qmacro_block_to_array` and reuse.
- masterplan `linq_fold.md` naming table — `Captures` row still showed the
  old `table<string; tuple<ExprCall?; LinqCall?>>` shape; updated to current
  `table<string; ExprCall?>` and added `MatchResult` / `EmitCtx` rows.

Local verification:
- 10/10 iterator_wrap, 10/10 walker, 36/36 theme8, 385/385 core fold pass
- lint clean

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New daslib module shipping a `with_` macro that binds array/table
element refs inside a block with an automatic container lock around the
body — push/erase/resize/clear inside the body panic at runtime instead
of silently dangling, matching the safety the typer's
error[31300] (local-ref-to-non-local-expression-is-unsafe) was trying
to enforce.

Surface:

  with_(arr[0]) { _.f1 = 99 }                  // default `_` binding
  with_(arr[0]) $(elem) { elem.f1 = 99 }       // named binding
  with_(arrA[0], arrB[1]) $(a, b) { ... }      // multi-arg positional (up to 3)
  let v = with_(arr[0]) { return _.f1 }        // return-value (single-arg)
  with_(tab["k"]) $(v) { v.f1 = 555 }          // table; upserts if missing

Phase 1 cuts:

  - Container must be arr[i] or tab[k] ExprAt — locals, struct
    fields on locals, function-call results refused with a hint to use
    built-in `with`.
  - Workhorse-element containers (array<int> etc.) refused: mutation
    through a by-value block param wouldn't propagate; the user should
    write arr[i] = value directly.
  - At most one table-keyed arg per call — a 2nd insert/erase would
    rehash and invalidate the pinned entry, so the macro refuses any
    second table arg.
  - Multi-arg + value-returning body refused (helper proliferation);
    rewrite as a single-arg with_.

Implementation notes:

  - 6 generic helpers (_with_locked_arr1_v / _arr1_r / _tab1_v /
    _tab1_r / _arr2_v / _arr3_v) wrap lock + invoke + unlock in
    function-level `finally`. Helpers use __builtin_array_lock_mutable
    / __builtin_table_lock_mutable universally (the const-cast forms
    accept both const and var containers in one generic instantiation).
  - Table helpers resolve the element ref BEFORE locking (in `unsafe`)
    because t[k] from inside a locked table hits the insert path and
    panics even on existing keys.
  - Bodies that panic still leak the lock — daslang's `finally` is
    broken on panic per issue #2532, same as
    daslib/array_boost::array_view etc. Will tighten once #2532 lands.

Coverage: 31 tests across 5 functional files + 6 expect-fail compile
tests for every refusal path. Tutorial 18 + RST seealso links land in
tutorials/macros/ and doc/source/reference/tutorials/macros/. AOT
registration added to tests/aot/CMakeLists.txt. Module registration
added to doc/reflections/das2rst.das (regen needs a build with dasHV
present; the change matches the existing pattern for `defer`).

Inspired by spiiin's blog macro (https://spiiin.github.io/blog/1637349975/)
but adds the missing lock + multi-arg + macro-time refusals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot flagged that the new R1-R4 + Rb pattern chains accept BOTH a
pre-reverse `_select(f)` and a post-reverse `_select(g)` in the same match,
where master's imperative `plan_reverse` had an explicit `!seenSelect` guard
that bailed those chains to tier-2 cascade.

This is an intentional extension over master, not a recognition bug. The two
selects compose cleanly — `pushExpr` uses the pre-projection, the post-select
projects the reversed survivors at return. No semantic conflict; just plain
function composition over the buffer. Net result: strictly faster (splice
instead of cascade) for an obvious chain shape that master couldn't fuse.

- 2 new tests in test_linq_fold_terminal_select.das demonstrating both
  patterns (R1-R4 with `to_array`, Rb with `first`) splice correctly with
  both selects.
- linq_fold.md decision log entry documenting the intentional extension.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per Boris's review: the splice-emit fns weren't being exercised across the
full source × terminator matrix. The prior iter_wrap tests covered only the
iter-source corners (and partial array-source). Add the missing corners
systematically — 17 tests / 34 sub-runs — so each buffer-emitting pattern
has all four (or all relevant) cells of the matrix covered:

  reverse()              — 4 corners (iter|array src × iter|array terminator)
  distinct() / by        — 4 corners + 1 distinct_by iter-only carryover
  reverse() + distinct() — 4 corners
  reverse() + take(N)    — 4 corners

`typeinfo typename(got)` is the load-bearing assertion. Array-bound tests
use `let got <-` (no consuming for-loop on the binding) and the assertion
strings reflect the resulting `array<int> const` binding type.

The iter→iter corners cover PR A R1's `buffer_return(..., ctx.expr_is_iterator)`
fix on emit_reverse_buffer_inplace and emit_hashtable_dedup; the array→iter
corners cover R2's same fix on emit_reverse_backward_walk_dset_gate and
emit_reverse_backward_index_walk. The array<int>-const corners protect
against future over-correction (always wrapping with `to_sequence_move`).

PR B onward should follow this naming convention for new emit fns:
test_matrix_<shape>_<src>_to_<term>.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two scope cuts that go together:

1. Drop the `let v = with_(arr[0]) { return _.f }` shape. The user can
   trivially write `var v : T; with_(arr[0]) { v = _.f }` and the
   return-value path was dragging in two helpers, a body-walks-for-return
   detector, an arity-vs-return refusal, and a class of generic-inference
   bugs the void form sidesteps.

2. Drop the workhorse-element refusal. With the helper sig now
   `block<(var x : TT&) : void>` and the macro emitting block params as
   parser-shaped `auto -const removeConstant=true` (matching exactly what
   `$(a)` produces directly), daslang's typer drives the inference all
   the way to `int&` / `float&` / etc. — no special-case in the macro,
   workhorse mutation propagates correctly.

Net diff to the surface: same API minus return-form. Workhorse arrays
and tables now Just Work:

  var ints = [1, 2, 3]
  with_(ints[1]) { _ = 222 }            // ints == [1, 222, 3]

  var tab : table<string; int>
  tab |> insert("alpha", 10)
  with_(tab["alpha"]) $(v) { v = 200 }  // tab["alpha"] == 200

Files dropped: `_with_locked_arr1_r` / `_with_locked_tab1_r` helpers,
`block_returns_value` walker, `failed_with_multi_return.das`,
`failed_with_workhorse.das`, `test_with_return.das`.

New: `tests/with_boost/test_with_workhorse.das` (5 tests covering
single-arg / named / float / multi-arg / mixed workhorse+struct).

Tutorial Section 5 and RST Section 3 swap return-value content for
workhorse content; expected-output updated.

28/28 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-channel runtime routing decision driven by a configurable budget
(default 32). Each frame, the closest-to-head channels win the HRTF slots;
the rest run simulated 3D (constant-power pan + distance attenuation, no
convolution). Set via set_hrtf_budget(n). Sticky margin in rank space
(budget + max(2, budget/10)) prevents flapping when channels swap rank
between frames, while clamping to 0 when budget=0 so "all simulated"
actually clears in-flight HRTF channels.

HRTF/simulated branch in mix(): when is_hrtf is true the existing HRTF
processing emits stereo for the existing 2-channel converter; when false
the HRTF stage is skipped and a second converter built once in set3D
(source.channels -> MA_CHANNELS) handles the mono-to-stereo upmix at
native rate instead of an interpreted daslang loop.

Calibrates the HRTF broadband gain at azimuth=0, elevation=0 once in
initialize_mixer using a 1 kHz sine probe (2048 frames, measure last
1536 to skip the 256-sample crossfade region). Simulated-path normalizer
is hrirGain * sqrt(2) to compensate the -3 dB constant-power center pan,
sanity-clamped to [0.25, 4.0] with a sqrt(2) fallback. DC was the wrong
probe in the first attempt: HRIRs attenuate DC by ~10x, so the measured
RMS was unrepresentative of the broadband response.

Cross-context stats via a caller-provided LockBox. set_audio_stats_box(box)
registers a box on the audio context; the audio thread publishes one
AudioSystemStats snapshot per ~1-second window with utilization_pct,
hrtf_count, total_3d. The example reads via box |> grab(s) on the main
thread and prints once per second.

Addresses Copilot review comments on #2879:
- R1: drop ma_volume_mixer_set_linear_pan(false) from setup3D. The
  per-channel pan-mode is now driven by the budget; setting it false
  globally introduced a -3 dB loudness regression on all 3D channels.
- R2: derive pause_fade_step field default from MA_SAMPLE_RATE (was
  hard-coded 1/96, correct only at 48 kHz).

Tests: tests/audio/test_hrtf_budget.das covers hrtf_budget_classify
exhaustively for budget=0/32/999, sticky-margin behavior, and the
regression case (budget=0 must clear in-flight HRTF channels rather
than preserving them under sticky-margin).

Example update (examples/audio/hrtf/main.das):
- B key cycles HRTF budget: mixed top-32 / all simulated / all HRTF.
- Per-second stats line printed: utilization, hrtf/total split.
- Pre-existing lint sweep: PERF020 redundant int casts on GLFW key
  constants, STYLE013 SoundSource named-arg ctor, STYLE016/STYLE005/
  STYLE024 cleanups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
release only decrements the LockBox refcount; the actual delete
requires lock_box_remove (or it leaks the C++ JobStatus + Feature +
3 smart pointers).

Lifecycle now wraps the audio system:
- main creates stats_box (rc=1)
- enters with_audio_system; set_audio_stats_box adds share-ref (rc=2)
- main loop runs, exits with_audio_system block
- audio_system_finalize; audio thread releases share-ref (rc=2 to 1)
- outer defer; lock_box_remove (rc=1 to 0, delete)

Also adds:
- get() (read-only) replaces grab() (one-shot consume) for the
  periodic stats poll
- --max-frames N CLI option on the example for headless repro of
  shutdown-leak debugging

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Copilot R4 review batch:

- Privacy sweep (closes R4-1, R4-2 + your question on public surface):
  the walker test only needs Slot/SlotMatcher/SlotCardinality + m_*/c_*
  constructors, SplicePattern, Captures/EmitCtx/EmitFn typedefs,
  chain_prefix_of, check_pattern_table_reachable, alias_table. Everything
  else moves to private: LinqCall (reverts 74c5b5c), SourceAdapter,
  MatchResult, RequiresPredicate, match_pattern, call_norm_name,
  slot_matchers_equal, slots_structurally_match, the predicate library
  (array_source / take_arg_is_int / no_terminator), 3 var pattern tables,
  6 emit_* fns, both populate_* fns. R4-1 and R4-2 (null-deref worries on
  synthetic test inputs reaching public call_norm_name / match_pattern)
  dissolve — only the macro pipeline calls them now, with well-formed AST.
  As bonus, removing LinqCall from match_pattern's public signature drops
  the Sphinx cross-ref dependency it was carrying.

- R4-3: emit_reverse_walk_overwrite_scalar — defensive bail when projection
  or top type is partially-typed (null _type / firstType), matching the
  guard pattern in emit_reverse_backward_index_walk.

- R4-6: rename implicit_to_array → no_terminator. The old name read like a
  return-shape predicate; return shape is actually driven by
  ctx.expr_is_iterator. Updated linq_fold.md naming table to match.

- R4-5: linq_fold.md status — flip the migration-phases table rows for
  PR A phase 0 + phase 1 from "not started" to "complete" to align with
  the Status section. Updated the Tests / exports philosophy section to
  enumerate the actual public surface after this sweep.

Test plan:
- compile_check clean on daslib/linq_fold.das + test_linq_fold_pattern_walker
- lint clean
- test_linq_fold_pattern_walker / _iterator_wrap / _terminal_select green
- test_linq_fold_ast (228 / 228), _theme8 (36 / 36), _theme3_c1_c5 (24 / 24),
  _theme45 (32 / 32), _non_copyable_default (6 / 6) all green

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses Copilot R3: the docstring claimed null clears stats publication,
but the body unconditionally called add_ref/set on the box, crashing on
null. Add an early-return branch that pushes a 0 box pointer; the audio
thread's command handler already reinterprets that as null and clears
g_stats_box.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrite of the with_ macro after the helper-per-arity approach hit its
ceiling. Inline emission scales to any N container args mixing array +
table freely, removes the 4 _with_locked_* helpers entirely, and fixes
a latent bug where multiple $e(subexpr) splices re-evaluated the same
container expression (fatal for array literals — three different temp
arrays got locked, invoked, unlocked).

Emission shape, fully spelled out at each call site:

  {
    var __with_c_0 & = unsafe(arr1)         // pre-bind each container
    var __with_c_1 & = unsafe(arr2)         //   once, ref a local thereafter
    var __with_tref_2 & = unsafe(tab[k])    // table: pre-resolve element
    __builtin_array_lock_mutable(__with_c_0)
    __builtin_array_lock_mutable(__with_c_1)
    __builtin_table_lock_mutable(__with_c_2)
    invoke(<user_block>, __with_c_0[i1], __with_c_1[i2], __with_tref_2)
    __builtin_table_unlock_mutable(__with_c_2)   // reversed
    __builtin_array_unlock_mutable(__with_c_1)
    __builtin_array_unlock_mutable(__with_c_0)
  }

No `finally` — daslang panic is fatal (not a C++/JS exception), so if
the invoke panics the program is exiting anyway and skipped unlocks
don't matter. This is the correct semantics, not a bug — see CLAUDE.md
update and the now-closed issue #2532.

Container constraint: subexpr must be an ExprVar-rooted lvalue chain.
Array literals / function-call results have temp lifetime that ends
with the surrounding expression, so ref-binding them would dangle.
Refused at macro time via is_lvalue_chain() — failed_with_array_literal
covers the diagnostic.

Block-param typing: every param is pinned to the container's element
type with ref flag set, so workhorse types bind as int& / float& / etc.
and mutation propagates. No struct-vs-workhorse special case in the
macro.

Tests (30 total, all passing):
  - test_with_array.das (6), test_with_table.das (5),
    test_with_workhorse.das (5), test_with_lock_panics.das (7)
  - test_with_n_arg.das (3) — NEW — 5/7 arrays, mixed 4-array+1-table
    proving inline emission has no arity cap
  - failed_with_*.das (5) refuse tests, including new
    failed_with_array_literal.das for the temp-lifetime refusal

Docs / memory / issues:
  - CLAUDE.md: new "Panic is fatal, not an exception" paragraph under
    Error handling, documenting that try/recover is for
    diagnostics-before-exit and `finally` skipping on panic is by design
  - feedback_finally_skipped_on_panic.md flipped from "bug, issue #2532"
    to "by design, closed not-planned"
  - feedback_no_try_recover_for_soft_fail.md tightened framing
  - tutorial Section 7 reworded — was demonstrating try/recover catching
    the lock panic, now documents the would-panic shape in a comment
    (consistent with the no-recover-and-continue framing)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
R4: rename g_stats_window_time_us to _ms — the variable accumulates
milliseconds (the dt_ms input from get_time_usec()/1000), but the
`_us` suffix and surrounding comments implied microseconds. The math
worked because everything was ms; the rename removes a unit-bug trap.

R5: skip 3D channels in the global_volume handler's per-channel ramp
loop. update_hrtf overwrites volume_mixer for every is3D channel each
audio callback (reading g_volume into the target), so the 25ms ramp
applied here for 3D channels was immediately discarded next callback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  - tests/aot/CMakeLists.txt: WITH_BOOST_AOT_GENERATED_SRC was generated
    via DAS_AOT but never added to test_aot's source list or
    SOURCE_GROUP_FILES — fixed both so the new AOT stubs actually
    compile/link into test_aot. (Copilot catch — real bug.)
  - tutorials/macros/18_with_boost.das: "Covers" list dropped the
    return-value bullet and the workhorse-refusal bullet (both
    inaccurate post-rewrite); added workhorse + tables + lock bullets.
  - doc/source/reference/tutorials/macros/18_with_boost.rst: Section 1
    no longer claims workhorse types are refused.
  - tests/with_boost/test_with_array.das: test_field_chain_container
    now actually exercises an ExprField-rooted lvalue chain
    (obj.children[i]) — was using plain arr[1], which didn't match
    the test's own description.
  - tests/with_boost/failed_with_two_tables.das: comment now describes
    the actual rule (max one table-keyed arg per call) instead of
    "2nd-arg is a table" which read like a one-off restriction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- R5-1 / R5-2: guard both populate_plan_*_patterns [_macro] initializers
  with is_compiling_macros_in_module("linq_fold"), matching the
  established daslib convention (ast_boost.das:1079, match.das:764,
  validate_code.das:183). Combined with the existing !empty() idempotency
  check via || to satisfy STYLE016.

- R5-3: decision log entry for Captures was stale — claimed
  tuple<ExprCall?; LinqCall?> but the actual typedef simplified to
  ExprCall? during PR A R1 (LinqCall record recovered on demand via
  linqCalls[call_norm_name(c)]). Updated the entry to reflect the
  shipped shape and note the dropped initial sketch.

Test plan:
- compile_check + lint clean
- test_linq_fold_pattern_walker / _iterator_wrap / _terminal_select /
  _theme8_fusion_arms all green

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two real fixes from the second Copilot review pass:

  - daslib/with_boost.das: `is_lvalue_chain` no longer follows ExprAt /
    ExprSafeAt hops. A subexpr like `outer[i].inner[j]` would have
    been accepted, locking only `inner` — leaving `outer` mutable so
    a body could push to `outer`, reallocate it, and dangle the
    `inner` ref. Now refused at macro time; new
    `failed_with_nested_array.das` covers the diagnostic.

  - daslib/with_boost.das: dropped the module-header line about
    "temporary-typed (Foo#) containers are bound without a lock" —
    obsolete since the rewrite. Every accepted container now goes
    through the same pre-bind + lock + invoke + unlock path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
R6: reset the rolling window (g_stats_window_time_ms, g_stats_window_samples)
when system_stats_box is swapped or cleared. Without this, a re-registered
box could publish its first utilization computed against accumulator state
left over from the previous registration.

R7: drop g_stats_window_count — it was incremented but never read. The
flush gate is realTimeMs >= 1000, not a callback-count threshold; the
variable and its misleading "once >=N callbacks we publish" comment
were vestiges of an earlier design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
das2rst.das generates a stub module-with_boost.rst when the module
is registered but has no handmade description; CI's
"Check for // stub in handmade docs" step caught this. Modeled on
module-defer.rst — short prose description + require snippet + a
small runnable example.
- R6-1: real bug in emit_reverse_walk_overwrite_scalar — when the
  chain has BOTH a post-reverse _select(g) AND first_or_default(d),
  the user's `d` is already typed at the post-select element type
  (that's the user-facing contract for first_or_default). The emit was
  applying termsel(d) for dRetExpr, which double-applies g and either
  miscomputes or breaks typing. Fix: dRetExpr = qmacro(dBindName) — bare,
  never project. lastRetExpr (the found-branch) still projects correctly.

  Regression coverage added in test_linq_fold_terminal_select.das:
  test_reverse_pre_and_post_select_first_or_default_nonempty (exercises
  the found branch) and _first_or_default_empty (exercises the default
  branch — empty where filter, expected -99, would have returned -98
  under the bug).

- R6-2: slot_matchers_equal treated `one_of` as order-sensitive
  (element-by-element equality). Since `one_of` is semantically a set,
  two `one_of` matchers with the same elements in different order should
  compare equal — otherwise chain_prefix_of / check_pattern_table_reachable
  miss real shadowing cases. Fixed to set-membership (same length + every
  element in a is in b). Forward-looking — current pattern rows use only
  `literal` and `alias` matchers.

- R6-3: masterplan kernel-section heading said "PUBLIC for testability",
  but R4 narrowed the actual public surface to a subset (the Tests /
  exports philosophy section reflects this). Updated the kernel heading
  to point at that section as the authoritative visibility statement
  and noted the snippet omits `private` for readability.

Test plan:
- compile_check + lint clean
- terminal_select 28/28 (incl. 2 new first_or_default tests)
- pattern_walker / iterator_wrap / theme8_fusion_arms green

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three Sphinx errors from the CI doc build:

  - doc/source/stdlib/generated/with_boost.rst:6 + :64 "Unknown
    target name: 'with'" — das2rst.das passed
    `"with_ macro: locked array/table element binding"` as the
    module description; RST parsed the bare `with_` (trailing
    underscore = hyperlink reference syntax) as a broken target.
    Wrapped in double-backticks: ``with_``.

  - doc/source/reference/tutorials/macros/18_with_boost.rst:9
    "Title overline too short" — title source (incl em-dash) was
    65 chars, overline was 64. Bumped overline / underline length
    and dropped the stray leading space on the title line.

  - doc/source/stdlib/generated/with_boost.rst "document isn't
    included in any toctree" — added the generated file to
    sec_annotations.rst alongside defer (closest neighbour in the
    "Annotations and Contracts" bucket; with_ is a scope/lifecycle
    helper of the same family).
The asan/aot CI jobs run `test_aot -use-aot dastest --use-aot`, which
sets `policies.fail_on_no_aot = true`. Without a generated AOT stub,
linking fails with `error[50101]: AOT link failed on <test fn>`.

Adding the test to AOT_STRUDEL_FILES wires it into the same AOT
generation pipeline as every other audio-dependent test file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- R7-1: any_terminator_family alias originally listed count / long_count
  / sum / first / first_or_default, but the only consumer
  (plan_distinct_patterns → emit_hashtable_dedup) rejects first[_or_default]
  with return null. The walker matched, the emit returned null, the loop
  fell through to the next pattern or to cascade — wasted match work.
  Rename + narrow: distinct_terminator_family = [count, long_count, sum].
  Walker test updated to assert the new name + 3-element length.

- R7-2: masterplan grammar-kernel snippets showed the projected end-state
  (all PR B/C/D aliases + parameterized take_arg_is_int) without flagging
  which entries are live today vs planned. Annotated both tables with
  per-row status (PR A ✓ / planned) and added a note pointing at the
  authoritative live list in linq_fold.das. Removed the misleading
  any_terminator_family entry; documented why take_arg_is_int is
  hard-wired today (single consumer) and when to promote to factory.

Test plan:
- compile_check + lint clean
- pattern_walker / iterator_wrap / terminal_select / theme8 / theme45 green

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
das2rst's document_call_macro falls back to emitting bare
"Function annotation <name>" text when the class has no //! docstring
that write_to_detail can stash into detail/function_annotation-*.rst.
For `with_`, the trailing underscore at end-of-line then parses as an
RST hyperlink reference, producing:

  generated/with_boost.rst:64: ERROR: Unknown target name: "with".

Add a //! docstring inside the WithMacro class body — matching the
LpipeMacro shape in daslib/lpipe.das — so the fallback never fires.
The docstring keeps `with_` wrapped in double-backticks (inline
literal), preventing the same parse hazard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Default-`_` rewrite path now clones `userBlock.finalList` alongside
  `userBlock.list`. The parser doesn't accept `with_(...) {...} finally {...}`
  (the trailing `finally` is rejected as a syntax error in call-arg position),
  so this is defensive AST hygiene rather than a user-reachable bug — but a
  future macro that constructs a finalList-bearing block via qmacro and
  passes it to `with_` would have silently lost the cleanup section.

- Tutorial RST: drop the hard-coded panic string `"can't push into locked array"`
  (the runtime message is actually `"can't resize locked array"`). Replace with
  a neutral `// panics — array is locked` so the text doesn't go stale.

- Tutorial das + RST: clarify the single-table rule. The macro refuses the
  2nd table-keyed arg even when the two tables are statically distinct (no
  alias analysis to prove distinctness), not just the same-table case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
borisbat and others added 10 commits May 25, 2026 22:11
…R B)

Copilot R8 flagged a real perf regression: pre-PR-A imperative
plan_reverse / plan_distinct accepted N consecutive _where calls and
merged predicates via merge_where_cond (decs mirror plan_decs_reverse
still does). PR A's pattern rows allow a single optional where_ slot,
so chains like ..._where(p1)._where(p2).reverse()... no longer splice
and cascade.

Correctness unchanged (cascade works); medium severity (uncommon
shape — users typically write _where(p1 && p2) directly). Fix is
well-scoped: collapse_chained_wheres pre-pass mirroring
collapse_chained_selects (~30 LOC), call from both stubs, no walker /
emit / pattern-row changes.

Added KR-1 to a new "Known regressions to address in follow-ups"
section in linq_fold.md with the fix sketch and owner PR (PR B).
PR description updated to call out the regression explicitly under
the "Behavior near-unchanged" claim.

No code change — defer is the agreed action with the user.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- macro_verify call.arguments[i]._type is non-null before clone_type
  (defensive guard; in practice the typer fills it whenever subexpr._type
  is filled, but the cheap check rules out null deref).

- Stamp rewrittenInner.returnType to void in the explicit-params branch
  too. The default-_ branch already constructed newBlock with void; the
  else branch was inheriting whatever returnType the user-block had. Now
  both shapes consistently reject `return value` inside the body via
  typer rather than silently accepting it.

- Use make_unique_private_name(...) for the per-container locals
  (__with_c_<i>, __with_tref_<i>) so name collision with a user var of
  the same name is impossible. Matches the hygiene convention from
  assert_once / async_boost / coroutines / match.

- test_with_lock_panics.das: add `options no_unused_block_arguments = false`
  since the $(a) / $(v) / $(va, vb) block params exist for syntax coverage
  but the bodies only exercise the lock-vs-mutation path on the container.
  Also retire the stale "(daslang issue #2532)" reference in the file
  docstring — #2532 was closed not-planned because finally-skipping-panic
  is by design (panic-is-fatal policy from CLAUDE.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-followup

audio: HRTF budget + simulated-3D mixing, plus #2872 features + review fixes
…atterns-foundation

linq_fold: PR A — pattern-table refactor foundation + migrate plan_reverse + plan_distinct
…ates

Foundation for the next round of pattern-table migrations. Standalone-shippable
even before any plan_*  body actually uses the new aliases/predicates: the
collapse_chained_wheres helper alone closes KR-1 (the PR A regression on
`..._where(p1)._where(p2)...` chains).

Changes:

- New helper `collapse_chained_wheres` (~60 LOC, mirror of `collapse_chained_selects`
  shape but composes via `merge_where_cond` instead of function compose).
  Wired into `plan_reverse` and `plan_distinct` stubs alongside the existing
  `collapse_chained_selects` call. No has_sideeffects bail needed — composition
  doesn't duplicate either predicate body (each runs at most once per element,
  same as the imperative chain with short-circuit `&&`). **Closes KR-1.**

- New aliases for upcoming `plan_loop_or_count` + `plan_order_family` rows:
  `order_family`, `range_op_family`, `accum_family`, `early_exit_family`,
  `loop_terminator_family`. Per the masterplan, aliases land when first
  consumer needs them — these all get a consumer in the PR B1 / B2 migrations.

- New predicates:
  - `inline_cmp_available` — true only for `order_by[_descending]` with an
    inline-splice-able key lambda (consumed by `order_streaming_min` /
    `order_bounded_heap` rows in PR B2)
  - `has_where_or_distinct` — disambiguates `order_fused_prefilter` row from
    bare `order_buffer_helper_dispatch` (consumed by PR B2)

- PR B sketch in `linq_fold.md` carrying row shapes / emit archetypes / LOC
  budget / co-occurrence audit notes. Status section split: PR B1 (this PR)
  closes KR-1 + foundation + plan_loop_or_count; PR B2 follow-up handles
  plan_order_family in isolation.

Tests:

- NEW `tests/linq/test_linq_fold_collapse_chained_wheres.das` (10 tests /
  18 sub-runs): N=2 and N=3 chains on both `plan_reverse` and `plan_distinct`
  surfaces; edge cases (single where unchanged, wheres separated by select
  don't collapse).

Test plan:
- compile_check + lint clean
- All 18 KR-1 regression tests pass
- All PR A tests still green: pattern_walker 10/10, iterator_wrap 34/34,
  terminal_select 28/28, theme8_fusion_arms 36/36

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…1ab4

daslib/with_boost: with_ macro (inline-emit, any-arity, mixed array+table)
Kernel extensions enabling variable-shape chain heads in pattern rows:

- `Captures` is now a wrapper struct `{ single, many }`. `single` holds
  c_one/c_opt captures; `many` holds c_chain captures (`array<ExprCall?>`).
  Mechanical migration of ~47 PR A access sites: `c["x"]` → `c.single["x"]`.

- New `SlotCardinality.chain` arm + `c_chain()` constructor + `slot_chain_of(
  names, cap)` convenience helper. Walker gained a greedy match-while-in-set
  branch; empty match still creates the `many` entry so emit fns can rely on
  `c.many |> key_exists("…")`. Private `slot_matches_call` extracted from the
  walker, reused by both scalar and chain arms. `slots_structurally_match`
  lint helper extended to compare all 3 cardinality arms.

- `emit_array_lane` signature refactored: `var expr : Expression?` → `isIter :
  bool`. The only use of `expr` was reading `_type.isIterator`; the new emit
  fn passes `ctx.expr_is_iterator` directly.

Migration of `plan_loop_or_count`:

- Imperative ~210-LOC body deleted. New stub: flatten → collapse_chained_*
  pre-passes → match_pattern → invoke emit. Single pattern row with c_chain
  head matching `["where_", "select"]` greedy, then canonical-order positional
  slots (skip / skip_while / take_while / take / post_take_where / term).

- `emit_loop_or_count_lane` (~180 LOC, verbatim lift) walks `c.many["head"]`
  applying the same where_/select arm logic (AND-merge, chained-select
  rebinding, where-after-select projection-replace) the imperative loop did.
  Range ops + post-take-where + terminator read from `c.single[…]`. Fast paths
  (length / any-empty) and lane dispatch (counter / array / accumulator /
  early-exit) unchanged.

- `loop_terminator_family` alias gained the 6 missing EARLY_EXIT terminators
  (`last` / `single` / `element_at` × `_or_default`). First cut missed them;
  matrix run caught it via `test_linq_fold_ast` "expected 1 for-loop, got 0"
  failures (terminator wasn't matching → planner cascaded to tier-2).

KR-1 closure: `collapse_chained_wheres` (shipped in foundation commit) is now
wired into all 3 stubs (plan_reverse, plan_distinct, plan_loop_or_count).

Tests:

- `tests/linq/test_linq_fold_pattern_walker.das` — 3 new c_chain tests
  (constructor shape, lint helper distinguishes c_chain, reachable-table
  accepts c_chain heads). Semantic c_chain coverage is end-to-end via
  test_linq_fold_loop_or_count.
- `tests/linq/test_linq_fold_loop_or_count.das` (new) — 11 tests / 22 sub-runs
  covering canonical 4 lanes, where-after-select rebinding, multiple wheres
  post-select, range chains, post-take-where, length / any-empty fast paths.

Validation: full linq matrix (26 files, 1467 sub-runs) + decs matrix (25
files, 235 sub-runs) green. Lint clean.

Masterplan updated: PR B1 marked complete; PR B2 (`plan_order_family`) stays
deferred (foundation it needs already shipped in B1). Kernel snippet, naming
table, walker contract, alias table, predicate library, decision log, and
KR-1 row all reflect what shipped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pre-push hook caught spacing on `@@<EmitFn>` literals; the formatter
canonicalizes to `@@ < EmitFn >`. Mechanical fix; no code change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pre-push lint caught var→let opportunity on the structural-match
test arrays. The arrays are passed by-reference to chain_prefix_of
and never reassigned.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…attern-table-prb

linq_fold PR B1: c_chain cardinality + plan_loop_or_count migration
@pull pull Bot locked and limited conversation to collaborators May 26, 2026
@pull pull Bot added the ⤵️ pull label May 26, 2026
@pull pull Bot merged commit fbaa188 into forksnd:master May 26, 2026
@pull pull Bot had a problem deploying to github-pages May 26, 2026 08:58 Error
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants