Skip to content

[pull] master from GaijinEntertainment:master#1011

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

[pull] master from GaijinEntertainment:master#1011
pull[bot] merged 30 commits into
forksnd:masterfrom
GaijinEntertainment:master

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented May 19, 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 : )

borisbat and others added 30 commits May 18, 2026 19:44
…h bench suite

* New daslang_insert_only_hash_{map,set} in include/das_hash_map/das_hash_map.h:
  strict subset of the regular API (no erase, no tombstones, no rehash_same_capacity).
  reserve_slot drops the HASH_KILLED branch + insertI tracking; iterator skip is
  "== HASH_EMPTY" instead of "<= HASH_KILLED". Same layout, same load factor cap,
  same find_index. Same hashing.

* Switch 8 Module class fields in include/daScript/ast/ast.h that are never
  erased anywhere in the codebase (audited via grep across src/ and modules/):
    handleTypes, callThis, typeInfoMacros, annotationData, requireModule,
    typeMacros, readMacros, options.
  Type signal that these tables are grow-only by design.

* include/daScript/das_config.h: add das_insert_only_{hash_,}{map,set} aliases.
  Graceful std::unordered_* fallback under DAS_CUSTOM_HASH=0 (API superset).

* include/daScript/misc/das_common.h: ordered() overloads for the new types.
* include/daScript/ast/ast_serializer.h + .cpp: AstSerializer operator<< +
  serialize_hash_map overloads for the insert-only map.

* examples/hash/: standalone bench suite (modeled on examples/sort/) with
  three executables - main matrix (std vs das vs absl, 5 key shapes,
  insert/churn/find x10, 270 cells); hash function vs table mechanics
  isolation (2x2 of {das_hash_map, absl::flat_hash_map} x {daslang_hash,
  absl::Hash}); insert-only vs regular comparison on find x10.

Tests: 8422 dastest passes, 7811 AOT tests pass.

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

Addresses Copilot review on PR #2722.

Under DAS_CUSTOM_HASH=0, das_insert_only_hash_{map,set} alias to the same
std::unordered_{map,set} as das_hash_{map,set}, so the new ordered() and
AstSerializer overloads collide with the existing ones. Wrap the insert-only
definitions/declarations with #if DAS_CUSTOM_HASH at 3 sites:

  * include/daScript/misc/das_common.h  - ordered() map+set overloads
  * include/daScript/ast/ast_serializer.h - serialize_hash_map / operator<<
  * src/builtin/module_builtin_ast_serialize.cpp - corresponding definitions

In fallback mode the existing das_hash_map overloads already serve insert-only
arguments because the underlying type is identical.

Bump AstSerializer::getVersion 83 -> 84 since Module::serialize now uses
das_insert_only_hash_map for annotationData and requireModule; old archives
must not deserialize into the new fields.

Also drop the misleading "Does NOT need Abseil" comment from
examples/hash/CMakeLists.txt - FetchContent_MakeAvailable(absl) runs at
configure time regardless of which bench target is built.

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

das_hash_map: insert-only variants + Module field switch + hash bench suite
Captures three patterns surfaced during PR #2721 review:

- group-by-first-key-wins-single-hash-tab-dummy-addr-compare: the
  `tab?[uk] ?? dummy` + addr-compare idiom that brought groupby_count
  76→37 ns/op. Documents per-element hash-op count + miss-path commit
  semantics + AST-test fingerprint (`key_exists==0`, `unique_key>=1`).
- streaming-dedup-take-guard-outer-loop-not-fresh-key-branch: where to
  place the take(N) break-guard in distinct+take splice (outer loop,
  not inside fresh-key branch); explains the adversarial-input gap the
  bench couldn't catch.
- linq-bench-eager-vs-lazy-distinct-arr-vs-each-arr: why `arr|>distinct`
  and `each(arr)|>distinct` produce wildly different bench numbers —
  array-shape distinct is eager, iterator-shape is lazy. Required
  source-shape match across m1/m3/m3f lanes for apples-to-apples.

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

Two interlocking changes in `plan_group_by`:

1. **Miss/hit emission restructure** (no behavior change for existing arms).
   The per-element splice splits on `addr(entry) == addr(dummy)`: on miss,
   run `missInit` (acc starts at the first-element value); on hit, run
   `hitUpdate` (incremental update). Observationally equivalent for the
   pre-existing count/length/long_count/sum arms — same final state — but
   the per-branch split lets each reducer pick its own per-side shape
   independently. Unlocks A.2 below and the future min/max/first arms
   queued for the next PR.

   Existing G2/G3/G8 AST tests stay green with a new
   `count_call(body, "key_exists") == 0` assertion pinning the single-hash
   hot path from PR #2721 (the `tab?[uk] ?? dummy` + addr-compare idiom).

2. **Inner-select-sum recognizer**. `is_bucket_reducer_call` extended to
   match `sum(select(<bind>._1, <lambda>))` — the AST shape after the typer
   resolves `_._1 |> select(<lambda>) |> sum()`. On match, plan_group_by
   peels the inner lambda via `fold_linq_cond(innerLambda, itName)` and
   splices the body directly into `entry._1 += <body>` (hit) /
   `entry._1 = <body>` (miss). Accumulator type derived from the OUTER
   sum call's `_type` (the inner projection's typer-resolved result type) —
   the peeled body's `_type` is null because apply_template doesn't
   re-type-infer. Both bare body `_._1|>select|>sum` and named-tuple wrap
   `(K=_._0, S=_._1|>select|>sum)` supported.

**Headline (100K rows, INTERP):**

| Benchmark | m1 sql | m3 linq | m3f prev | m3f this PR | Win |
|---|---:|---:|---:|---:|---|
| groupby_sum  | 174 | 101 | 108 | **36** | 3× over prev, 2.8× over m3, 4.8× over SQL |
| groupby_count (regression check) | 142 | 71 | 37 | **36** | parity — A.1 restructure preserves the count splice |

groupby_sum now matches groupby_count's ~36 ns/op: same number of allocs
(1, the result array), same per-element work (single hash + entry._1
mutation), no per-bucket array materialization.

**Deferred to follow-up PRs** (per `~/.claude/plans/modular-stirring-tide.md`):

- PR-A2: bare `_._1|>min/max/first` + inner-select-min/max/first; multi-
  reducer named tuples like `(K=..., N=..|>length, S=..|>sum, M=..|>min)`
- Separate follow-up: `average` reducer (needs 2-slot per-key acc +
  post-process division)

**Test coverage added:**

- tests/linq/test_linq_fold.das `test_group_by_inner_select_sum_fold_parity`
  with 4 subtests: bare body, named-tuple wrap, empty source, reference
  parity vs plain LINQ chain
- tests/linq/test_linq_fold_ast.das G4 bare + G4b named-tuple AST tests
  asserting splice fired (no `group_by_lazy` / `select` calls in body,
  2 for-loops, single-hash fingerprint)

239 + 97 tests pass in both interp and AOT modes; lint + format clean.

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

linq_fold group_by: inner-select-sum splice — closes #2721 deferred follow-up
Generalizes plan_group_by along two axes:

A.3 — per-reducer dispatch. `is_bucket_reducer_call` extended to 8 reducer
shapes: bare {sum, min, max, first} and inner-select {sum, min, max, first}
(`<reducer>(select(<bind>._1, <lambda>))`). New `emit_reducer_branches`
helper produces per-reducer (missInit, hitUpdate) pairs:

- min/max: direct `<` / `>` on workhorse acc types; `_::less` fallback for
  non-workhorse (matches the reference at linq.das:1224,1308).
- first: miss-init only (hitUpdate null). Subsequent same-key elements are
  ignored, exploiting the first-key-wins guarantee from PR #2721.
- inner-select min/max: bind the projection result to a per-element temp so
  the inner body evaluates exactly once per source element — matches the
  reference's `select` laziness, and avoids re-running side effects.
- first_or_default is rejected at the arity check (2 args) and cascades.

A.4 — N+1-slot named tuples. The 2-slot recognizer (key + 1 reducer) is
replaced by `recognize_reducer_specs` returning `array<ReducerSpec>`. The
named-tuple form now accepts arbitrarily many reducer slots (key at _0,
reducers at _1.._N). The planner walks the spec array once and concatenates
per-slot missInit + hitUpdate statements into the per-element loop —
N reducers fused into one pass instead of N separate passes.

Field-access into dynamic slots (`entry._{slot}`) is built programmatically
via `mk_slot_ref` since qmacro has no dynamic-field-name splice. The
loop-body emission is now conditional on whether any reducer contributes a
hit update — first-only chains drop the else branch entirely.

Bail paths (all cascade to tier 2):
- key not at slot 0 of the named tuple
- unrecognized reducer in any slot (e.g. `average`)
- first_or_default (2-arg, fails arity)

Tests:
- 18 new parity tests in test_group_by_min_max_first_fold_parity and
  test_group_by_multi_reducer_fold_parity (bare, named, inner-select,
  multi-reducer, empty source, reference parity)
- 5 new AST-shape tests: G5a (min workhorse direct compare), G5b (min
  non-workhorse `_::less`), G6 (first no hit compare), G7 (multi-reducer
  fused pass), G10 (first_or_default cascade)

Both PR-A1 baselines (groupby_count, groupby_sum) hold parity.

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

4 new 100K-row benchmarks (m1 SQL / m3 plain LINQ / m3f splice):
- groupby_min: 175 / 111 / 42 ns/op  (2.6× over m3, 4.2× over SQL)
- groupby_max: 173 / 108 / 43 ns/op  (2.5× over m3, 4.0× over SQL)
- groupby_first: — / 71 / 36 ns/op   (2.0× over m3; no direct SQL aggregator)
- groupby_multi_reducer: 189 / 139 / 53 ns/op  (3 reducers fused into 1 pass;
  2.6× over m3, 3.6× over SQL)

All 6 splice variants land within ~36–53 ns/op — per-element work bounded by
the single hash op + slot mutations regardless of which reducer or how many.
Multi-reducer pays ~5 ns per extra slot, still beats SQL by 3.6×.

LINQ.md: refreshed Phase status table, baseline rows, Phase 3+ subsection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…in-max-first-multi-reducer

linq_fold group_by: min/max/first reducers + multi-reducer fused pass
Closes the explicit deferred-bail from PR #2721 onward: any upstream call
between source and group_by_lazy used to cascade to tier 2. PR-B walks
the upstream segment and fuses chains of where_*/select* into the
per-element table-update loop.

Walk:
- For each `select`: bind the previous projection (if any) to a fresh
  `var v_N := projection_{N-1}` intermediate, then compute the new
  projection peeled to reference the latest bind name. Update elemType
  to the post-projection type so the table value-type witness matches
  what the key block will receive.
- For each `where_`: bind any pending projection first (so the predicate
  references a stable name — no double-eval), then AND-merge the peeled
  predicate into whereCond.
- Anything else (distinct, order_by, etc.): bail.

Final bind: if a projection is still unbound after the walk, bind it
now so itName (the post-projection name) is stable. The inner-select
reducer recognizer (PR-A1/A2) folds its inner lambda body against this
itName — without the bind, the recognizer would dangle on a name the
typer hasn't seen.

Per-element emission split into core + wrapping. The 3-stmt core
(`let k = invoke(key, it); let uk = unique_key(k); unsafe { table
update }`) is wrapped with `if (whereCond) { core }`, then prepended
with intermediateBinds, and spliced into the for-loop body via
$b(bodyStmts). The for-loop bind name is now `srcItName` (was
`itName`); `itName` aliases the post-projection name (== srcItName
when no upstream selects).

Tests:
- 8 new parity tests in test_group_by_upstream_fusion_fold_parity:
  where-only, select-only, where→select, select→where, chained selects,
  Person-fixture parity, empty result, count terminator.
- 4 new AST-shape tests: G11 (upstream where, no runtime where_ call),
  G12 (upstream select, no runtime select call), G13 (combined
  where+select), distinct-upstream cascade fingerprint.

All PR-A1/PR-A2 baselines (groupby_count, groupby_sum, groupby_min,
groupby_max, groupby_first, groupby_multi_reducer) hold parity — the
per-element-body split into core + wrapping is observationally
equivalent when intermediateBinds is empty and whereCond is null.

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

3 new 100K-row benchmarks (m1 SQL / m3 plain LINQ / m3f splice):
- groupby_where_count:  75 / 65 / 23 ns/op  (2.8× over m3, 3.3× over SQL)
- groupby_where_sum:    86 / 80 / 23 ns/op  (3.5× over m3, 3.7× over SQL)
- groupby_select_sum:    — / 110 / 58 ns/op (1.9× over m3; m1 omitted —
   _sql LINQ-to-SQL requires `_group_by(_.Field)`, no expression keys)

The where-fused benchmarks drop to ~23 ns/op — faster than the
no-upstream groupby_count (36 ns/op) because the where filter halves
the table-update count (only ~50% pass `price > 500`). Fewer hash ops
× same per-survivor cost = lower per-element averaged time.

LINQ.md: refreshed Phase status table, baseline rows, Phase 3+ subsection.

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

Fix correctness bug in PR-B's per-element emission: the initial walk
collected all binds into a flat intermediateBinds list and a single
AND-merged whereCond, then emitted `binds...; if (whereCond) { core }`.
That executes EVERY select before any where guard, so a chain like:

    _where(_ != 0)._select(10 / _)._group_by(...)

would run `10 / _` for ALL source elements, including `_ == 0` (divide
by zero). Reference LINQ evaluates `select` lazily per element — so a
select that follows a where must only run on where-survivors.

Fix: segment-based walk + inside-out emission. The walk produces
(binds, trailing_where) pairs — a `where` closes the current segment
with itself as trailing_where; a `select` that follows a where starts
a new segment (its bind is emitted INSIDE the where's guard).
Consecutive wheres with no intervening select AND-merge into the same
segment's trailing_where. Final emission walks segments in reverse,
wrapping the core body inside-out so each where's `if { ... }` brackets
exactly the binds + nested guards that follow it.

Regression tests added FIRST (per feedback_correctness_bug_add_test_first):
- where → select with side-effect counter: asserts the projection fires
  EXACTLY once per where-survivor (5), not once per source element (10).
- select → where → select: asserts only the FIRST select runs
  unconditionally; the SECOND fires only on the where's survivors.

Both tests failed on the broken implementation (got 10, expected 5) and
pass after the fix. All 269 parity + 106 AST + 18 group_by tests stay
green; all 6 PR-A1/PR-A2/PR-B benchmarks unchanged — the lazy emission
has zero perf cost (same hash ops + slot mutations per survivor).

LINQ.md: refreshed the BufferGroupBy core row (Copilot caught the
inconsistency with PR-A2/PR-B's new rows below it — the core row still
claimed upstream where/select bailed, contradicting subsequent rows).
Single current status for the phase-table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves the "two daslang trees can't both run daslang-live" interference
that the parallel-checkout plan surfaced. After this:

  daslang-live my.das --live-port 19090     # binary form
  daslang script.das -- --live-port 19090   # scripts using live_api

coexist with another instance on the default 9090.

Port precedence (highest first), unified across C++ binary and .das module
via an env-var roundtrip so the single-instance lock and the HTTP bind
can never disagree:

  1. live_api_set_port(p)             (programmatic, always wins)
  2. script argv --live-port N        (find_flag_raw_value, full argv)
  3. env DASLANG_LIVE_PORT            (daslang-live re-exports its
                                       resolved port into this env)
  4. get_das_root() /daslang-live.cfg.json  "port" key
  5. default 9090

C++ daslang-live binary parses --live-port pre-doubledash, writes the
resolved port back into DASLANG_LIVE_PORT, and keys acquire_single_instance()
on that port (Windows: daslang-live-single-instance-<port> mutex; POSIX:
/tmp/daslang-live-<port>.lock).

MCP do_live_launch now forwards its port arg to the spawned binary --
fixes the deferred-test root cause documented in the mouse-card
daslang-live-no-port-flag-single-instance-lock-e2e-spawn-tests-infeasible.md
(now marked RESOLVED).

Tests cover parse_port_string boundaries, --live-port and --live-port=N
forms, JSON config parsing, precedence chains, and the
live_api_set_port-after-init smoke. 78/78 pass interpreted.

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

Per Copilot review on #2726:

- parse_port_strict() in daslang-live binary replaces atoi for both --live-port
  arg and DASLANG_LIVE_PORT env. atoi silently accepted '9090abc' -> 9090; the
  new parser rejects trailing garbage to match the .das side's try_to_int.
- Accept --live-port=N alongside --live-port N for parity with
  find_flag_raw_value on the .das side.
- do_live_launch now rejects non-digit / over-5-char port at entry so a
  caller-supplied port cannot smuggle quotes / spaces / metacharacters into
  the shell-ready argv string passed to system().

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

linq_fold group_by: fuse upstream where_/select* into per-element loop
CI darwin Release failure (run 26078968013): test_aot reported
error[50101] AOT link failed on every live_api function referenced
by tests/live_host/test_port_override.das (the first AOT test to
require live/live_api).

Root cause: live_api.das was missing from AOT_LIVE_HOST_MODULE_FILES
in tests/aot/CMakeLists.txt. Without an AOT-cpp build of live_api
itself, the test_port_override.das.cpp recorded hashes that disagreed
with what test_aot.exe computed at simulate time -> link miss on
live_api_set_port / parse_port_string / parse_argv_port / etc.

Pre-existing live_host tests only require live_host (the C++ DLL
bridge), so the gap was invisible until now.

Fix: add modules/dasLiveHost/live/live_api.das to the list, same
spot as live_api_builtins / live_api_stdio. Local reconfigure
generates test_aot_live_host_modules_live_api.das.cpp under
modules/dasLiveHost/live/_aot_generated/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
plan_group_by now accepts an optional `_having(pred)` between `_group_by_lazy(k)` and
`_select(group_proj)`. The predicate is peeled and rewritten against the per-key
accumulator slots: `<hb>._0` → `kv._0`, `<reducer>(<hb>._1)` /
`<reducer>(select(<hb>._1, <lam>))` → `kv._{spec.slot}` when the reducer name matches an
existing select-side spec. The rewritten predicate wraps `push_clone(outputExpr)` in the
result-build loop (and increments a counter loop in the `count` terminator lane). Per-
element table-update is untouched — the filter runs once per distinct key.

Bails to tier 2 when the predicate references a reducer with no matching select slot, or
touches the bucket value in any non-reducer shape (raw `_`, raw `_._1`, unrecognized
field). Both cases stay correct via the cascade.

Closes the SQL `HAVING` shape against the splice path. Headline benchmark
`groupby_having_count` lands at 36 ns/op — 2.2× over m3 plain LINQ, 3.9× over m1 SQL —
matching the no-having `groupby_count` since the per-element work is identical.

Tests: 8 parity subtests + 1 cascade subtest in test_linq_fold.das, 4 AST-shape tests in
test_linq_fold_ast.das (length-match splices, count-terminator splices, unmatched-reducer
cascades, raw-bucket cascades). All 390 linq_fold tests green in interpreter + AOT.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rewrite_having_pred only walks ExprCall/ExprField/ExprVar/ExprOp1-3 (plus the
ExprRef2Value peel). For any other node type — `ExprAt`, `ExprInvoke` from a non-
peelable lambda, `ExprIfThenElse`, etc. — it falls through to `return expr` unchanged.
If that subtree contains references to the synthetic `hb` bind, those references leak
into the result-build splice and the typer fails with `can't locate variable '\`hb\`…'`.

Adds `HbScanner : AstVisitor` (modeled on `RemoveDerefVisitor` in templates_boost.das)
and a thin `expr_uses_var(e, name)` helper. plan_group_by calls it after
rewrite_having_pred — if any ExprVar named `hbName` survived the rewrite, bail and let
the chain cascade to tier 2 (which is always correct via the array-shape pipeline).

Failing-test-first per the correctness-bug-rule:
test_group_by_having_unhandled_node_cascades uses `_._1[0] == 1` (ExprAt indexing into
the bucket array) — without the scan the compile-time error from the previous run was
`can't locate variable '\`hb\`0x829\`0x49'`. With the scan, the chain cascades and
produces the correct filtered result.

Closes Copilot review comment on PR #2727.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Boris's direction on PR #2726 R2: keep daslang-live stateless --
drop the env-var roundtrip and the config-file fallback. Both sides
(C++ daslang-live + .das live_api) now scan the same source (full argv,
including post-doubledash) for --live-port, so the lock key and the
HTTP bind cannot drift.

Correctness bugs fixed (each with a repro test per the
"correctness-bug -> repro test" rule):

- Thread 4: MCP do_live_launch's port_is_safe accepted "0", "65536",
  "99999" -- daslang-live would error after 10s of polling. Renamed to
  port_in_range and now actually parses + range-checks [1, 65535].
- Thread 5: C++ arg loop stopped at doubledash while .das scanned full
  argv, so `daslang-live foo.das -- --live-port 19090` would bind 19090
  on the .das side but key the C++ lock on 9090. New
  find_live_port_in_argv() scans the full argv. parse_argv_port test
  pins the post-doubledash case on the .das side.
- Thread 8: env-roundtrip masked the config-file fallthrough. Resolved
  by removing both -- no more env, no more config file.

Doc-only (Threads 6, 7): live_api_set_port "always wins after init" was
misleading. Server binds at LiveApiAgent constructor time and does not
rebind. Reworded the docstring on live_api_set_port and the [init]
function comment to say "before agent constructs".

RST audit (Boris's "MCP server and live documentation might be out of
sync" request):
- mcp.rst: project_root was referenced 18+ times in protocol.das but
  documented 0 times. Added a Common parameters subsection listing
  project + project_root once for all tools that accept them.
- mcp.rst: Live-reload control table rewritten with per-tool arg
  surface (file/project/project_root/port on live_launch; full on
  live_reload; paused on live_pause; name/args on live_command;
  commands on live_commands).
- daslang_live.rst: CLI reference filled in to match main.cpp
  (-project_root, -v1syntax, -track-allocations, -heap-report,
  --no-dyn-modules, --no-dump-leaks, --live-port, -h/--help).
- daslang_live.rst: single-instance lock note now mentions port-keying
  (daslang-live-single-instance-<port> / /tmp/daslang-live-<port>.lock).
- daslang_live.rst: REST API section documents the 3-level precedence
  chain (programmatic > argv > default) and the "before constructor"
  caveat on live_api_set_port.

skills/daslang_live.md updated to match the simplified chain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Copilot review on PR #2726 R3:

- Thread 9 (real correctness bug): C++ find_live_port_in_argv kept
  'last VALID', while .das clargs.find_flag_raw_value keeps 'last RAW
  (validated to 0 on bad input)'. A tail like '--live-port 19090
  --live-port abc' would have C++ key its lock on 19090 while .das
  defaulted to 9090 -- lock-vs-bind drift. Dropped the if(p>0) guard
  so the C++ scan is also last-occurrence-wins-unconditional. Repro
  test added per the correctness-bug rule: parse_argv_port treating
  invalid trailing values as 0.

- Thread 10 (doc fix): g_resolvedLivePort comment still mentioned a
  'DASLANG_LIVE_PORT env handoff' that was removed in cf58023.
  Reworded.

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

linq_fold group_by: splice _having between group_by_lazy and select
…ducer

Extends plan_group_by to splice the `average` reducer (and `inner-select-average`)
on top of PR-A1's miss/hit emission shape + PR-A2's N+1-slot reducer-spec walker.

Each average slot holds a `tuple<double; uint64>` (running sum + element count)
internally; result-build divides at output. The user-facing return type (`double`
for bare form, `tuple<...; Avg : double; ...>` for named form) is preserved so
the buffer's declared shape matches the user's chain.

- `is_bucket_reducer_call` recognizes `average` in both bare and inner-select arms.
- `slot_acc_type` overrides the slot type to `tuple<double; uint64>` for average.
- `emit_reducer_branches` emits `entry._{slot} = (double(it), 1ul)` on miss and
  `entry._{slot}._0 += double(it); entry._{slot}._1 ++` on hit; the inner-select
  variant evaluates the inner projection once into a `let vavg`-bound temp.
- `plan_group_by` scans for average slots; when present, the named-tuple
  tabValueType clones the user's body type and substitutes only the average
  slots, and the result-build output is a programmatically-constructed
  `ExprMakeTuple` with per-slot divide for average slots.
- `rewrite_having_pred` routes average-slot references through a shared
  `mk_slot_output_expr` so `_._1 |> average > 50.0` in a having predicate works
  the same way as `_._1 |> sum > 50` does.

Bucket non-emptiness is structurally guaranteed (a key only enters the table
when at least one source element produced it), so `count` is always ≥1 — no
divide-by-zero guard needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
groupby_average headline at 100K INTERP: m1=173 / m3=106 / m3f=52 ns/op
(2.0× over m3, 3.3× over m1 SQL). Regression checks against groupby_count,
groupby_sum, groupby_min, and groupby_multi_reducer stay at their PR-A1/A2
baselines (36/36/43/52 ns/op).

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

For (_._0, _._1|>average) — positional tuple, no field names — the recognizer
still accepts the body since it only checks ExprMakeTuple shape + bind._0 at
slot 0. But the avg+named result-build synthesis was deriving slotCount from
argNames, which is empty for positional tuples → loop didn't iterate and the
recordNames[0] write hit an empty array, crashing the splice at macro time.

Fix: derive slotCount from argTypes (populated for any tuple shape); only fill
recordNames when argNames is populated (preserves positional shape on output).

Per the correctness-bug rule: a failing parity subtest for the positional form
is added first — without the fix it breaks the whole file compile, with the
fix it passes.

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

daslang-live: --live-port flag + port-keyed lock + .das resolution chain
…roupby-average

linq_fold group_by: average reducer splice (2-slot acc + post-process divide)
…y sources

Closes the documented reverse_take regression (m3f 34 vs m3 22 ns/op at
TAKE_N=10 / N=100K). The R1-R4 path pushed every source element into a buffer
then did O(length) reverse_inplace + resize(N) — wasting work proportional to
length - N.

Adds an R6 arm in plan_reverse taken when:
- take(N) is present at chain tail,
- no upstream where_,
- no upstream select,
- source type is isGoodArrayType || isArray (need O(1) indexing).

R6 emission walks indices [len-1 .. max(0, len-N)) directly into the buffer:

    let __len = length(src)
    let __takeN = N <= 0 ? 0 : (N < __len ? N : __len)
    var buf : array<T>
    buf |> reserve(__takeN)
    for (k in 0 .. __takeN) {
        buf |> push_clone(src[__len - 1 - k])
    }
    return buf

Take-bound clamp preserves take(N<=0) → empty and take(N>length) → full
semantics. Iterator sources fall through to R1-R4 (verified by a new negative
AST test).

m3f drops 34 → 0 ns/op (sub-nanosecond per element averaged over the 10
visited indices regardless of N) — beats m3's lazy-generator approach too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the reverse_take footnote, refreshes the operator-coverage row,
drops the deferred entry, and adds a Phase 3+ PR-C section with the
backward index loop emission shape + bail conditions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three doc/test improvements from the review:

1. Phase 3+ headline table — add a follow-up row `reverse_take (PR-C,
   backward index loop)` showing m3f=0, following the same convention used
   for `groupby_sum (PR-A1, inner-select-sum)` at line 378. The original
   regression row stays as historical record.

2. plan_reverse terminator table — split the `take(N) |> to_array` row
   into two rows distinguishing the R6 array-source path (backward index
   loop) from the R1-R4 iterator/has-filter/has-select fallback path
   (push + reverse_inplace + resize). Avoids conflicting guidance between
   the early reference table and the dedicated PR-C section.

3. `test_reverse_take_iterator_source_uses_reverse_inplace` — reinforce
   the negative test with the same shape pins the old positive test had:
   `count_inner_for_loops == 1`, `count_outer_let_vars == 1`,
   `count_call("take") == 0`. Catches drift in both directions: R6
   mis-firing on iterator sources OR R1-R4 emission shape changing.

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

linq_fold reverse: backward index loop for reverse |&gt; take(N) on array sources (PR-C)
@pull pull Bot locked and limited conversation to collaborators May 19, 2026
@pull pull Bot added the ⤵️ pull label May 19, 2026
@pull pull Bot merged commit 832b719 into forksnd:master May 19, 2026
@pull pull Bot had a problem deploying to github-pages May 19, 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.

1 participant