Repository navigation
Feature: threads-complete #575
Copy link
Copy link
Open
Labels
Description
Tracking issue for the /feature run that takes nish/threads to completion. This issue is the run's ledger; each slice's PR links here.
Run
- repo: amritk/nish
- base: main @ 35f8b62
- plan: branch
ccr-23558892-oof1r5→.claude/plans/threads-complete.plan.md - requirements: "let's take multi threading to completion, ensure it's the easiest to use/understand but still the most powerful/performant of all the languages"
- flags: max-slices 4, max-rounds 10, deadline 48h, merge on
- verification: test
npm test(undegraded) · coverage: none configured; gate is the repo's definition of done · lintnpm run lint && npm run lint:dead· typesnpm run check·src/stages alsoWASI_SYSROOT=/usr node tests/run.js wasiandnode tests/run.js performance· benchnode bench/run.mjs --only par(the plan'snode bench/run.mjs paris wrong) - bounds: deadline 2026-10-13T17:52Z · no-branch 45m · no-commit 90m · re-runs 1 per check per head · check-ins 15→30→60m on no drift
- acceptance: no browser surface; the acceptance session runs the criteria as commands, natively and under Node
Acceptance
| # | criterion | static | qa | state |
|---|---|---|---|---|
| 1 | parallelMap(xs, (x) => x * k) with a const local k compiles, prints the same natively and under Node, and is ≥3x the sequential loop on 4 cores |
— | — | pending |
| 2 | bench/par-regions (2^17-element map ×1,000) is faster on merged main than on base main beyond the noise band, and faster than its sequential loop |
— | — | pending |
| 3 | NISH_NUM_THREADS=1 and taskset -c 0 each run a parallel program on one thread with unchanged output |
— | — | pending |
| 4 | s.spawn(() => work(a)) with void entries compiles and matches Node |
— | — | pending |
| 5 | #564's repro is refused, #567's repro is accepted, and a task filling its own new Array<i32>(n) compiles |
— | — | pending |
| 6 | A per-value AtomicI32 histogram over 4 tasks prints Node's checksum and beats the per-value-lock version |
— | — | pending |
| 7 | A task returning a string, a parallelMap producing string[], and a Channel<string> pipeline each match Node; a class-valued result is still refused |
— | — | pending |
| 8 | A guard passed to a helper that stores through it compiles and runs | — | — | pending |
| 9 | Each of the ten most common thread refusals ends with code to write instead | — | — | pending |
| 10 | docs/BENCHMARKS.md compares Nish, Rust and Go on ≥4 parallel kernels; Nish within 1.10x of the fastest on each, or the table says plainly where it loses |
— | — | pending |
| 11 | The guide's concurrency page has the which-tool table, and every example on it is compiled by npm test |
— | — | pending |
| 12 | npm run check, undegraded npm test, and the wasi and performance sections are green on main after the last merge |
— | — | pending |
Slices
| stage | owns | model | depends on | session | branch | PR | CI | rounds | state |
|---|---|---|---|---|---|---|---|---|---|
| pool | runtime/runtime-parallel.c, runtime/nish.h, tests/runtime-test.c, tests/run.js, bench/par-regions.ts, bench/run.mjs, docs/BENCHMARKS.md, docs/wp20-threads.md (+ knip.json, regenerated tests/self/goldens/checked.txt) |
lead | — | session_01T1S9vcWYb2sNierTWAQ3Ce |
feature/threads-complete/pool @ c9f58fe |
#586 → a19d5d6 | green | 2/10 | merged |
| soundness | src/{parallel,attributes,escape,codes}.ts, tests/cases/**, tests/self/goldens/**, tests/differential/**, docs/LANGUAGE.md, docs/IR_COOKBOOK.md (+ tests/link/thread_{task_private,channel_ctor_reads}*, docs/guide/using-nish/threads.md, tests/run.js) |
lead | — | session_018kZgaqhSBzj2JbshfNiKe6 |
feature/threads-complete/soundness @ 7c29167 |
#590 | green | 3/10 | fixing |
| perf-ir | src/{emit-parallel,runtime,parallel}.ts, runtime/runtime-parallel.c, runtime/nish.h, tests/**, docs/{LANGUAGE,IR_COOKBOOK,BENCHMARKS,wp29-thread-surface}.md |
lead | pool, soundness | — | feature/threads-complete/perf-ir |
— | — | 0/10 | pending |
| data-forms | std/threads.ts, src/**, tests/**, examples/**, docs/{LANGUAGE,IR_COOKBOOK,wp29-thread-surface}.md |
lead | perf-ir | — | feature/threads-complete/data-forms |
— | — | 0/10 | pending |
| task-forms | std/{threads,random}.ts, std/README.md, src/**, tests/**, examples/**, docs as above |
lead | data-forms | — | feature/threads-complete/task-forms |
— | — | 0/10 | pending |
| atomics | std/threads.ts, src/**, runtime/**, tests/**, examples/**, docs as above |
lead | task-forms | — | feature/threads-complete/atomics |
— | — | 0/10 | pending |
| copy-at-join | std/threads.ts, src/**, runtime/**, tests/**, docs as above |
lead | atomics | — | feature/threads-complete/copy-at-join |
— | — | 0/10 | pending |
| guards-diagnostics | src/**, tests/**, docs as above |
lead | copy-at-join | — | feature/threads-complete/guards-diagnostics |
— | — | 0/10 | pending |
| bench-compare | bench/par-*.{rs,go,ts,c}, bench/run.mjs, bench/README.md, docs/BENCHMARKS.md |
lead | perf-ir | — | feature/threads-complete/bench-compare |
— | — | 0/10 | pending |
| guide | docs/guide/language/concurrency.md, scalar.config.json, docs/AI.md, README.md, docs/wp29-thread-surface.md, docs/MASTER_PLAN.md |
lead | all, #570 foundation | — | feature/threads-complete/guide |
— | — | 0/10 | pending |
| acceptance | — | sonnet | all | — | — | — | — | — | pending |
Review ledgers
#586 (pool) — merged
- round 1 @ 0cf46aa: 4 of 16 confirmed → fixed in c9f58fe. round 2 @ c9f58fe: 0 of 2 → converged.
- rejected: weakened
on_caller/arena asserts; RSS bound; no spawnSync timeout; more-tasks-than-workers cap and stalepool_finished; fork inside a body;pthread_atforkre-registration; fixed-sizecpu_set_t; 512-byte/proc/self/cgroup; wp20 vs BENCHMARKS figures; BENCHMARKS.md regenerated on a new machine; par-short 0.86x; wp20 §8c.1;Cpus_allowed_listvs online count; cgroup rows skipped without user namespaces. - lead decisions:
knip.jsonunshareentry accepted (system binary, not a loosened rule); threaded ceiling 1,536 → 2,816 re-measured; squash message from 49c4819's trailers.
#590 (soundness)
- round 1 @ 96909e8 — 16 findings verified (probes, mutants), 6 confirmed (test strength + doc wording) → fixed in 82aa39c.
- round 2 @ 82aa39c — 3 verified with probes on head and main, 2 confirmed: soundness hole (nested-field write
h.inner.x = 1through a call ornewin a scope region accepted; native 1, Node 0; call form pre-existing on main) and_thisnot isolated → fixed in 7c29167 (writesReach, 8 new negatives). - round 3 @ 7c29167 — 4 verified with probes, 2 confirmed: soundness hole (ctor stores a param's memory into
this—this.inner = h.inner/this.items = h.items— then writes through it;thisexempt as fresh; 4 shapes all native≠Node) and regression (a helper writing only its ownnew Grid()refused in a region since 7c29167; accepted at 82aa39c). Asked to share one fact: does a constructor keep an argument's memory inthis. - rejected (never re-raised): arrow capture of a private array; private array as
spawnargument; ctor keeping the array; nested array of arrays; store into a parameter's slot;foo(this.ch)/const c = this.ch/this.m()/arrow namingthis; non-empty literal in a task;writesOutputpanic-tail; arrays-only rule;superedge;dump_shared_write_calleegolden move;??receiver negative (Map.get's probe is a write);reserve/getOrInsertwrapper (refused); lifted arrow writing through the enclosing function's parameter (arrows capture nothing). - lead decisions: accepted scope additions (
tests/link/thread_task_private*,tests/link/thread_channel_ctor_reads*, guide threads page,tests/run.jsafter perf(runtime): a persistent FIFO worker pool, an affinity-aware core count and NISH_NUM_THREADS #586); pre-existing nested-write gap for calls fixed here rather than split out (fix(checker): a scope's region write check ignores constructor calls (new) #564's rule exposes it throughnew). - disputed: none · skipped: none
Log
- 2026-10-11T17:52Z plan accepted (deadline 48h); ledger opened
- 2026-10-11T17:53–17:54Z spawned pool (
session_01T1S9vcWYb2sNierTWAQ3Ce) and soundness (session_018kZgaqhSBzj2JbshfNiKe6) - 2026-10-11T18:37–20:02Z both workers nudged after idling on background test runs; pool's branch pushed 18:42Z
- 2026-10-11T20:25Z pool opened perf(runtime): a persistent FIFO worker pool, an affinity-aware core count and NISH_NUM_THREADS #586; 20:30Z red (knip, attribution) and conflicted → handed back; 20:58Z round 1 (4 confirmed)
- 2026-10-11T21:20Z soundness opened fix(checker): close the constructor hole in scope regions and admit a task's private writes #590 (Fixes fix(checker): a scope's region write check ignores constructor calls (
new) #564, threads: relax the channel-holding constructor rule (NL2479) to allow reads of this's own fields #567) - 2026-10-11T21:25Z perf(runtime): a persistent FIFO worker pool, an affinity-aware core count and NISH_NUM_THREADS #586 converged in round 2; squash-merged as a19d5d6
- 2026-10-11T22:20Z fix(checker): close the constructor hole in scope regions and admit a task's private writes #590 round 1: 6 of 16 confirmed
- 2026-10-11T22:50Z fix(checker): close the constructor hole in scope regions and admit a task's private writes #590 round 2: nested-field write hole reproduced (native≠Node) +
_this→ handed back - 2026-10-11T23:02Z fix(checker): close the constructor hole in scope regions and admit a task's private writes #590 fix 7c29167 green
- 2026-10-11T23:25Z fix(checker): close the constructor hole in scope regions and admit a task's private writes #590 round 3: ctor-
this-alias hole reproduced (native≠Node, 4 shapes) + over-refusal regression → handed back
Reactions are currently unavailable
Activity
amritk commentedon Oct 12, 2026
Log — #590 (soundness)
src/attributes.ts(everything()called in a loop insidecomposeArguments). Reproduced locally with the 0.18.0 seed (146 warnings).main(feat(std): a streaming JSON tokenizer over u8[], in nish/json-tokenizer #582, refactor(std): give std/crypto/aes.ts four private seams, each called from one place #592) into the branch on the owner's "Fix it", regeneratedchecked.txt, and verified check, lint, lint:dead and selfhost (ratchet 143, fixed point holds). Pushed as 8a67993.thisalias fix with its siblings, and thenew Grid()over-refusal positive.Generated by Claude Code
amritk commentedon Oct 12, 2026
#590 review round 4 at 8a67993: rejected.
A static adversary and a probe that built the head both reviewed it. The probe compiled every program and ran it natively and under Node.
Round 3 verified closed (each shape refused NL2394, against 7/6 on the merge-base):
this-alias via field, push, index, const local,this.touch(), and a helper return;new Wrap(h.inner);usingdeclarators;thread_scope_region_fresh_objectaccepted,7 15both ways).Still open (blocking). These are all also accepted on the merge-base, but they are in #564/#567's scope:
followFreshignores caller memory stored into aconst w = new C()by:w.inner = h.inner);push;w.bind(h));attach(w, h));attach(this.w, h)).thishanded on through a local in a constructor.const me = this; attach(me, h)never setsthisAliased. 1 program.New over-refusals (accepted on base, refused at head):
new Grid(4, arr)with the helper's own array;pushesh.inneronto its own array.Not blocking: no
docs/securityrecord was touched.Every program text has been handed to the worker. Round 5 follows its push.
Generated by Claude Code
amritk commentedon Oct 12, 2026
#590 review round 5 at afb5959: rejected.
Two agents reviewed it: an adversary, and a probe that built the head. Every finding below was confirmed by a run in which the program was accepted and native and Node printed different output.
Round 4 verified closed. All 61 round-4 probes keep or reach the expected verdict:
The probe also ran about 60 new programs against the new model. All the expected refusals hold for:
return this, fresh objects nested in fresh objects, arrays;let, loops and branches, recursion, generics, interfaces, for-of;constaliases and callbacks.Still open:
children[1], but anN_PROPERTY's value ischildren[0]. 9 programs fail, includingconst p: Pair = { inner: h.inner }; p.inner.x = 1.u.mid = t.mid,[t.mid],push(t.mid),u.mid = ms[0], and a constructor'su.mid = this.mid. 6 programs fail. 2 of them were already holes at 8a67993; 3 are new since c716dd9; 1 combines holes A and B.c.reset(v)followed by an own write.Direction given to the worker:
this, used in any value position, escapes that root.Generated by Claude Code
amritk commentedon Oct 12, 2026
Check-in, 02:41Z: #590 (soundness)
goldens.js --update. CI on 727d093 is green and mergeable_state is clean.Generated by Claude Code
amritk commentedon Oct 12, 2026
#590 review round 6 at 758356d: no soundness holes; one blocking over-refusal regression.
What round 6 checked:
Blocking regression: a function's own local
Mapwithsetis refused in a region.tallymakesnew Map()and callsm.set("a", h.v). fix(checker): close the constructor hole in scope regions and admit a task's private writes #590 refuses it with NL2394; base ea34c4e accepts it, printing1 1both ways.getOrInsert, a constant value andsetalone are all refused the same way.Non-blocking, folded into the same push:
_thiscells for assign and alias in the escape matrix;The constructor-less-class finding is moot: its fields must be literals, so it can't be written (NL2030).
Recorded for the guards-diagnostics stage (all pre-existing): builders, factories, fluent methods returning
this, and an object returned from a user function are treated as unknown, so writes through them are refused. These matter for ease of use.Generated by Claude Code
amritk commentedon Oct 12, 2026
Check-in, 05:01Z: #590.
Generated by Claude Code
amritk commentedon Oct 12, 2026
#590 review round 7 at 23d66e3: converged on soundness. Merge decided, with the over-refusals split out to #602.
What round 7 checked
for...ofvariable is aROOT_UNKNOWNpath, so a write through it counts against all parameters. That makes the table-store change and thefor...ofchange safe.for...ofwere refused.Decision. The rule is sound, but it over-refuses common Map/Set/array code that
mainaccepted, with the same output natively and under Node. Examples:m.keys();m.values();m.set("a", h.leaf)with no write after it;geton own fresh objects;for (const l of ownArr) l.x = 1.Fixing these needs a design change: a read-back from an own container
cbecomes the pathc[], collection builtins write shallowly, and scalar stores no longer pollute. That deserves its own focused review rounds. #590's scope (#564, #567) is complete and sound, and nothing is released until a human merges the Release PR. So:feature/threads-complete/soundness-precisionafter fix(checker): close the constructor hole in scope regions and admit a task's private writes #590 lands.Generated by Claude Code
amritk commentedon Oct 12, 2026
perf-ir spawned (06:16Z).
feature/threads-complete/perf-irREGION_COSTretuned from the pool's measurement, and theNISH_NUM_THREADSdocs.It started from main ahead of #590's merge, because the two stages touch different parts of
src/parallel.ts. It will merge main and regenerate the goldens once #590 lands. Its verification line usesnode bench/run.mjs --only par, correcting the plan'spar.Generated by Claude Code
amritk commentedon Oct 12, 2026
#590 (soundness) merged, 07:58Z, as squash caaa57f. It closes #564 and #567.
Merge gate
?.chain that ends in??#603) and re-ran the shard that had hit the apt-mirror flake at 288152a.Next
feature/threads-complete/soundness-precision. It needs negatives that fail when its rule is reverted, and positives for the over-refused programs.REGION_COST2^22→2^20 retune are implemented, and its suite is running.Generated by Claude Code
amritk commentedon Oct 12, 2026
#602 progress (08:20Z)
The soundness worker has implemented all four fixes on
feature/threads-complete/soundness-precision, which it cut from caaa57f.??stays unknown. A named inner map handed toouter.setkeeps its escape.The review probe corpus (456 programs, the runners and each round's verdicts) is now on the plan branch
ccr-23558892-oof1r5, in.claude/plans/threads-complete-probes/, so workers can re-run it.Generated by Claude Code
amritk commentedon Oct 12, 2026
Wake 08:58Z: no stage PR is open yet.
Both worker sessions report a seven-day rate-limit warning. The run continues; if limits bite, I'll prioritise finishing open stages over spawning new ones.
Generated by Claude Code
amritk commentedon Oct 12, 2026
perf-ir: PR #607 opened (09:28Z), head 6480580. Worker's local runs (on f732f6c, which matches the head except for main's #605):
npm test17,521 passed and 0 failed; wasi 38/0; performance 203/0; selfhost 182/0; nish-cmp 0 undeclared. Its measured figures:runtime-parallel.c.text is 605 bytes (2,745 threaded), within its ceilings.docs/wp20-threads.md§8e is stale.using g = j.bins[v & 7].lock()is refused with "Element access requires a numeric index". It may be a checker bug; the guards-diagnostics stage will triage it.Review round 1 started:
__ulockpath, the wasm fallback), on the wall-time claim against its noise band, and on BENCHMARKS.md, which was regenerated whole on a different CPU;Generated by Claude Code
amritk commentedon Oct 12, 2026
#607 (perf-ir), round 1 at 6480580: rejected.
Correctness is clean. The probe ran more than 10,900 contended-lock runs:
All
thread_*andpar_*programs pass, Node parity holds, and the static review traced the protocol (Drepper mutex 3) as sound.Blocking findings
Grain threshold off by 2x. The code divides at the grain, which gives two chunks of about grain/2. That lands on the measured 0.98x row, not the claimed 1.19x one. The docs also misstate which body is cheapest; that body costs 3.
"Median 12% faster" lies inside the 14% noise band.
BENCHMARKS.md was regenerated wholesale on a different CPU. That rewrote unrelated recorded claims: fib, sieve, Map/Set, sizes.
Lock timing regression against main, with a noise band of 1–4%:
contended 200contended 200The cause: a 64-load spin puts waiters to sleep on most contended acquisitions. The PR makes 235k futex waits and 428k wakes where main makes 2.8k yields, and about 190k of those wakes find nobody asleep. The PR wins only on short critical sections at 4+ threads, where wall time is 8–11% lower.
Direction to the worker: the lock must not be slower than main beyond the noise band at any measured thread count. Tune the spin, defer marking waiters until about to sleep, yield before sleeping, or count waiters. If it can't, drop the futex part and keep only the grain retune and the docs.
The probe programs are on
ccr-23558892-oof1r5under.claude/plans/threads-complete-probes/perf-ir/.Non-blocking items (handed over too):
syscallargument widths, a test that forces the sleep path, a tighter IR regex, and docs/AI.md:898 and wp20 §8e, which are stale.Generated by Claude Code
amritk commentedon Oct 12, 2026
#602 is up as PR #608 (10:23Z), head 55c2d07.
Worker's report
readback_store_alias, which is pinned by two rules.npm test17,542 passed, 0 failed; selfhost 186/0, with the ratchet at 143.Review round 1 has started. A static adversary, plus a probe that rebuilds both #608 and main, re-runs all 454 programs, and attacks the new read-back rule: smuggling caller memory into an element, then writing through a read-back.
perf-ir #607 status: the round-1 blockers on grain, claims and BENCHMARKS.md are fixed locally. The futex lock protocol is being replaced by one that keeps a sleeper count and only wakes when a sleeper is registered; the worker is tuning it against main. If no variant stays within main's noise band, the futex lock gets dropped.
Generated by Claude Code
18 remaining items
amritk commentedon Oct 16, 2026
2026-10-16T05:35Z: #644 round 2 @ 3975bce
thisescaping, multi-declarator,?:/??,id(), generics, guard escape, number arrays.reject_thread_task_private_object_array_ownis refused by the older direct-store rule, not the new one;h.poke()on an own holder whose field is fresh;Map.set/Set.addon an own Map or Set in a body;Generated by Claude Code
amritk commentedon Oct 16, 2026
2026-10-16T06:10Z: #640 merged as ce97fa2 (fixes #618)
Round 4 @ 9080c81 converged.
absolute,strlenOfandmySqrtare not over-refused.--dump-tliname on the 5 triples is listed.fls.Generated by Claude Code
amritk commentedon Oct 16, 2026
2026-10-16T07:07Z
83cd291, which changes tests only:_array_ownnow goes through a pushednew HB(box), a new_declaratornegative is added, and unused imports are dropped;RULE_COUNTresolved to 638 = 637 + fix(checker): refuse an exported function that takes a C symbol, and spell private ones apart #640's 1, and the goldens regenerated with the tools.session_01KNYm29yFkadZWiVnFLYtf8on branchfeature/threads-complete/atomics.AtomicI32/AtomicI64(load, store, add, sub, min, max, compareExchange) under Mutex's R1/R7/R8.examples/histogram-atomic.ts, measured against the lock version and one thread, for acceptance Add the orientation map a new session starts from #6.Generated by Claude Code
amritk commentedon Oct 16, 2026
2026-10-16T09:15Z: #654 (atomics), round 1 @ 2d4aec9
Probe: about 90 attack programs, 9 positives, edge semantics, wasi and timings. Adversary: static review.
Refused correctly:
new,this, separateusings, nested scopes,??, returns, Map, nested fields, for-of;Positives match Node at 1, 2 and 8 threads. I32 overflow, signed min/max and compareExchange's return value also match. I64 atomics are
align 8on wasm.Handed back:
std/threads.tsdeclaringclass AtomicI32gets the real atomic lowering.isAtomicClasslacks the package check. The symptoms are wrong output, invalid IR, or NL0003.using s = scope(), t = scope(), one scope's task can touch an atomic or Mutex that the other scope's task updates. It is pre-existing on main for Mutex, and is fixed here as the "every declarator" shape.AtomicI64under Node is documented three ways.parallelReduce's split;Timings (probe machine, histogram, 32M values):
The worker is making the local-tally pattern the example's default, and the docs lead with it.
Generated by Claude Code
amritk commentedon Oct 16, 2026
2026-10-16T10:20Z: #654 (atomics), round 2 @ de3168f
Confirmed fixed:
std/threads.tsnow behaves as an ordinary class.using s = scope(), t = scope()are refused.?:, generic, Map).Handed back:
10and Node200. It is pre-existing on main and is the same family as round 1's H2.Mutex<A>againstMutex<B>is refused, and so are two different bindings of the same type.Channelobject literal is accepted and Node throws (pre-existing). NL2514 is to cover everynish/threadsclass; the general class case is checker: an object literal typed as a class with methods is accepted, and Node throws on the method call #656.The PR also conflicts with main; a merge and regeneration were requested.
Noted for guards-diagnostics:
using g = m.lock(), s = scope()names the guard as the scope in its message.Generated by Claude Code
amritk commentedon Oct 16, 2026
2026-10-16T12:50Z: #654 (atomics), round 3 @ 74e34de
Soundness converged.
?:, helpers, holders, generics, arrays, Maps, capture vs argument, three declarators, constructors, higher-order helpers, nullable fields, and destination variants.nish/threadsclass gets NL2515.CI: 28/28 green.
Several r3 mutex and destination shapes diverge between native and Node on main today. This PR closes them.
Codes, after the user's #642 took NL2512:
Handed back (docs and tests only):
Merge on green after that push.
Generated by Claude Code
amritk commentedon Oct 16, 2026
2026-10-16T14:50Z: #654 (atomics) merged as ec8e597
It went through 3 review rounds.
std/threads.tsand a race across two scopes in oneusing.Final head 77ec9a6:
ci.yml, ported because main's shard 1 had reached 14m49s after perf(crypto): P-256 multiplies the base point by a precomputed table #663).Acceptance #6 met. On 4 cores, 32M values:
Codes: NL2513 (parent touches an atomic), NL2514 (atomic member), NL2515 (threads-class literal), NL2516 (sibling scopes).
Stage copy-at-join dispatched on
feature/threads-complete/copy-at-join. It covers string and scalar-array results,Channel<string>, and strings through a guard, with use-after-free hunting underchurn(), for acceptance #7.Remaining: guards-diagnostics, guide, then acceptance.
Generated by Claude Code
amritk commentedon Oct 16, 2026
2026-10-16T18:20Z: #669 (copy-at-join), round 1 @ ffd99a8
Held: no observable use-after-free, no native≠Node difference, no crash, no invalid free. This was tested under churn plus valgrind at 1, 2 and 8 threads, across every escape route and on panic paths. Every exclusion is refused, and acceptance #7's three positives match Node.
Handed back:
string | nullslot gets no copy, leaving a dangling pointer in the Mutex. No read path exists yet; main refuses it.Channel<string>send is quadratic in the sender's arena. 1M kept sends take 63.6 s against 0.48 s for i32.string[][]negative, and docs for the Mutex-drop leak.Measured on the probe machine: string results are currently slower than sequential in every form. 1M strings take 694 ms parallel against 410 ms sequential. The worker's analysis puts the cause in the serial per-element copy-in. A follow-up, adopting worker blocks into the joiner's arena (O(blocks) copy-in), is planned after #669 merges.
Generated by Claude Code
amritk commentedon Oct 16, 2026
2026-10-16T22:45Z: copy-at-join merged (#669 → e4a4a02).
Round 1 at ffd99a8 found five problems. All were fixed in 48cd0fd and 94abfa2:
string | nullskipped the copy;Channel<string>send was quadratic: 63.6 s for 1M sends;runtime-cross.c, with its own ceilings.Round 2 at 94abfa2 produced five adversary items, fixed in 51aa834:
+=through a guard was confirmed unreachable and is pinned;parallelFromandparallelMapInto;The probe ran 122 programs at 1, 2 and 8 threads: all printed the same as Node, and valgrind was clean.
CI first failed on two things:
std_randomgolden had not been regenerated;LINK_READ_LATER.Both were fixed in 5888215 and da7338d, and every check passed.
Acceptance Ship the self-hosted compiler #7 (a task returning a string, a
string[]map, aChannel<string>pipeline, and a class result still refused) is covered bytests/link/thread_task_string,par_map_stringandthread_channel_string, plus the reject_* negatives. It is to be re-run at acceptance.Deferred to a follow-up:
Started:
guards-diagnostics: session_01UgnMogyQPbnd8zBBzQWPjh, branchfeature/threads-complete/guards-diagnostics. It covers the plan's guard helpers, diagnostics and--fix, plus six over-refusals found in review:h.poke()on a task's own holder;Map.set/Set.addon a body's own Map or Set;copy-adoption: the copy-at-join worker, branchfeature/threads-complete/copy-adoption. It makes copy-in O(blocks) by adoption and fixes the trim cost.Remaining: guide, then acceptance against the 12 criteria, then close-out.
Generated by Claude Code
amritk commentedon Oct 17, 2026
2026-10-17T05:50Z: guards-diagnostics merged (#691 → 7587702).
(g) => g.value, returned guarded memory after the lock was released. Native ran out of memory where Node printed the string, and valgrind reported an invalid read. The fix judges an expression body as itsreturn. The same gap was closed in the channel rules (NL2501).--fixmethod hoist that produced an invalid identifier;Mutexcode missing<T>;r.poke()and its sibling shapes are refused. One blocker:--fixdeclaredkNowtwice when two statements captured the samelet. Fixed with numbered names (kNow,kNow2) in f5d808a and 483a07e.tests/link/thread_mutex_helper.tests/wordings.Still open: copy-adoption #686, head f0e1cfa, worker session_01NaDqCvGo7iKjhV9cj15pqN. Round-1 review items 1–5 are fixed:
malloc_trimcuts held memory to 52 MB, against 118–135 MB on 0.18.0;Blocking: F1, a 64 KB transfer block per chunk however little the chunk keeps (3,000 small maps peak at 62 MB, against 4 MB on main). It needs a main merge after #691.
Next: merge #686, then the guide, bench-compare (acceptance #10), acceptance, and close-out.
Generated by Claude Code
amritk commentedon Oct 17, 2026
Ledger: #697 and #686 merged.
npm test, the refusal-to-fix table, and AI.md's threads rules. The accuracy review's findings were fixed before the merge: the--fixoverclaim, the mutex codes, and the declined list.Transfer blocks start at 4 KB and double up to 64 KB. A join holding 64 KB or more is linked into the caller's arena; below that, the elements are copied in.
glibc's trim and mmap thresholds are set when the pool starts. A parked worker hands its arena back after a second with no work.
Three review rounds plus two runtime probes. The probes found three problems, all fixed before the merge:
Figures on the merged head (main / copy of main / this change):
parallelMap, 4 threadsAt 1 thread the map is not faster than the sequential loop, and no claim is made there.
Next: acceptance against the 12 criteria on merged
main, then close-out.Generated by Claude Code
amritk commentedon Oct 17, 2026
Acceptance on merged main efbbd8c (4 vCPU VM): 9 pass, 1 fail, 2 partial.
parallelMap, const local k, ≥3x sequentialx * k, one call over 2^25: 1.4x. 40 calls over 2^22: 0.43x, slower than the loop. With a compute-heavy body: 3.6–3.9x.NISH_NUM_THREADS=1,taskset -c 0spawnnew ArrayChannel<string>match Node; classes refusednpm test. The 12 ok examples also match Node.npm test: 21,013 passed, 0 failed, 2 environmental skips (aarch64crt1.o, andNISH_BOOTSTRAPunset). The bootstrap checks were run separately: nish-cmp has 0 undeclared differences, and fuzz--stage1agrees on 200 of 200. wasi 57, performance 213.Root cause of #1, confirmed in the IR. A function that calls
parallelMapand returns a scalar has nonish_arena_mark/nish_arena_release, while its loop twin does. So every call keeps its result: 674 MB maxrss against 34 MB. No warning is printed, andusing arena()around the call is refused.Follow-up workers started:
feature/threads-complete/par-reclaim: reclaim that frame when the result doesn't escape, with negatives for each escape route and an RSS-bound fixture.feature/threads-complete/refusal-remedies: end every thread refusal with code, and add one refusal table to the guide with a test.Generated by Claude Code
amritk commentedon Oct 17, 2026
Ledger: #702 is merged (ef8502b).
fix(checker): every thread refusal now ends with the code to write....or a placeholder.main. Two first sentences were shortened (NL2465, NL2471).main's messages, 19 of 25 fail it; on this branch, 0 of 37 fail.This addresses acceptance criterion #9.
Still open: #703, par-reclaim, which addresses criterion #1.
Generated by Claude Code
amritk commentedon Oct 17, 2026
Ledger: #703 merged (5082385),
perf(codegen): a frame that callsparallelMap,parallelFromorparallelMapIntonow releases its arena when the result does not escape. This includes the string blocks it adopted under #686.How it was checked
churn(). The runtime probe ran all of them against Node at 1, 2 and 8 threads, and valgrind reported them clean.using arena()now accepts number and string maps..llgoldens record which frames reclaim and which do not.Measured
Workaround: the WASI parity loop now runs Node with
--no-turbo-fast-api-calls. Without it, a Node 22 garbage collector segfaults inside the fastfd_writecall.Next: re-running acceptance criteria #1 and #9 on merged main, then the final close-out.
Generated by Claude Code
amritk commentedon Oct 17, 2026
Final acceptance. The run is closing.
Criteria 2–8 and 10–12 were verified on efbbd8c. Criteria 1 and 9 were re-run on merged main at 5082385.
kruns at least 3x the sequential loop on 4 coresNISH_NUM_THREADS=1andtaskset -c 0Channel<string>, with class results refusedCriterion 1 in detail
x * k, 40 × 2^22 elementsx * k, one call over 2^25 elementsThe machine has 4 real cores, and 4 independent sequential copies slow each other by only 1.1–1.3x, so the hardware would allow about 3x. The memory-bound map waits on memory, but the heavy body's gap points at overhead in the runtime's scheduling or chunking. That is not closed.
An earlier 3.7x figure for the heavy body came from
c1_heavy, whose checksum is always 0 (an XOR over a bijection), so it proved nothing. The re-run used an order-sensitive fold instead.Criterion 9, NL2484. Its remedy is fixed text,
const sendAll = (ch: Channel<i32>): void => { ch.send(1) }(src/parallel.ts:4776). It ignores the program's channel type, so pasted into aChannel<string>program it does not compile. The test misses this because the guide's example happens to useChannel<i32>.Merged in this run: #607, #608, #609, #615, #616, #621, #640, #644, #645, #654, #669, #686, #691, #697, #702 and #703.
Every worker session is archived and no PR subscriptions remain.
Generated by Claude Code