Skip to content

Phase 1 / Debt / M1.D-created entries (S5) - #82

Open
guysenpai wants to merge 180 commits into
mainfrom
phase-1/debt/m1d-created
Open

guysenpai wants to merge 180 commits into
mainfrom
phase-1/debt/m1d-created

Conversation

@guysenpai

@guysenpai guysenpai commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Squash message

chore(debt): close ten entries M1.D created (M1.D/S5)

S5's subject is the fifteen entries M1.D itself created, under a rule the
milestone did not have: a defect found while working closes in the same session,
or it is a STOP and an arbitration, never a new number. Measured on the delivered
lot (sha256 6da1d093, 648 lines): TEN of the fifteen are closed - M1.D.40 by S4,
and 44, 45, 38, 39, 46, 48, 36, 37, 35 by S5 - and FIVE leave the milestone.
S5 minted ZERO new entries, which is what it existed to demonstrate against a
milestone that minted nine.

The five that leave are not accumulated debt. M1.D.34: restricting the f64 leg
changes what the matrix attests. M1.D.42: whether a directory may declare several
modules. M1.D.43: a removal primitive in a registry whose ids are positional.
M1.D.47: carry the zone beside the type, or split ResolvedType. Four architecture
decisions. M1.D.41 is the fifth and is a restructure rather than a renaming: seven
of its subjects would collapse onto one reserved name.

THREE ENTRIES' OWN TEXT WAS CONTRADICTED BY THE TREE, which is this milestone's
purpose applied to its own output. M1.D.47 does not overlap 44 and 45 at all - the
digest input path holds zero ResolvedType references across its four functions,
and the premise was a homonym, zone against @storage. M1.D.35's recorded blocker
was dissolved by M1.D.21 one session earlier in the same milestone. M1.D.41's
sixteen renames are unsatisfiable.

The standing result: A COUNTER-MEASURE WRITTEN DOWN STOPS NOTHING UNTIL AN
INSTRUMENT EXECUTES IT. dead_tests.zig:920 names the b.path(spec.path) trap in the
repository's own words and the linter resolves that shape; my extraction walked
into it anyway and under-counted M1.D.41 by exactly three. Tenth instance of the
selector family and the first where the parade existed before the error.

Beside it: one retention point measured instead of implemented, the prompt having
said two - a string is the only arena form that can reach an event field, and
captureEventFilter already copies it. Coverage by accident of spelling measured
directly - return a + "y" refused where return "x" + "y" was not, both parking the
same handle. Four errors of mine, every one caught by an instrument and none by
re-reading, plus a fifth in the closing tally itself. Three blind probes in one
gate whose first cause was ONE CHARACTER, wait(1s) against wait(1.0s), with both
negative controls reading zero. And the latent defect in allocateSlot caught
before it shipped: it returned chunks.items.len - 1 unconditionally, a wrong
location the moment an earlier chunk can be chosen.

Floor re-derived from the suite at every gate and never carried: 2379 -> 2395,
windows 2393. Green at Debug/f32, ReleaseSafe and -Dphysics_f64=true; lint
conservation OK, fmt, bindgen-verify, and forge-determinism with its eight
witnesses byte-identical.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com


What S5 closes

Fifteen entries M1.D itself created. TEN closed, five leave the milestone.

closed
M1.D.40 by M1.D/S4, before this session
M1.D.44 tag identity enters the schema digest — threaded through all THREE TagSet descriptor producers, not the one obvious site
M1.D.45 storage and requires both ruled OUT — one by arbitration, one by measurement
M1.D.38 the unreachable E0217 emission removed rather than relocated
M1.D.39 the trailing comma, on four arg_list users and not the three the entry named
M1.D.46 builtin names reserved, the predicate DERIVED from the table
M1.D.48 rule-arena values refused at the async retention point
M1.D.36 the second contact epsilon removed
M1.D.37 bindgen-verify narrowed to the four generated files, one declarant
M1.D.35 first_partial — a partially-filled chunk is refilled

The five that leave, with their motive

None is accumulated debt.

  • M1.D.34 — restricting the f64 leg changes what the matrix attests.
  • M1.D.42 — whether a directory may declare several modules.
  • M1.D.43 — a removal primitive in a registry whose ids are positional.
  • M1.D.47 — carry the zone beside the type, or split ResolvedType.

Four architecture decisions. And one restructure:

  • M1.D.41 — not a renaming: src/ holds two executables, simd/bench/ two, simd/tests/ three, so seven subjects would collapse onto one reserved name.

And the shape of it

M1.D produced fifteen entries while emptying its own table. Ten are closed by the session that existed for that, and S5 minted zero new ones — the rule it ran under being that a defect found while working closes in the same session, or it is a STOP and an arbitration, never a new number. It stopped five times.

Three of those entries had their own text contradicted by the tree, which is this milestone's purpose applied to its own output.

🤖 Generated with Claude Code

guysenpai and others added 30 commits September 21, 2026 15:13
The digest input path holds zero ResolvedType references across its four
functions and 86 lines - schemaDigestOf, tagSetDesc, computeLayout,
schemaDigestFor - with the extractor shown to fire on isRuleArenaType in the
same execution, so the zeros are results and not silences. types.zig names
schemaDigest zero times; registry.zig names ResolvedType zero times;
ResolvedType exists in two files, one of them a single test helper.
computeLayout derives FieldKind from the AST type node, so the chain is
AST -> FieldKind -> Layout -> digest and the type checker sits beside it, not
upstream.

The premise came from a homonym: M1.D.47's zone is a value's memory lifetime,
M1.D.45's storage is @storage(.sparse), the ECS storage mode, which
registry.zig:198-200 declares OUT of the hash by ARCH-005. So it is one entry
for one gesture, and 44 and 45 are a second, disjoint gesture on
schemaDigestOf.

The 105-site figure costed option 1 of the S4/G8 arbitration, a recursive
optional payload, and not the plan's deliverable; it reproduces under no
selector tried - 68/6, 90/15, 298/12, 214/2.

The gesture is not single: the ambiguous variants are built at eight sites,
four of them resolving a type NODE, which carries no zone and cannot. Those
four sit under namedTypeToResolved, 37 call sites, most of which cannot supply
one. M1.D.47 is not started and the arbitration is raised.

Floor re-derived from the suite and unchanged at 2379, measured twice under a
live ZLS watch build; lint conservation OK at 2379; fmt clean. Zero .zig
touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Of 37 call sites, exactly ONE both admits an ambiguous collection type and knows
the zone - lookupFieldType's .resource arm at :8178, which dispatches on the
receiver and therefore holds the provenance already. THIRTEEN admit one without
knowing it: twelve parameter/return sites and one let annotation. The remaining
22 are moot or derivative, and that is a measurement of the gate at :3896, where
collection field types are resource-only and component/struct/event are rejected
at :3917 - probed, 0 errors on resource and 1 undefined_symbol on each of the
other three, which is the positive half that makes the zeros results.

At the twelve parameter and return sites the zone is NOT DETERMINATE, which is
stronger than unknown. The corpus holds zero collection parameters and zero
collection returns against 18 let annotations, and zero in a corpus is not
illegality: fn f(xs: int[]), fn g() -> int[], fn h(m: [int: int]) and
fn i(s: Set<int>) all check with 0 errors. The deciding case is one program in
which a single fn total(xs: int[]) takes an arena literal AND get(Bag).items at
two call sites, 0 errors. There is no single answer for that site to know.

So a zone variant meaning unknown re-arms the S4/G8 defect one level down, and
the measurement points elsewhere: the zone is a property of a VALUE, not of a
declared type. The thirteen sites describe signatures, which range over both
zones by design. The open question is the SHAPE, not the blast radius, and it is
architecture rather than a debt-milestone correction.

One site is unmeasured and its own control says so: the cast target at :6255.
types.zig:6256 must reject a non-builtin target, the probe reported 0 errors for
a as int[], and its control 1 as bool - which that same line must reject - also
reported 0. The probe never reached the arm, so all four readings are silences.
Reported unmeasured rather than resolved on a blind instrument.

Both probes appended to types.zig, run, reverted; file byte-identical at
sha256 4e2e9e88 before and after each, zero ZZPROBE residue in the tree. Floor
re-derived from the suite after the reverts and unchanged at 2379, which is what
says the tree came back; lint conservation OK at 2379; fmt clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
M1.D.44 - tag identity enters the schema digest. ComponentDesc gains
content_digest, hashed; TagTable.contentDigest hashes the leaf count then each
path by bit index, PLACING leaves at their own index rather than iterating the
map, since a bit_index is a pure function of declaration order and hash order
would vary build to build. Three TagSet descriptor producers were enumerated,
not one: the interpreter's tagSetDesc, the codegen emission whose comment claims
it mirrors the interpreter exactly, and a Tier 0 test literal. desc.fields was
measured and REFUSED as the carrier - componentFields has ten readers, several
on the scene-serialization path, which walk fields by kind.

The false refusal is named and pinned: appending a tag inside one word is safe
and is now refused, because a whole-table digest cannot tell a surviving prefix
from a changed table. Accepted deliberately; an append crossing a word boundary
was already refused. The test pinning the defect as an accepted adjacent case
inverts to REFUSED, declared here. Counter-factual with the surviving subset
named first: dropping content_digest from the hash reddens rename, reorder and
append, exactly three, and the green twin stays green.

M1.D.45 - both out. storage by arbitration (ARCH-005). requires by measurement
taken before deciding: it never appears beside a size, alignment or offset in
registry.zig, and every closure consumer is forward-looking, so a reload editing
only the requires set leaves live bytes valid and meaning what they meant.
Residual raised rather than filed: a newly added requisite is an invariant live
entities may violate unchecked, which is not a layout problem.

M1.D.38 - the E0217 emission is removed rather than relocated. It was dead by an
early return fourteen lines above; relocating it above that return would fire
for two undeclared symbols, where undefined_symbol is the true diagnostic. The
code stays declared with no producer and its reason, and what single-module mode
answers is pinned so the wrong route reddens.

M1.D.39 - the set was FOUR, not the three the entry names. The three shapes are
three callers of one parseCallArgList; the grammar also gives annotations an
arg_list, and parseAnnotations carries its own loop with the same defect.
Enumerating the function's callers gives three, the grammar production's users
four. Both fixed, both independently witnessed: the test is red before the fix
and red again when only the annotation break is removed.

Floor re-derived from the suite 2379 -> 2385, windows 2383. The dead-tests
bilateral control refused the first attempt and the declared value was moved to
the measured one. Green at Debug/f32, ReleaseSafe and -Dphysics_f64=true; lint
and fmt exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
M1.D.48 - the four silences are closed under a control shape that fires: both
negative controls fire and both INT controls stay silent in the same execution.
SET, OPTIONAL, CLOSURE and a string concat of two literals all park in
AsyncTask.result with zero diagnostics, so all SEVEN rule-arena forms of Value
are reachable there and none was diagnosed.

The string row is the one not asked for and it is the point: a concat built from
a LOCAL was already refused while the identical "x" + "y" was not, though both
park the same .string_run. The first was covered only because let a made it a
local the sibling rule walks - coverage by accident of spelling.

And the second retention point needs NO refusal, measured rather than assumed.
An int? event field and an int[] event field are refused as FIELD TYPES, a
struct-valued filter is refused by the filter checker, and an enum carries a
discriminant. A string is the only arena form that can reach a captured filter,
and captureEventFilter deep-copies exactly it. So stabilising strings alone is
complete, not partial, and the fix lands at one point instead of two. The
assumption is written into a test so it falls the day an optional or collection
event field becomes legal.

The refusal sits at .return_stmt under conc_branch == .race - where a value
enters the parked result - and consults isRuleArenaType, the SAME predicate as
the sibling local rule, so the two cannot drift on which forms are arena-backed.

False-refusal cost measured before proposing: ZERO on the repository, the corpus
holding no race construct at all. The cost is two constructed cases, both found
by the green twin rejecting them: a bare string literal, not separable because a
literal, a concat and a resource string are all builtin .string_; and a bare enum
shorthand, which arrives .unknown and lands on the arbitrated arm instead. Both
pinned in a test named for the cost. Counter-factual: disabling the refusal
reddens the per-form and cost tests and leaves the green twin and the filter test
green.

M1.D.46 - measured first under a declaration-level negative control: all seven
collisions were accepted with zero diagnostics. isReservedEngineTypeName is
DERIVED from builtin_resources rather than listed, with a test walking the table,
because a second list is how the two come to disagree silently. TagSet gains a
named constant. E0101 is reused with a contextual message on the duplicate-test-
name precedent. The green twin includes names that merely resemble reserved ones.

Floor re-derived from the suite 2392, windows 2390; the control refused the
first attempt. Green at Debug/f32, ReleaseSafe and -Dphysics_f64=true.

Probe hygiene under the new rule: appended at end of file, retired by git
checkout. The last probe also shared the file with real work, so it was cut at
its marker and the removal VERIFIED on the diff - 284 insertions, zero
deletions - which is the evidence a global replace could not give.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
M1.D.36 - fast_paths.zig defined its own contactMargin with its own conv_k = 16
while gjk.zig exports both. The entry says five call sites; there are four plus
the definition - five OCCURRENCES. The local doc said the margin "must stay
gjk.zig's", an intent the code did not enforce. Dependency direction measured
before importing: the narrowphase graph is acyclic and epa.zig and shapecast.zig
already depend on gjk the same way.

The neighbour above it is NOT in the class: boxExtent mirrors coreExtent's .box
arm at a site holding raw half-extents and contains NO constant, so it has
nothing to drift. Swept out deliberately rather than folded in.

Verified by the determinism instrument rather than by argument: forge-determinism
exits 0 with the eight witnesses byte-identical and all four discrete invariants
OK; forge suite green at f32, f64 and ReleaseSafe.

M1.D.37 - the sharper fault is the PATH SET, not the comparison base. The gate
named bindings/generated/ and src/core/platform/ wholesale while the adapters
write exactly four files. The first holds two hand-maintained .api.zig sidecars
no adapter writes; the second holds nine entries of which one is generated. So a
comment edit to threading.zig turned a bindgen gate red - S4/G5 of this milestone
did exactly that. Narrowed to the four, and based on HEAD because "matches the
COMMITTED output" is the criterion the gate's own comment states.

Then the second declarant was removed: the four paths were spelled once in the two
zig fmt passes and again in the diff. generated_binding_files is now the one list
all three read, so a new generated file cannot be formatted and then left ungated.

Two-sided witness, re-run after that refactor: with threading.zig dirty the old
predicate exits 1 and the full gate now exits 0; with a generated file dirty the
predicate exits 1. Both directions, or the narrowing would be indistinguishable
from switching the gate off.

M1.D.41 - predicate derived, no rename performed. My first arm B gave 90 subjects
against the entry's 16 because it dropped the single-entry constraint: a directory
entered from outside by two or more of its files is not a module. Corrected, arm B
reproduces exactly under six independent graph variants.

My arm A was short by exactly three, which is the whole 8-against-11 gap:
build.zig:1138 is root_source_file = b.path(spec.path) inside for (test_specs),
and three of the table's entries are under src/. A literal extraction cannot see a
path arriving through a loop variable - and tools/weld_lint/dead_tests.zig:920
names that exact trap in the repository's own words. The entry's count is right and
mine was wrong. A ninth subject, plugin_loader/desc.zig, came from applying the
arm-B re-export discriminant to an arm-A declared root.

The deliverable is NOT executable: src/ holds two executables, simd/bench two, and
simd/tests three files with no root.zig - each group would collapse onto one
reserved name. Seven subjects cannot be renamed in place; closing them is a
restructure, the same verdict forge/ carries. No lint rule written either, on the
M1.D.11 precedent that a completeness rule over a non-conforming set produces
filling.

Classification put through an adversarial audit first: five lenses, 35 verdicts,
28 survived and 7 refuted. It found two errors and both were mine. Every decisive
claim re-checked by me at the source before being written.

Floor unchanged at 2392 - this gate adds no test, stated because it was measured.
Green: test, lint, fmt, bindgen-verify, forge-determinism.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measurement only; no line written, and the reason is not the one the entry
worries about.

Who holds the locations: World.entity_locations, the single EntityId -> Location
map (world.zig:167, declared "the single map" at :20). The archetype holds none,
which is exactly why releaseChunkIfEmpty ANSWERS the renumbered index instead of
repairing and World.reclaimChunk does the repair - the split M1.D.18 delivered.
A reuse policy needs no new seam.

What a move invalidates: NOTHING, because the named remedy moves no entity.
removeSwap swaps the trailing entity into the freed slot, so every chunk holds a
dense prefix and free space is always at the tail - never an interior hole.
Partial-chunk REUSE appends at chunk.entity_count of a non-trailing chunk, which
is the operation allocateSlot already performs on the trailing one. No entity
moves, no Location changes, no pointer is invalidated.

That is separate from COMPACTION, which the entry's title suggests and its
deliverable does not name. Were it taken it would invalidate the moved entity's
Location - world-owned, and reclaimChunk already repairs that class - and any
live chunk pointer, comptime_query holding cur_chunk with a chunk_idx/slot
cursor, already covered by the contract that structural change never runs
mid-iterateArchetype.

ComponentRef supports it WITHOUT CHANGE, and M1.D.18's recorded blocker is stale:
it says moving an entity invalidates the chunk_ptr of every live ComponentRef and
therefore touches a just-frozen contract. M1.D.21 removed chunk_ptr in S2/G1 of
this same milestone, one session before that was written. Same shape as
M1.D.12's deferral condition, met by work that landed elsewhere.

Cost sized: only finding a partial chunk is new. A per-spawn scan is O(chunks),
worst precisely in the regime the entry names where M1.D.18 measured 8200. A
maintained first_partial u32 on the archetype is O(1) amortised - one u32 and
maintenance at two sites.

One observable changes and it is already not an invariant: under reuse a late
spawn can iterate before earlier entities. The declared contract at query.zig:16
is archetype-creation order then chunk then slot, which describes the walk and
survives. Spawn order equals iteration order is not that contract and is already
broken today, removeSwap moving the trailing entity into the freed slot on every
non-trailing removal.

What is NOT measured is the benefit. M1.D.18's 6-against-2 is the full-drain case
that reclamation already closes; the half-empty regime has no measurement, and
the instrument is absent - ecs_hybrid_crossover.zig:149-150 records that
DynamicQuery offers neither chunkAt nor chunkCount. So the obstruction is gone,
the remedy is cheaper than it reads, and what is missing is the figure nobody
doubted. Recommendation: build the probe first. Delivering a Tier 0 allocation
policy on an unmeasured benefit is this milestone's dominant defect with the sign
reversed.

Floor unchanged at 2392; test and lint green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Archetype.first_partial is a LOWER BOUND on the index of any non-full chunk, not
"the first partial chunk". A bound makes each maintenance site decidable alone
where an equality would make every one owe a proof: lowering is always safe,
advancing must be earned, and exactly one site earns it - allocateSlot, walking
past chunks it has just observed full. Four sites: the walk advances on an
observation, removeSwap lowers unconditionally by @min, and the two release arms
clamp.

A latent defect went with it: allocateSlot returned chunks.items.len - 1
unconditionally, correct only while the trailing chunk was the sole reachable
destination and a WRONG LOCATION the moment an earlier chunk can be chosen. It
now returns the chosen index.

No entity moves. removeSwap swaps the trailing entity into the freed slot, so a
chunk's occupants are a dense prefix and its free space is always at the tail;
reuse appends there, which is what the trailing path already did. Hence no
location repair, no ComponentRef change, no new seam.

Three witnesses in the regime reclamation does not reach - chunks stabilising
above zero - each pairing the chunk count with chunks_released pinned at ZERO so
the other mechanism cannot take the credit: a quarter of chunk 0 drained then
refilled against a FULL trailing chunk, count stays 3; the reused entity recorded
at chunk 0 with its bytes read back through that location and every displaced
survivor still readable; and the bound asserted as a bound across three rounds of
drain-and-refill.

Counter-factual built to isolate the DECISION from the MECHANISM - the bound stays
maintained and only the choice reverts to trailing-only. Predicted and measured:
tests 1 and 2 redden, test 3 stays GREEN, so the first two measure the reuse and
the third measures the bound.

The benefit is UNMEASURED and no figure is offered. M1.D.18's 6-against-2 is the
full-drain case reclamation already closes; the half-empty regime has no load
measurement and the instrument is absent - DynamicQuery offers neither chunkAt nor
chunkCount. What justifies the change is structural: the invalidation set is empty
and the cost is one u32 and four sites.

Floor 2392 -> 2395, windows 2393. Green at Debug/f32, ReleaseSafe and
-Dphysics_f64=true; lint, fmt, and forge-determinism with witnesses unchanged -
which matters because this is a Tier 0 allocation-policy change.

S5 closes: of the fifteen entries M1.D created, NINE are closed and six leave the
milestone as architecture decisions or a restructure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
S5's subject is the fifteen entries M1.D itself created, under a rule the
milestone did not have: a defect found while working closes in the same session,
or it is a STOP and an arbitration, never a new number. Measured on the delivered
lot (sha256 6da1d093, 648 lines): TEN of the fifteen are closed - M1.D.40 by S4,
and 44, 45, 38, 39, 46, 48, 36, 37, 35 by S5 - and FIVE leave the milestone.
S5 minted ZERO new entries, which is what it existed to demonstrate against a
milestone that minted nine.

The five that leave are not accumulated debt. M1.D.34: restricting the f64 leg
changes what the matrix attests. M1.D.42: whether a directory may declare several
modules. M1.D.43: a removal primitive in a registry whose ids are positional.
M1.D.47: carry the zone beside the type, or split ResolvedType. Four architecture
decisions. M1.D.41 is the fifth and is a restructure rather than a renaming: seven
of its subjects would collapse onto one reserved name.

THREE ENTRIES' OWN TEXT WAS CONTRADICTED BY THE TREE, which is this milestone's
purpose applied to its own output. M1.D.47 does not overlap 44 and 45 at all - the
digest input path holds zero ResolvedType references across its four functions,
and the premise was a homonym, zone against @storage. M1.D.35's recorded blocker
was dissolved by M1.D.21 one session earlier in the same milestone. M1.D.41's
sixteen renames are unsatisfiable.

The standing result: A COUNTER-MEASURE WRITTEN DOWN STOPS NOTHING UNTIL AN
INSTRUMENT EXECUTES IT. dead_tests.zig:920 names the b.path(spec.path) trap in the
repository's own words and the linter resolves that shape; my extraction walked
into it anyway and under-counted M1.D.41 by exactly three. Tenth instance of the
selector family and the first where the parade existed before the error.

Beside it: one retention point measured instead of implemented, the prompt having
said two - a string is the only arena form that can reach an event field, and
captureEventFilter already copies it. Coverage by accident of spelling measured
directly - return a + "y" refused where return "x" + "y" was not, both parking the
same handle. Four errors of mine, every one caught by an instrument and none by
re-reading, plus a fifth in the closing tally itself. Three blind probes in one
gate whose first cause was ONE CHARACTER, wait(1s) against wait(1.0s), with both
negative controls reading zero. And the latent defect in allocateSlot caught
before it shipped: it returned chunks.items.len - 1 unconditionally, a wrong
location the moment an earlier chunk can be chosen.

Floor re-derived from the suite at every gate and never carried: 2379 -> 2395,
windows 2393. Green at Debug/f32, ReleaseSafe and -Dphysics_f64=true; lint
conservation OK, fmt, bindgen-verify, and forge-determinism with its eight
witnesses byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measurement only; nothing written, because the entry's own condition was met:
one part does not reserve.

The world mutations of Interpreter.compile are exactly four sites - Pass A's
fresh arm (registerComponentRaw, and for a resource addResource then
initResourceCollections), finalizeRequires, the TagSet fresh arm and the builtin
time resources' fresh arm. Passes B to E are world-read-only. Enumerated at the
code, then put through a read-only adversarial audit of 37 agents, which added
two side effects: getMutResource sets dirty on a fresh resource with a collection
field and on no other, and finalizeRequires frees every live closure before
recomputing any.

OOM sweep with FailingAllocator at every index and resize_fail_index = 0, both
controls firing in all three scenarios. First compile: 92 fail points, 79 change
the world, and after a retry 10 resources are never seeded, 2 collection slots
are null and 45 string pointers dangle. Reload adding types: 3, 2 and 23 of 60.
Plain reload: 2 of 44 destroy the live @requires closures. The retry does not
repair: freshness is idOf != null, so seeding is skipped for good.

Everything inside the registration window reserves - each fallible call is an
allocation sized by the pre-mutation world, registry and store, or a refusal
decidable from it; ids are positional, so new ids and the staged closure are
predictable.

What does not reserve is OWNERSHIP. String defaults are immortal blocks owned by
the compile's errdefer, and the committed registration writes their address into
the world. A failure after the commit - passes B to E, which read the committed
registry and so cannot precede it - frees what the world points at: 12 of the 12
post-registration failures dangle, and not only on exhaustion. The same split on
the path that succeeds, measured: tearing the previous interpreter down after a
reload zeroes the live session's collection and string pointer, and the string it
was reading belonged to the old interpreter.

Found by the same instrument in the same function: nine leaks from one site
(rule_descs.append after compileRule), six more leak sites from the audit, one
double free verified at the source, one OutOfMemory swallowed by catch continue,
and one u16 offset panic decidable from the AST.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
engine-zig-conventions.md §12 now has four flat prohibitions; the new one bars
decision reasoning from a code file even when it passes the three criteria.
CLAUDE.md carried the criteria and none of the prohibitions, so they never
reached a session start; it now carries all four.

The set was reproduced first: c3e6d31..f4a92da added 372 comment lines for
392 of code, 24 blocks of six or more totalling 285. Each of the 53 blocks was
judged as one unit - kept whole, reduced to the shortest wording that keeps its
contract, or removed whole - from read-only per-file proposals, each reviewed,
applied by a script that refuses any unit holding a non-comment line.

Result: 106 comment lines for the same 392 of code, and 2 blocks of six or more
where there were 24; both are pre-S5 paragraphs touched only to correct a
sentence S5 made false. Verified on the diff: comment -444/+106, blank -2, code 0.

The adversarial pass was cut at 9 of 18 agents by a usage limit; the five
unaudited files were audited by hand and the four unrefereed findings verified
at the source. All four held and are fixed - two dropped wrong-fix guards
(bindgen-verify against HEAD, why @requires stays out of the digest), one
sentence that described the real digest as the wrong one, one grammar pointer -
and the hand audit restored a fifth guard in dead_tests.zig.

Comments S5's code had made false were corrected in the same pass and counted
apart: the orphan rule still listed among validateTraitImpl's checks, "0 false
refusals", allocateSlot filling only the trailing chunk, a drained chunk never
refilled, content_digest missing from the digest's tuple, fast_paths importing
support.zig only, among others.

Found and not fixed, each being a code change: the tagset_component_name
constant is read by nothing in interp.zig, so S5/G4's "cannot disagree" is
false; vk-gen-check checks nothing (it looks up bindgen-verify before creating
it; CI runs bindgen-verify directly); and etch-grammar.md contradicts itself on
annotation trailing commas, §1.5 against §21.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes M1.D.43. A registry entry owns the immortal persistent blocks its
default bytes point into and destroys them with itself; the interpreter
and the scene cook own none.

compile() registers through one transaction: every declaration, TagSet
and the builtin time resources are prepared without touching the world,
every refusal and every @requires closure is computed over the predicted
graph, registry and store capacity is reserved, then an infallible
commit adopts entries, buffers and closures. World.deinit is the one
place resource payloads are released.

Same instrument, same function: seven leak sites and a double free in
compile(), an OutOfMemory swallowed by initArrayBlock and initMapBlock,
a u16 panic past 64 KiB (now error.LayoutTooLarge), a leak in
planTableDriven, and descriptor building, which now allocates in an
arena the Descriptors value owns.

Tests: an allocation sweep of compile() in three scenarios with both
controls, the two teardown orders, the 64 KiB refusal, and a sweep of
the nineteen Level-B programs. Floor 2395 -> 2403, windows 2401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ownership decision and its motive, the transaction's four steps and
what was set aside, the defects closed with it, the counter-factual per
form, the outcome counts of the sweep, the behaviour that changed, the
bounds left open, and the gates. Floor 2395 -> 2403, windows 2401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prepareEntry copies every non-empty string default into an immortal
block the entry owns, so registerComponentRaw and the Etch compile
follow one ownership rule and a caller keeps all it passed. An empty
default is written {0,0} on every path. A collection default that names
a container is refused (CollectionDefaultNotEmpty).

Five scene tests released a resource string by hand before World.deinit
released it again: five decrefs on freed memory, now gone.

vk-gen-check depends on the bindgen-verify step itself instead of a
lookup made before that step existed. Every TagSet name in the
interpreter and the codegen derives from tagset_component_name; the
emitted source of the five tagged programs is byte-identical. The parser
cites the annotation_args production of etch-grammar.md 1.5.

Locking tests: the refusal order of a doubly-faulty reload, no default
evaluated on reload, closures that see TagSet and the builtins, every
injected name reserved, and the two raw-path ownership properties.
Floor 2403 -> 2411, windows 2409.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loader copied a resource's raw data into the store and rewrote only
the string slots the string table names, so a scene file with non-zero
bytes at a string offset put a pointer nobody owns in the store, which
rollback, commit and World.deinit then decref. Every string slot is now
zeroed before any allocation.

prepareEntry refuses a field reaching past its default bytes
(FieldOutOfBounds), and both passes of the string-default copy read the
caller's immutable bytes, so overlapping slots can no longer make the
copies outnumber their count.

A false build claim is corrected in each place it stood: vk-gen-check's
old block, the "RENDER IS HELD" note on wired tests, the stub and
refusal counts, the counter-proof manifest, and shaders-check, which no
CI runs. forge-asm-inventory takes each cell's precision, so the f64
build is inventoried. Comments this gate touched hold each fact once.

Floor 2411 -> 2415, windows 2413.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three found items settled, the two bounds closed, the three locks
and their counter-factuals, what the sweep brought back and closed, and
seven findings raised at the stop for arbitration, each measured.
Floor 2403 -> 2415, windows 2413.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A type node's `data` indexes the slab of its own kind. Forty-five sites
read `named_types` through it, and eleven had no guard on the kind: a
resource `xs: int[]` lowered as `xs: i64` with `FieldKind.int_`, a rule
parameter `xs: int[]` beside an `Entity` one was bound to the entity, and
an optional node's data read far out of bounds.

`AstArena.namedTypeName` returns null for every kind but `.named`, and
all forty-five reads go through it, each writing its non-named branch:
the codegen refuses (`UnsupportedConstruct`; `fnTypeZig` becomes
fallible), the interpreter binds `.unit`, skips, or refuses
(`InvalidProgram`, `RuntimeFailure`), and the guarded sites keep the
branch they had.

Two adjacents on the same lines. `checkRule` now refuses every non-named
rule parameter with the message it already carried, instead of only an
unresolvable one. The interpreter resolves `type` aliases on rule
parameters as the codegen and the checker do, so `type Ent = Entity`
makes the entity parameter and `type Seconds = float` binds a float.

`no_raw_named_type_read` flags any `.named_types` access outside
`src/etch/ast.zig`, with a bad fixture run through the real binary.

Floor 2415 -> 2436 / 2434, measured: 2417/2436 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The resource witnesses cannot tell the struct emission from the
registration: both read the same fields and the struct pass runs first.
An event reaches only the struct emission, unchecked.

Floor 2436 -> 2437 / 2435, measured: 2418/2437 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A parameter type that resolves to nothing was refused; a return type
was resolved and never checked, so `fn f() -> Nope` type-checked. Both
fn and method returns now carry the diagnostic their parameters do. No
corpus program declares one: the suite stays green.

Floor 2437 -> 2440 / 2438, measured: 2421/2440 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item 1 of S5/G8 ter. A literal that overflows its type, and a constant
whose folding overflows or divides by zero, are refused when the program
is checked; at run time both backends follow part1 §12.4: overflow
panics in Debug and ReleaseSafe and wraps in ReleaseFast, an integer
division by zero panics in every mode.

The fold. `const_eval.fold` is the one folder: the checker admits a
constant through it and the interpreter, the scene cook and the codegen
store the value it gives, so a default the checker admits cannot fall
back to zero at compile. Its arithmetic is checked in every mode. Field
defaults, top-level consts and field filters fold through it (E1101 on
overflow, division by zero or a non-finite float); `true or false` now
folds instead of storing false, and a string concatenation default,
which no backend could materialise, is refused.

The literals. Every int literal fits `int` and every float and duration
literal is finite, in any context, with `-9223372036854775808` judged
negated. `literalTypeFits` refuses a literal outside `i32`, `u32` or
`f32` (E0200), reached by all 27 of its callers, by resource instance
values, params-block defaults and collection literal elements against a
`let` annotation. A non-literal or negative array size or fill count is
E1101 instead of an unknown type or a zero length.

The run time. `value.zig` holds the mode policy: add, sub, mul, neg and
`i64.min / -1` return null where overflow panics and wrap otherwise.
A compound assignment reports the typed cause the binary operator does.
`as i32` / `as u32` narrow, `as f32` rounds, a float whose truncation
does not fit its integer target fails in every mode. A store into an
`i32` / `u32` field wraps or refuses instead of an `@intCast` abort. An
inclusive range to `i64` max ends, and `tick_until` converts its budget
checked. The codegen routes integer `+ - * / %`, negation, compound
assignment and narrowing casts through prelude helpers that choose by
mode, where it emitted native operators (UB in ReleaseFast, and runtime
signed `/` did not compile); float `%` lowers to `@rem`.

Adjacents on the same paths: an omitted struct field takes the zero of
its type, or its string or enum default, where the interpreter gave int
0; an `i32` filter accepts an int literal it falsely refused; the scene
cook's `registerOne` mapped `SchemaChanged`, `FieldOutOfBounds` and
`CollectionDefaultNotEmpty` to OutOfMemory through an `else`.

Floor 2440 -> 2490 / 2488, measured: 2471/2490 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The interpreter refuses at compile a default whose folding fails, where
it left the slot zero; the codegen emits a constant default as its
folded value and a runtime literal without its separators, and a
compound integer assignment through its helper. None of these had a
test that tells the form from the one it replaced.

Floor 2490 -> 2495 / 2493, measured: 2476/2495 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item 3 of S5/G8 ter. The checker's resource collection branch validated
the element type and `continue`d past every default check, so
`xs: int[] = 5` passed, and the interpreter kept only what it could
read: an array or map literal, element by element, dropping any element
that failed to fold and ignoring every other form.

The checker: a `T[]` default is an array literal, a `[K: V]` default a
map literal, a `Set<T>` default `Set.new()` (stdlib §4.5), each element a
constant of the declared element type (E1101 otherwise), a variant of it
for an enum element (E0105), and a literal within its range; a wrong
shape is E0200 for a constant and E1101 for anything else. A default on
an element type already refused (E0222) is not checked a second time.

The runtime: a fill default `[v; n]` materialises `n` copies where it
kept one, an enum element is stored as its variant where it was
dropped, a repeated map key keeps its last value as the runtime literal
does where the default kept both, and any form the checker refuses is
`InvalidProgram` where it gave an empty container or dropped the
element. The scene cook refuses a resource with a collection field,
which it wrote into a `.scene.bin` the loader always refuses.

Adjacents: a scalar enum default is checked (E0105, E0200), where
`m: Mode = .nope` stored the first variant; a float map key or set
element is refused by the Hash bound (E0601) as it is on a `let`; a
Vec3, Color or Duration element is E0222, which the element set's own
text already limits to scalar POD, string and enum.

Floor 2495 -> 2519 / 2517, measured: 2500/2519 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The five unchecked refusals shared one test, so a mutation of one form
was hidden behind the first source that still refused. The no-cascade
witness now uses a default the element check would refuse if it ran.

Floor 2519 -> 2523 / 2521, measured: 2504/2523 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item 7 of S5/G8 ter. A resource is no entity column, so `@requires`
naming one is refused (engine-ecs-internals §3). The registry had no
notion of the difference: a Zig component requiring `GameTime` resolved
at the first compile, and the lock that pinned it now pins the refusal.

The registry records a kind (`ComponentDesc.kind`, `.component` by
default, an addition that moves no protocol version). A closure whose
requisite is a resource is `RequisiteIsResource`, and a resource
carrying requisites `RequiresOnResource`, both from `closeOne`, which
every closure computation reaches. `World.addResource` records the kind
and refuses an id a closure already requires, so a host making a type a
resource after registering it is refused in either order. The
interpreter declares the kind of every type it registers, builtin time
resources included.

Adjacents on the same paths:
- The scene cook maps the two new refusals, its registry being its only
  `@requires` guard.
- The scene loader refuses an archetype naming a resource and a resource
  section naming a component, which a crafted scene could do: the
  component was installed as a resource.
- The codegen refuses `@requires` (`RequiresUnsupported`), which it
  dropped: its `register()` carries no requisite and resolves no closure.
- A reload that changes a type's kind or its `@requires` is refused as a
  schema change, where it was accepted and never applied; a changed
  storage mode stays accepted and unmigrated, per §13.
- A builtin resource name held by another type is refused, where the
  interpreter mapped any holder as the resource.
- E0506 names a resource as one.

Floor 2523 -> 2542 / 2540, measured: 2523/2542 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item 4 of S5/G8 ter. `annotation_arg = expression | IDENT ":" expression
| IDENT`. The parser looked for a name by consuming a leading ident and,
when no `:` followed, rebuilt an expression from it by hand, which
skipped every form `parsePrimary` owns: `@tag(v as f32)` was refused,
`@tag(none)` and `@tag(some(1))` became an ident and a call, and
`@tag(Name: 1)` was refused because a capitalised name was not looked
for. The argument is now a name by two-token lookahead, and otherwise
an expression.

Adjacents on the same production. The annotation name is an IDENT, as
§1.5 writes it: a capitalised `@Unit(...)` was parsed and accepted as a
custom annotation, and no source in the tree uses one. `@storage(none)`
stays E0503, a value outside the domain, and not E0504, `none` being a
constant. The event and structural observer readers refuse a named
argument, as the tag and requisite readers already did: `@on_event(e:
Hit)` was read as `@on_event(Hit)`.

Floor 2542 -> 2553 / 2551, measured: 2534/2553 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item 6 of S5/G8 ter. `asset_field = [ annotation ] , IDENT , ":" ,
asset_value , [ "," ]`, in a file the user edits by hand: the reader
refused the comma between fields, the one form the ruling names, and
much of the rest of the grammar. It now takes:

- the comma between fields;
- a field annotation, parsed and not kept (the model carries none);
- comments;
- the six string escapes, decoded, with any other refused;
- triple-quoted strings with their common indentation removed;
- `_` digit separators where the lexer admits them.

It refuses what the grammar does not produce: a `+` sign, an exponent,
`1.`, a float that is not finite, an unescaped `{`, a raw newline in a
simple string, a boolean literal as a field name, and content after the
construct. The §3 schema is enforced where it was defaulted or ignored:
each fixed field and block present once and of its kind, no other
top-level field, `type` an identifier, and `uuid`, `source_hash` and
`extracted.blob` in canonical form (`SchemaViolation`, a new variant).

The writer escapes what the reader decodes, so a string holding a
quote, a backslash, a brace or a line break round-trips. The glTF
importer refuses a non-finite position or bound, which the writer had
no literal for and emitted as `nan` or `inf`.

`asset_cook` minted a fresh UUID for any intermediate the reader
refused, against §3's "Jamais régénérée"; `existingUuid` makes an
unreadable intermediate an error. Measured end to end: a second cook
keeps all three UUIDs, a corrupted intermediate fails the cook.

Two fixtures change value to satisfy the schema: the round-trip hashes
and the float test's empty `uuid` and missing blob.

Floor 2553 -> 2573 / 2571, measured: 2554/2573 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The importer refuses a bound the intermediate format has no literal
for; the test gives an accessor a maximum of 1e40, infinite as an f32.

Floor 2573 -> 2574 / 2572, measured: 2555/2574 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three nested `zig build` invocations (the triangle example, the
synth_100 proof, the access counter-proof cases) ran with no option at
all, so each compiled for the host's native CPU, in Debug, at f32,
whatever cell ran it. They now receive `-Dtarget`, `-Dcpu`,
`-Doptimize` and `-Dphysics_f64` from the outer build, and each
sub-project declares `physics_f64` and passes it to its `weld`
dependency. CI passes the matrix precision to the two nested steps it
runs.

Measured: `verify-synth-100` succeeds; `ecs-access-counterproof
-Dphysics_f64=true -Dcpu=baseline` 8/8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`dead-tests` walked the lint paths only, so a test block under
`examples/` was invisible to the conservation check whether or not a
step collected it. Extended, the guard names one:
`examples/vertical_slice/math.zig`, one block, reached by no test root
because the slice module is imported across a module boundary, which
collects none of its tests. It is std-only and now has its own test
root.

Floor 2574 -> 2575 / 2573, measured: 2556/2575 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The decoder refuses a position the intermediate format has no literal
for, and nothing witnessed it: under an accessor without bounds, the
computed bounds are infinite too and the bound check refuses first.
The test gives one vertex an x of +inf under finite accessor bounds,
the only case the position check alone decides.

Floor 2575 -> 2576 / 2574, measured: 2557/2576 passed, 19 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gate never ran. In CI, `test-ppm-psnr` reported `2/3 tests passed
(1 skipped)` at 9d2609c: the example runs from its own directory and
writes `examples/triangle/out/smoke_test.ppm`, while the gate read
`out/smoke_test.ppm` at the repository root, found nothing and skipped.
The artifact upload, pointed at the same wrong path, uploaded
`test-output.txt` alone (294 bytes).

The gate now reads where the example writes, fails and names the file
when the capture or the golden is absent, and always re-runs, since it
reads a file no input of the step declares. It leaves `zig build
test`, where no cell can produce the capture and it could only skip,
and `dead-tests` declares it uncollected. The workflow's `exit 0` for
an absent golden goes: the golden is committed.

`tests/render/capture.zig` is deleted: its spawning test duplicated
the smoke job's triangle run plus this gate and skipped on every cell
(no triangle binary is built in `zig build test`), and its two helper
tests duplicated the gate's own.

Witnessed locally: no capture fails with `PpmAbsent` naming the path;
a copy of the golden passes 3/3; the same copy shifted by 8 in every
channel fails.

Floor 2576 -> 2570 / 2568, measured: 2553/2570 passed, 17 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
guysenpai and others added 30 commits September 30, 2026 00:57
S5/G9 duodetricies: bad9fe1, 09d6be2 and 5db3bec, each with its red
run and counter-factuals; the defects three read-only audits found in
the option and closed with it; the deviation on witnesses written after
their fix; residuals and readings for Guy. B6 carries the two STOPs: an
optional field that is stored, and a collection of a declared type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
S5/G9 undetricies, read only at a7a795c: every site of types.zig that
accepts, propagates, produces or refuses unknown, 417 lines, the full
table in briefs/artifacts. 67 produce it with no diagnostic, in twelve
families, four unreachable today. One is a defect of 5db3bec, confirmed
by a probe: an optional slot shared by two different generic scopes.
Guy's rulings on B6 recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The census's F11, a defect of 5db3bec: an optional type node's slot
was keyed by whether some generic scope was open, not by which, so a
field `P?` read inside `fn f<P>` and again inside `fn g<T>` met two
payloads under one key, a Debug panic and undefined behaviour in
ReleaseFast. A node now holds one slot per payload, found by equality:
a scope changes a node only through its names that are type parameters
of the file, so it gets one slot per subset of them, bounded by the
scopes the file can open. Floor 2846 -> 2848 / 2846, read from the suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The census's F1: a collection whose element, key or value was not a
builtin typed unknown, so `let a: P[] = 5` was accepted and a parameter
`P[]` refused E0102. The element is now a pointer, as an optional's
payload is: a type node's in a slot reserved before checking, one per
type its generic scopes can give it, and one built at check time is
allocated. Array and map literals join their elements as branches do,
two arrays of one element and two lengths to a dynamic array; an
element fits its collection with its own wraps and shorthands; generic
maps and sets unify and substitute. A map key and a set element must be
Hash + Eq wherever the type is written, so a struct or an array is
refused there, E0601. Slicing a set, which typed `T[]`, is refused. The
codegen fails loud on an array element type it cannot spell. Floor
2848 -> 2853 / 2851, read from the suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
S5/G9 duotricies: 6d9bb1c and c55fe1a with their red runs and
counter-factuals, one prediction refuted and the unit witness added for
it; the declared changed tests; what F1 leaves raised: shared handles
against value semantics, enum keys, T[N] keys, collection fields, a
dynamic array into a fixed slot, .{...} as an element.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every write the interpreter can perform on a shared value aliases;
structs copy in the codegen and alias in the interpreter, and dynamic
arrays share in both, differently. Five questions raised as B7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A nominal type carried a name of the checked file's pool, so a type of
another file was unknown unless this file bound it under the same name,
and a local type shadowed a foreign one of its name. It now carries its
declaration. A foreign signature resolves through its file's aliases,
fixed arrays and qualified types, and a foreign Error is this file's.
A qualified type resolves an alias member, a qualified field is judged
as a named one, and a qualified type naming no type is reported once.
Service signatures are checked, a whole-module import's components and
resources join the runtime-name check, and one method given one type
under two names is E0218.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Of the 22 lines, 18 close and four stay for causes of other families;
35 jokers remain. Three findings outside F2 and F3 raised as B8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A binding of get(T) wrote through itself as soon as it was let mut, a
write through '?.' checked clean and failed at run time, and a closure
body wrote a capture through a field or a mutating method. A binding
now carries the mode of the ECS ref it holds, and one walk judges every
write target and mutating receiver: read-only refs, '?.' and captures
are refused, a get_mut binding is rebound only to a get_mut, and a
compound operator on a string field is refused as on a string binding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A copy of a local whose Zig type the lowering did not track was
annotated i64: a fixed array, an anonymous struct, a struct field, a fn
or method result, a closure, an optional and self in a mut self method
did not compile once copied. Their types are now tracked or left to Zig,
a copied closure keeps whether it throws, and a dynamic array, map or
set bound by let to a non-empty literal is a var so it can be filled.
Corpus program 90 copies each and reads the copy in both engines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copy on write stops on the mechanism for arena values: three texts
disagree and ruling 2 decides a mechanism. What does not depend on it
is done in two gates; B9 raises the mechanism and four more questions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A binding, a field or an element read as a value is copied: a struct,
a rule-arena collection or an optional holding one with its contents; a
persistent handle is shared and counted (etch-reference-part1.md §5.2,
2ff2cc54). A receiver and an assignment target stay places. A closure
captures a copy of its environment.

A mut self method writes a replaced self to the caller's binding or
struct field (§8.3), in the sync call and in the async call frame. An
assignment evaluates its right-hand side, then its place (§7.9); a
field write through a component ref re-resolves the ref after it.

The arena for's shrink guard is gone: the iterated value is a copy.
The test asserting that guard is replaced by a table case iterating
the entry copy. Floor 2857 -> 2859 / 2857.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every push, pop and insert on a persistent collection goes through one
barrier after its arguments: the write lands in the block when the
receiver's place holds it and nothing else counts it, else in a copy
the place is rebound to (etch-memory-model.md §4.4). A resource field
is now a place, and a write through it marks the resource changed.

A for over a persistent collection iterates it as at its entry, sync
and async; its shrink guards are gone. The test asserting the sync
guard is replaced by a table case. Floor 2859 -> 2860 / 2858.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An element is a place: a[i] = v, a[i] op= v, a[i].x = v and a
mutating method on a[i] check and run in both engines (ruling 4). A
slice is refused, being a copy; m[k] = v inserts and a compound
operator on a map entry is refused (§11.3). An ECS ref reached through
a field or an element is read-only.

The interpreter writes an element after its right-hand side (§7.9), a
persistent one through the copy-on-write barrier; self = v replaces an
element, and each slot of [v; n] is its own value. The codegen binds a
right-hand side that is not pure before forming its place, inserts a
map entry, writes a compound element through one pointer, and types an
element read. Floor 2860 -> 2864 / 2862.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dynamic array, a map or a set read as a value is cloned in the frame
arena (etch-reference-part1.md §5.2), a for's iterable included; a
receiver, an element's array and an assignment target are emitted as
the storage they name. A map or a set type is named once, so a copy
type-checks; a slice of a fixed array is a copied dynamic array.

The interpreter's corpus runner fails on a runtime error it ignored.
Program 92 covers each copy, and a struct argument measured not to
alias its mut self receiver in Debug and ReleaseFast.
Floor 2864 -> 2865 / 2863.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A closure body is typed at its literal, its parameters bound to their
annotation, so a closure stored or never called is checked, its writes
to a capture included; a call types it again with its arguments and
drops what that repeats.

E0223 refuses an async for over a dynamic array or a map that is not a
resource's collection field when its body suspends: the for iterates a
copy (etch-reference-part1.md §5.2), and a copy of an arena collection
lives in the arena another body resets. The test pinning the map
literal as accepted is replaced. Floor 2865 -> 2866 / 2864.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gates 3 to 7 of S5/G9 quinquatricies: (A) in the interpreter, the
persistent barrier, element writes, (A) in the codegen, closures
checked where written and E0223 on an async for over the arena. Each
with its predictions, its red, its counter-factuals and its floor.
B10 raises what copy on write found outside itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ruling 3: etch-resolver-types.md §16.4 gives T[N] Eq when T is Eq, and
etch-stdlib.md §4.3 already gave it Hash when T is Hash, so T[N] is a
valid map key and set element, compared element by element at run time.

- Checker: isKeyType and isEqType gain an array_fixed arm. A float
  element stays refused as a key (floats are not Hash) and is compared
  with ==; a struct element stays refused for both.
- Interpreter: valueEql compares two arrays element by element through
  itself, so nested arrays, strings by bytes and NaN follow the scalar
  rules. Value.eql keeps array_ref and array_persistent false.
- Codegen: == and != on two fixed arrays of a scalar element lower to
  std.meta.eql. A string element fails loud, std.meta.eql comparing a
  slice by its pointer. A T[N] map or set key stays fail-loud, as a
  string key does.

Two equal keys in one literal keep the last write: a convention both
engines share, not spec text. The interpreter's length test in valueEql
is unreachable through the checker; while a dynamic array can still
enter a T[N] slot (the next gate), no test reaches it either.

Tests added: types "a fixed array of a hashable element is a key, and
of an Eq element is Eq" (17 accepted, 11 refused); interp "a fixed array
is compared and looked up element by element" (17 runs); corpus program
93_fixed_array_eq in both engines. No test removed or weakened. Floor
2866 -> 2868 / 2864 -> 2866, collected by the suite and by the closure.

Twelve counter-factuals, each alone: all bite. Removing the Eq arm
reddens all 17 interpreter runs, not the 8 predicted, the prelude's own
function comparing two fixed arrays. The literal-arm probe first failed
to compile and measured nothing; redone, it bites at the first
literal-to-literal comparison, not at the one predicted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ruling 5: a dynamic array entering a T[N] slot of any length is a
defect, and a known length must match. Read statically: a T[] of any
origin does not enter T[N]; a literal's length, a fill's count, a
T[M]'s M and `[]` (zero) must equal N. E0200, E0204 at a return or a
tail, E1764 in a data entry: no new code.

- elementsFit refuses a dynamic array into a fixed one. Every slot that
  reaches valueFits gains it: let, assignment, element and field write,
  argument (fn, method, closure, generic, foreign, service), return,
  tail, push, insert, m[k] =, literal elements and map values, T?, some.
- lengthMisfits walks an untyped value through blocks, if branches,
  match arms, some, array elements and map values, so `[]` and
  `[[], []]` are judged by their length where they carry no type. It
  runs before fitsValue's unknown short-circuit.
- joinBranches counts a branch that is `[]` on every path as an array
  of length zero: with a fixed array of another length it joins to T[],
  so `if c { [1, 2, 3] } else { [] }` stays accepted, as T[].

No engine changes: neither backend sees a refused program.

Tests added: types "a fixed array's slot takes only a value of its
length" (48 refused, 18 accepted); three rows in import_resolve_test's
foreign_refused (a foreign parameter, a foreign tail, a service
parameter). Three accepted rows mixing a length and `[]` pin the join,
not the spec, which makes `[]` check-mode only under E0207, a code
diagnostics.zig does not carry. No test removed or weakened. Floor
2868 -> 2869 / 2866 -> 2867, from the suite and the closure.

All 51 refused rows measured red first, each with no diagnostic.
Fifteen counter-factuals, each alone: all bite; five reached more or
fewer rows than predicted, each for a reason now recorded.

Changed verdict to raise: `if c { d } else { some([1, 2, 3]) }`, with
d: int[], was accepted typed int[3]?; it is now E0200, the join not
composing the optional rule with the length rule. Left open: a loop's
`break []` into T[N], params-block defaults of any non-builtin type,
and `let e = []`, all still accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ruling 6: `.{…}` as a collection element and under `P?` is one defect,
and the literal resolves by the type its slot expects, as `.south`
does. Check mode (etch-resolver-types.md §4.1, §4.2):

- checkExpr(id, expected, ctx) threads the slot's type through some,
  array and map literals (elements, fill, values), blocks, if and
  match. A let, an assignment to a binding or a place, a return, a fn's
  and a method's tail, a fn's, a generic fn's concrete, a method's and
  a closure's argument, push, insert, ?? and a struct literal's field
  values call it. A program holding no `.{…}` types as before.
- Each resolved `.{…}` is recorded in AstArena.anon_structs, as
  enum_shorthands records an enum; the interpreter and the codegen read
  it. The interpreter's let and nested-field special cases are gone.
  The codegen keeps its let guard: `.{…}` under an optional annotation
  fails loud there, the codegen holding no optional of a struct.
- E0210's message drops "(let annotation or typed field value)".

Tests added: interp "an anonymous struct literal takes the struct its
slot expects" (30 runs); types "an anonymous struct literal is refused
where its slot names no struct" (8 rows); type_node_kind "the codegen
refuses an anonymous literal under an optional annotation"; corpus
program 94_anon_struct_positions, 13 positions in both engines.

Changed test, declared: "anonymous struct literal resolves in check
mode, rejected without an expected type" asserted E0210 for a fn
argument and now asserts no diagnostic, part1 §6.10's own example being
that position; its header naming "the two wired contexts" is removed.
No test removed. Floor 2869 -> 2872 / 2867 -> 2870, from the suite and
the closure.

Red first at 2431bc6: 28 runs E0210 (the two the special cases served
green), five refusals under E0210, the codegen test, program 94. Thirty
counter-factuals, each alone: all bite but one predicted green, which
shows the codegen guard's only witness is its own test. The map-value
probe first failed to compile and was redone.

Left out, raised: throw, a loop's break values, a data-entry field P?,
service arguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gates A to C of S5/G9 sextricies: T[N] as a key compared element by
element, a fixed array's length, and `.{…}` resolved by its expected
type. Each with its predictions, its red, its counter-factuals and its
floor. B11 raises what they found outside themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A method call whose receiver is a place rooted in a binding or an ECS
accessor now evaluates its arguments first and resolves that place
afterwards, in the interpreter and in the generated Zig, per
etch-reference-part1.md §7.9. A value receiver, and one under `?.`, is
still evaluated first; `?.` on `none` evaluates no argument. A throw, a
return, a break or a continue raised by an argument now skips the call
on every call path: method, fn, closure, service, and the async forms.

The codegen binds each argument that is not pure to a typed temporary
before it forms the receiver's place.

Changed tests:
- cow_runs: "a push whose argument rebinds its receiver leaves the
  resource" (3) becomes "... pushes onto what it bound" (32), the order
  it asserted being the one §7.9 refuses; two rows join it (327, 33).
- index_runs: "a push whose argument removes its receiver element
  leaves the resource" (30) is replaced by "an index write whose index
  removes its receiver element leaves the resource" (130); the push
  case becomes the Zig test "a push whose argument removes its receiver
  element fails loud".

New: corpus program 95_method_call_order; the call-order table and
tests on traits, throws, type-name arguments, extension names, the
tick count, `has_extension` and service calls. Floor 2872/2870 to
2881/2879, measured from the suite and matched by the closure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `let` whose binding no later statement of its scope, and no value the
scope ends with, names is no longer declared in the generated Zig: its
value is evaluated and discarded, `_ = @as(T, v);` or `_ = &(v);`, and
an empty collection is not emitted. An unread component alias reads its
slot. Zig refused every such program as an unused local; the
interpreter already ran them.

The identifier walker now reads `some(…)` and map literals, and counts
a catch body only where the try body can throw, as the emitter does.
The set path refuses `Set.from` over a non-literal before it can drop
an unnamed set.

New: corpus program 96_unused_lets and the test "the codegen refuses a
set built from a binding, even unread". No test changed or removed.
The cooked corpus moves in program 96 only. Floor 2881/2879 to
2882/2880, measured from the suite and matched by the closure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A value breaking out of a `loop` is now checked against the type the
loop's slot expects, as an `if`'s branches and a `match`'s arms are:
`break .{ … }` resolves to the expected struct, and an array literal
breaking into a fixed array takes its length, `break []` into
`int[3]` being refused. The length walker and the every-path walker
look through a loop's recorded break values, so a loop breaking
`none` on every path counts as a join branch.

A break finds its loop by index rather than by pointer: typing the
value can push frames and move the list.

New: the tables loop_break_refused / loop_break_accepted and their
test, the break_runs table and its test, corpus program
97_loop_break_slots. No test changed or removed. Floor 2882/2880 to
2884/2882, measured from the suite and matched by the closure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A params-block default is now checked with the field's declared type as
its slot and refused, E0200, when it does not fit: an enum, a struct,
an array or an optional default is typed at last, and a builtin one no
longer has its mismatch discarded, `b: bool = 1` being refused. A type
the checker does not resolve, such as an engine one, is not judged.
An audio_graph's defaults are checked in a validate pass of their own
instead of pass 1, so they see the whole file and its imports.

A shader keeps its range-only check: a shader default of another type
is E1613 in the spec, a code with no variant.

New: the tables params_default_refused / params_default_accepted and
their test, two foreign_refused and two foreign_accepted rows. No test
changed or removed. Floor 2884/2882 to 2885/2883, measured from the
suite and matched by the closure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A data entry's field value is now typed with the field's declared type
as its slot, so `.{ … }` resolves under a `P?` field, as an element of
`P[]?` or `P[3]?`, as a map value, under `some` and beside a spread,
and a mistyped or incomplete one is refused with the struct literal's
own code. A `.variant` is resolved once: the special case that typed
it before `valueFits` did is gone, and with it the duplicate E0105.

Without a rule context a loop checks its first top-level break against
the slot and records it, so a length rule sees through it there too.

New: the tables data_entry_value_accepted / data_entry_value_refused
and their test, a shorthand_refused row, an anon_runs row and two
foreign_accepted rows. No test changed or removed. Floor 2885/2883 to
2886/2884, measured from the suite and matched by the closure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`[]` with no expected type is now E0207 EmptyArrayTypeAnnotationRequired
(etch-resolver-types.md §4.2, §4.4): an unannotated `let`, a join no
slot expects, an iterated, compared or received literal, an unannotated
closure argument, a void fn's tail. Under a slot it is checked: an
array literal under any known type is check mode, and `[]` under a type
that is no array is a misfit of the slot's own code, `Vec3 = []` asking
three elements and getting none.

Map keys, set elements and index keys take their collection's key or
element type; data entries, instance fields, event fields and service
arguments are checked against their declared types, so `.{ … }` and
`[]` resolve there too. A declared type the checker cannot resolve is
still a slot, so a type parameter or an unmodelled type reads no E0207.

Changed tests: the three fixed_length_accepted rows "branches / elements
/ arms of a length and empty, joined dynamic" move to the new refused
table as "…, nothing expected", their `[]` having no slot and now E0207.

New: E0207 in diagnostics.zig and its stable-code lines, the tables
empty_literal_refused / empty_literal_accepted and their test, two
foreign rows, the service `.{ … }` row, and the test "an empty literal
for a field of an imported resource in a scene file takes its type".
Floor 2886/2884 to 2888/2886, measured from the suite and matched by
the closure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A type's postfix `[N]`, `[]` and `?` now compose in any order, each
wrapping the type before it (etch-grammar.md §264, §267): `int?[]`,
`int?[3]`, `int?[]?`, `[int: int]?[]` parse at every position a type is
written. A `?` directly after a `?` is not read, so `int??` and
`int? ?` stay unwritable, and `??` stays the coalescing operator.

The codegen refuses what it cannot spell: an array literal whose first
element the checker wraps into an optional, or a later one wrapped
under an element type that is no optional, and a `let` whose annotation
is an optional of a payload it does not name, such as `int[2]?`, which
it used to emit as a non-optional binding.

New: four parser tests, the tables optional_element_accepted /
optional_element_refused and their test, optional_element_runs and its
test, five codegen tests, corpus program 98_optional_elements. No test
changed or removed. Floor 2888/2886 to 2899/2897, measured from the
suite and matched by the closure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `.variant` under a known slot now takes the slot's enum wherever the
expected type reaches it: an element of `Dir[]` or `Dir[N]`, a fill, a
map key or value, `some`'s payload, a nested array, a block, a branch,
an argument, a return. Under a slot that is no enum it is E0105. Each
shorthand is reported once, however many places fit it, and one a
closure reads at a call is no longer left pending as ambiguous. The
struct-literal and instance-field special cases go, the new arm
covering them. `some(x)` under a slot that is no optional checks `x`
against the slot, so `let d: Dir = some(.north)` is E0200.

New: ten shorthand_refused rows, the table shorthand_accepted and its
test, fifteen shorthand_runs rows, three foreign rows, and the codegen
test "the codegen refuses an array of enum variants". No test changed
or removed. Floor 2899/2897 to 2901/2899, measured from the suite and
matched by the closure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
S5/G9 septtricies: gates D to K, each with its predictions, its red,
its counter-factuals and its floor, from 2872/2870 to 2901/2899. B12
lists what they found outside themselves and the questions they leave:
B11.1 and B11.5 not ordered, `int??`, the gate order, E1613, and the
measured defects beside them. The census gains F13 and F14 (B11.10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant