The four wwstage whole-struct field-copy sites (cgenexpr.ww) copied
`ssi.totsize` — the slot-padded, round-8 structinfo size — instead of
the SOURCE struct's natural size. cstage copies `f->type->size` (the
field struct's aligned r.size; cmd/w6c/cgen.c:5302). wwstage over-copied
into the field's slot padding.
Fix: length = copysrcnatsize(c, n.rhs) = tichase(src.type_).size, read
from the SOURCE node's stamped tinfo (the checker's natural r.size,
check.ww N_TSTRUCT). This never reads structinfo / fi.foff / fi.fsz, so
it is correct at HEAD unconditionally and independent of the
registerstruct natural-offset change (#44/#55) that follows — a pure
wwstage convergence onto the length cstage already emits. Distinct from
the existing structnaturalsize (structinfo max(foff+fsz), a #44-coupled
source).
LENGTH ONLY. The ragged-tail completeness (both stages' field copies
inline a tail handling only {4,1}; a natural size %8 in {2,3,5,6,7}
falls through to an 8-byte MOVQ over-read) is a SEPARATE both-stage
class — cstage cgen.c:5302 has the identical incomplete tail — folded
into #73 (route both stages' field copies through the canonical greedy
aggcopy emitter). Touching only ww's tail here would create a gate-blind
cs!=ww on narrow-tail inputs, so it is deliberately left for the
both-stage fix.
No isolated runtime repro: the over-copy writes [natural, totsize),
which under HEAD's slot-padded field layout is the field's OWN padding
(the successor parks at the next slot). It only becomes a clobber once
#44 packs the successor at its natural offset (the 681 ragged_tail_12B
regression that forced this ordering). So this commit is byte-id-clean
and a no-op on the present corpus; its proof is the all-green run plus
the #44 commit that depends on it.
w6a's parsenum diverged from the C twin's strtoll(s,end,0)
(cmd/w6a/lex.c:30) on three hand-written-asm edge shapes (all
gate-blind — w6c emits the canonical $5/$8/-8(BP), never these):
(a) `$ 5` — leading whitespace: strtoll skips it (->5); ww had no
skip and silently encoded imm 0.
(b) `$08` — strtoll base-0 reads a leading 0 as octal, stops at '8'
(->0); ww parsed it as decimal 8.
(c) `-(BP)` — strtoll/cstage require a digit after the sign, so a bare
`-(` is unrecognised operand; ww silently took it as 0(BP).
Add the whitespace skip + octal base-0 detection to parsenum, and the
digit-after-sign guard to the operand scanner — both assemblers now
agree byte-for-byte (a/b) and both reject (c).
Not a Hare item (w6a is ww's plan9-lineage assembler); reference is the
C strtoll twin. w6a embeds into its own combined.ww snapshot; regen'd.
530_w6a_parsenum pins the byte-identity + both-reject matrix.
prev()'s walk-back decremented offs (i32) past 0 to -1 and returned
`more`; a subsequent next() then passed the signed `-1 < len` guard and
read d.src[-1] — a silent OOB decode of a garbage rune (no runtime
bounds net). Hare's decoder.offs is `size`: the underflow wraps to
SIZE_MAX so every `offs < len` guard exits safely (next returns more,
not a rune). Change offs to size and spell prev's loop as the Hare-form
`offs < len` guard; index sites take an i32 temp (ww's slice index is
i32 and `[...]` reads ':' as the slice separator).
No-runtime-net residual: remaining() would silently build a ptr-1/len+1
OOB view when called in the post-`more` state; guard it with a loud
abort (caller contract: don't call after `more`). The offs type ripples
into strings.ww's iterator<->decoder bridge (move/slice) — cast at the
four sites, safe on the rune-return path where offs is in range.
utf8/strings embed into all five selfhost combined.ww snapshots plus the
smoke.combined.ww test amalgamation; all regen'd. utf8test gains
prev_more_then_next_no_oob pinning the closed OOB.
parsefile's recovery fallback chewed an unrecognized top-level
construct to the next ';' without emitting an error or bumping p.errs,
so a typo'd keyword / stray token silently vanished from the AST and
the build succeeded rc=0 with the declared work gone — no link error
catches a dropped @test or unreferenced exported fn. The C twin
(parse.c:1395-1400) errorf+p->errs++ and rejects. Emit errmsg in the
fallback arm; wwstage now rejects in lockstep with cstage.
lib/ww embeds into the w6c/wwdump combined.ww snapshots; both regen'd.
989_unknowndecl_reject pins the reject-matrix on both driver twins.
wwstage parseint dropped the pre-multiply overflow guard the C twin
carries (cmd/wcc/lex.c:156, if (v > (u64)~0ULL / (u64)base)), so any
integer literal exceeding u64 was silently accepted mod 2^64 while
cstage loudly rejected with 'bad integer literal' — a rule-10 stage
divergence and a silent wrong constant. Port the guard before the
multiply-add; wwstage now rejects in lockstep with cstage.
lib/ww embeds into the w6c/wwdump combined.ww snapshots; both regen'd.
989_intoverflow_reject pins the reject-matrix on both driver twins.
When the dynamic section pushed the header past the first page, the
entry point kept its first-page address — every sufficiently large
dynamic binary SIGSEGV'd into the headers. Recompute e_entry as
entry - 0x1000 + text_off in both stages (ELF: the entry must point
into .text wherever it lands). Both stages move in one commit: one
ELF contract; the 989_dynentry gate pins the field and the run.
A DATAR relocation against an undefined data slot was silently
dropped — the relocation vanished from the object. Reject loudly,
matching the cstage single-pass resolution behavior.
wwdump's -c/-r modes emitted output from a garbage parse silently.
Gate on the parse-error count first (the w6c main gate, main.ww:162);
wwstage-only — the C wwdump has no -c/-r modes.
The wwstage driver silently dropped directory entries past a 256
cap; the cstage twin already grows by realloc-doubling. Grow the
same way (seed 8, double) so both stages agree on any directory size.
wwstage's unguarded loop-label push wrote out of bounds at depth 17
(compiler-heap corruption); cstage guarded but emitted a wrong break
target. Loud cap error at the limit, both stages, agreeing wording.
wwstage capped defers at 16 and SILENTLY DROPPED the 17th; cstage
capped at 32. Align the cap at 32 and make exceeding it a loud
compile error in BOTH stages — the silent 16-vs-32 split was the bug
(a defer that never runs is a leaked resource). Both stages move in
one commit: one cap contract.
The try-operand resolution used bare lookups (ident + bare-leaf call);
a cross-module same-leaf collision mistyped the operand. Prefer the
current module. The historic 995 byte-id break attributed to this swap
was contamination from the guard-bug-carrying bundle — re-probed clean
in isolation and at the full stack. The N_DOT module-keyed leaf stays
task #51. Report item [11], lookup half.
The address-of-fn synthesis used a bare lookup — import order could
bind a same-leaf fn from another module, silently LEAQ-ing the wrong
function into a fn-ptr slot. Prefer the current module (mirror
check.c:410/1305). Report item #4 (loud and silent faces pinned).
A type lookup in a bundled build resolved to the newest-installed
same-leaf symbol from ANY module; prefer the current module first
(mirror cstage sym.c:131; the prior attempt's failure was its own
u64-vs-i32 guard bug, not a deeper layer — probe-proven). Also adds
the rule-7 #58 notes at the latent varianterr/scruttype pair and
rewrites the stale deferral block to closing cites. Report item [5].
Passing buf[1:] of a [MAX]u8 as a call arg dropped the default-hi
length (the arg-push N_SLICE arms' fallback covered only named-alias
bases). Same type-table resolve at pushargsrev local+global. This
closes the def-dim dimension family by construction: every N_INTLIT-
keyed dim consumer (cgslice, cgdot, letemitsize, arg-push) now
carries the tichase().alen fallback — grep-proven, no consumer
remains. Fourth member surfaced by the family grep.
buf.len on a [MAX]u8 returned 0 — the cgdot len arms (local/global/
def) and letemitsize only read an N_INTLIT dimension. Route a
non-literal dimension through tichase().alen (the #21 fix mirrored
into the field-read consumers; .ptr arms are dim-independent).
Review-era task #56.
alloc(alias{...}) keyed the size and field-fill off the syntactic
alias name — it under-allocated and emitted zero field stores. Chase
the alias via structlookupchain to the resolved struct (depth-2
chains verified). The scalar else-branch keeps its pre-existing
benign cs!=ww divergence, surfaced here and deferred as task #57
(site note at the arm). Review item #26.
The ?/! success-is-str decision was name-keyed off the FIRST variant
and only handled call operands — an ident operand with junk registers
unwrapped garbage, and error-first unions picked the wrong variant.
Key on the stamped success variant (successvariant + typeisstr,
mirror cgen.c:10459-10466/10595-10602). Review item #16.
Slicing an array whose dimension is a def constant gave len 0 — the
default-hi and cgbasecap arms only read an N_INTLIT dimension. Route
the dimension through the type table (one root, four arms: default-hi
and cgbasecap, local and global each), byte-identical for the def-dim
SLICE shape. The def-dim array .len/.ptr FIELD-read keeps the
N_INTLIT-only limitation — filed as task #56 (cgdot sibling).
Review item #21.
The unspecified tail of a short array literal repeated the last value
instead of zeroing — wwstage only (cstage already zeroes; the review's
both-stages reading didn't survive ground truth). Zero-fill the tail
per the zero-value semantics ruling. Review item #13.
The str-size arm of the global data emit was size-keyed and matched a
24-sized array, emitting a second DATAW for the same symbol. Gate on
the array kind (!isarr8). Review item #12.
An array-typed tuple element cannot reach paramfieldsize: the checker
rejects composite tuple elements (check.ww:2150, the #60 gate), pinned
by test 832. The rule-7 note at the fall-through now records the proof
instead of an open task. Task #39 closes as unreachable.
A tagged for-range destructure binding took the 8-byte default
(paramfieldsize had no tagged arm), skipping the full-extent copy —
the wwstage twin of the just-closed cstage destructure family. Read
the stamped tinfo size through the type table (twin of the slice
arm; cstage reads tp->type->size). Task #53.
The per-binding copy loop moved ONE word of a 24B str/slice binding —
.len and .cap read zero/garbage in BOTH stages (byte-identical, the
deepest both-wrong-identical of the drain: the F7-era stride fix
asserted convergence without re-measuring the absolute). Copy the full
extent for an sz>8 str/slice binding; the rewritten 989_tupfieldsize
pins all three header words with sliced caps so cap!=len has teeth.
The tagged-binding arm remains open as task #53 (wwstage
paramfieldsize). Review-era task #40, recategorized #263 fused.
Both stages move in one commit: one emission contract; splitting
would leave the byte-id gates red between the halves.
Widening a runtime f64 into a tagged slot pushed a stale AX as the
payload while the value sat in X0 — both stages shared the push bug
(float literals dodged it because TK_FLOAT loads AX too); the
divergent pop sides then produced different garbage. Spill the
payload from X0 (MOVSD) with the variant tag. Review item #49.
Both stages move in one commit: one emission contract; splitting
would leave the byte-id gates red between the halves.
The arg-drain loop checked node_isfloat before popping a widened
arg's GP words, so a float arg adjacent to a widened (tagged) arg
read the wrong stack slot: the f64 took the widened payload (#30), or
the float arm ate the tag word into X0 and the payload landed in DI
as the tag (#48). One missing branch, two manifestations — mirror
cstage's widen-first pop (cgen.c:9650-9665). Review items #30+#48
(fold reviewer-verified one-mechanism against the cstage twin).
A str field reached through a chained dot (o.i.s) emitted two loads
(ptr, len) and stored a stale CX as the cap — both stages, at any
non-zero chain depth. Emit the full header at the chained-dot leaf.
Review item #29.
Both stages move in one commit: one emission contract; splitting
would leave the byte-id gates red between the halves.
Reading a slice-typed tuple element (t.0) loaded only the pointer
word; len and cap took whatever was left in BX/CX — silent garbage in
BOTH stages once anything clobbered the registers between build and
read. Load all three header words at the tuple-element arm. Review
item #28.
Both stages move in one commit: one emission contract; splitting
would leave the byte-id gates red between the halves.
Casting a global str to []u8 dropped the cap=len synth (the trailing
MOVQ BX,CX) — the synth was gated on a local-ident source shape.
Key it on the source type so local/global/field/call sources all get
the header. Review item #19.
s.f *= v silently became s.f = v (and the other non-+=/-= ops dropped
likewise) in BOTH stages across five lvalue sub-arms: via-ptr field,
direct local field, str/slice pseudo-field, and the two global-field
forms. Funnel all five through a shared combine dispatch
(cgdotfieldcombine / cg_dotfield_combine) emitting the load-OP-store
sequence at field width, hard-erroring the unhandled kinds — close-by-
construction so no arm stays on the old PLUSEQ-only path (the #133
BUS-routing lesson; #227 sites A/B are the closed siblings). The
refactor routes the corpus's existing +=/-= sites through the same
helper output-identically (byte-id held). Review item #34.
Both stages move in one commit: one emission contract; splitting the
halves would leave the byte-id gates red in between.
arr[i].field /= %= <<= >>= silently dropped the op (load-combine-store
emitted plain assignment) in BOTH stages — gate-blind, the #133 class.
Route every compound op through the combine dispatch at the indexed-
field arm and hard-error the unhandled operand kinds (float/str/slice/
tagged), per the #133 template (3986818). The runtime-correct target is
the op's own algebra (a OP= b == a = a OP b). Review item #33.
Both stages move in one commit: the fix is a single emission contract —
splitting cstage cgen.c from selfhost cgenexpr.ww would leave the
byte-id gates red between the halves.
'as' on a nullable value compared the pointer itself to a tag (the
missing arm). Mirror cgtypetest's nullable fold and the cstage twin
(cgen.c:10694). Review item #17.
Try-propagation on a nullable value fell to the tagged path and
compared the pointer to a tag — the null check came out inverted.
Mirror ww's own cgtypetest nullable fold (cgenexpr.ww:652) and the
cstage twin (cgen.c:10366/10504). Review item #15.
exprtype N_MATCH took the first arm's yield type without walking the
rest — int-vs-str arms silently produced garbage downstream. Walk all
arms: typeeq-equal accepts; a definite coarse-family mismatch
(num/str/bool via the new yieldclass classifier, unknown classes stay
lenient) rejects. Same-family non-assignable pairs remain lenient —
the documented precision residual is task #52 (needs tinfo-level
type_assignable). The wave's table-driven reject test lands here:
989_catA_f2_reject, 19 rows x 2 stages, each member pre-fix-red-proven.
The F8 multi-success gate counted a `...inner` spread as one variant,
bypassing the multi-success reject. New trycountvariants recursively
flattens spreads (the #209 recursion; cstage check.c:2160). Also
carries the rule-7 deferral cites for the adjacent task-#50/#51 holes
(exprtypeoftry lookups, desugarcallargs fn-ptr bail, checkisas) — the
attempted scopelookupprefer hardening is byte-id-blocked by the #50
curmod layer (evidence in the task).
The size()/align() intercept hid behind a shadow gate with no cstage
twin — a shadowed name silently folded to zero. Intercept
unconditionally and make an unresolvable type a loud error, mirroring
cstage check.c:93/1538; the dead shadow gate is dropped.
wwstage ran no operand-kind check on binary/unary operators: str+str
compiled to integer ADD on the 24B header (silent garbage). Gate each
operator class on the operand kind, mirroring cstage check.c:1186-1238
wording; the pre-existing ptr-arith arm aligns to intkindast so
ptr+untyped_int keeps compiling (byte-id-neutral, the over-reject the
self-compile gate caught). New intkindast/numkindast/boolkindast
predicates.
arrayelen silently folded a non-const dimension to 0 — the 0-sized
slot aliased its neighbor (review item: cs loud / ww rc=0 clobber).
Mirror cstage check.c:715. Test rows land with the wave's final commit.
A module-global float-bearing struct passed by value drained all-GP —
the SSE-cursor classify keyed on the node shape and missed the global
ident; key off the stamped struct type (per-eightbyte classify,
F7-flavored stamp fix). Review item #31; dual-stage rows red-proven.
Widening a global tagged union into a wider tagged slot copied
nothing of the box; treat the global ident as a tagged source and
copy the full box from g(SB) through the nested arm. Both-wrong pair:
cstage spills frame garbage as the box (filed task #44, residual
non-deterministic so the rows assert divergence only). The non-nested
SUBSET-widen shape remains open as task #49 (site comment at the
fall-through). Review item #51.
Widening a global struct into a tagged slot copied word0 only; copy
the full payload from g(SB). Both-wrong pair: cstage zero-fills the
payload (filed task #43); rows pin ww-runtime-correct with the
documented cstage residual. Review item #50.
Returning a global struct by name emitted nothing into the return
scratch (ww) — copy from the g(SB) base. Both-wrong pair: cstage
zeroes the retscr instead (filed task #42); rows pin ww-runtime-correct
with the documented cstage residual. Review item #41.
Reassigning a module-global tagged union stored the payload into the
tag word; emit the full tag+payload store to g(SB). Both-wrong pair:
cstage silently DROPS the store entirely (filed task #41) — rows pin
ww-runtime-correct and the documented cstage residual. Review item #32.
The probe accepted any inner ident, hijacking same-leaf locals as a
module qualifier; gate on the untyped(module) inner only (mirror the
cstage twin). Review item #20; dual-stage rows red-proven.
The global tagged ident operand read saved BP instead of the global:
'is' compared garbage as the tag; 'as' never had a payload. Route the
load through the g(SB) base — tag at +0, payload at +8, cap at +16 for
str (one mechanism, both consumers). The 'is' half aligns ww UP
(cs==ww pinned); the 'as' half is a both-wrong pair — cstage spills an
uninitialized payload register (its N_TYPEASSERT assumes cgexpr filled
AX/DX/CX; filed as task #46), so its rows assert ww-runtime-correct
with the cs divergence documented until #46 lands. Review item #18.
Slicing a module-global str with default hi emitted nothing for the
bound; load the len word from g(SB)+8 (mirror the cstage twin).
Review item #47; dual-stage rows red-proven.
A slice-typed field of a module-global struct stored only the str-form
words; widen the arm to the full slice header via the g(SB) base
(mirror cgen.c sibling arm). Review item #35; dual-stage rows red-proven.