Commit Graph

742 Commits

Author SHA1 Message Date
559b77db40 selfhost/cmd/w6a: align parsenum to strtoll(base 0) semantics (#62)
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.
2026-06-13 11:03:42 +09:00
a9dcea70ed lib/encoding/utf8: decoder offs i32->size, closing prev/next OOB (#70)
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.
2026-06-13 11:03:33 +09:00
5cab22ecec lib/ww/parse: error on unknown top-level decl in parsefile fallback (#55)
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.
2026-06-13 10:33:48 +09:00
cf0789c897 lib/ww/lex: port the u64 overflow guard into parseint (#53)
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.
2026-06-13 10:32:56 +09:00
8f370533bb ww driver: zero discovered tests is a loud failure
rundirtests returned rc=0 when it discovered no tests — a typo'd
path or empty corpus read as success. Fail loudly, matching the
cstage driver.
2026-06-13 10:20:46 +09:00
675a0368cd w6l: dynamic e_entry rebases with the actual text offset, both stages
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.
2026-06-13 10:17:51 +09:00
95da870734 w6a/ww: DATAR naming an undefined slot is a loud reject
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.
2026-06-13 04:43:57 +09:00
2ce94a194a wwdump: -c/-r gate on parse errors
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.
2026-06-13 04:40:58 +09:00
4dc8459a90 ww driver: enumeratedir grows past 256 entries
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.
2026-06-13 04:37:47 +09:00
f7845057a7 wcc: loop-label stack guards its depth loudly, both stages
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.
2026-06-13 04:34:36 +09:00
92cd573197 wcc: defer capacity 32 with a loud cap error, both stages
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.
2026-06-13 04:31:12 +09:00
1f2bc7fc26 wcc/ww: exprtypeoftry prefers the current module's symbol
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.
2026-06-13 03:04:52 +09:00
51e3b8f134 wcc/ww: &fn synthesis prefers the current module's fn
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).
2026-06-13 03:01:31 +09:00
63e837e820 wcc/ww: scopelookuptype prefers the current module's symbol
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].
2026-06-13 02:58:09 +09:00
ffe37deaee wcc/ww: def-dim array slice-arg default-hi takes the length from the type table
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.
2026-06-13 01:56:01 +09:00
b97be7301f wcc/ww: def-dim array .len field-reads resolve the dimension from the type table
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.
2026-06-13 01:52:42 +09:00
faf1a2908b wcc/ww: alloc of an alias struct literal chases the alias for size and fill
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.
2026-06-13 01:49:07 +09:00
36e17b58f2 wcc/ww: try-unwrap str success reads the stamped operand type
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.
2026-06-13 01:44:27 +09:00
75c1b278e6 wcc/ww: def-dim array slice takes len and cap from the type table
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.
2026-06-13 00:21:21 +09:00
ffb858bba6 wcc/ww: under-length array-literal tail zero-fills
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.
2026-06-13 00:17:57 +09:00
94a55c565f wcc/ww: no-init array global emits one DATAW slot, not two
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.
2026-06-13 00:14:35 +09:00
0f0d2d2c1c wcc/ww: document the unreachable N_TARRAY destructure arm
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.
2026-06-13 00:10:59 +09:00
347f6c42c8 wcc/ww: paramfieldsize sizes a tagged-union destructure binding
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.
2026-06-13 00:07:36 +09:00
2a2ac49c64 wcc: for-range destructure copies the full str/slice binding, both stages
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.
2026-06-12 22:52:20 +09:00
ef7c0c1675 wcc: widen-push spills the float payload from X0, both stages
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.
2026-06-12 21:14:57 +09:00
a4a4cd7c16 wcc/ww: cgcall drain pops widened GP words before the float arm
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).
2026-06-12 21:11:35 +09:00
f00775759d wcc: chained-dot str leaf loads the cap word, both stages
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.
2026-06-12 21:07:55 +09:00
0a6f500b8c wcc: tuple slice-element read loads the full 24B header, both stages
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.
2026-06-12 21:04:35 +09:00
850746cfa8 wcc/ww: str-to-slice cap synth keys on the source type
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.
2026-06-12 21:00:59 +09:00
c405e777d3 wcc: single-dot field compound assignment routes through one combine helper
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.
2026-06-12 19:29:58 +09:00
1be0e6b5db wcc: indexed-field compound assignment wires all ten ops, both stages
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.
2026-06-12 19:26:24 +09:00
02eb867036 wcc/ww: cgtypeassert gains the nullable arm
'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.
2026-06-12 19:22:51 +09:00
6f3c896fea wcc/ww: cgtryprop/cgtryunw gain the nullable arm
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.
2026-06-12 19:19:26 +09:00
87367ae332 wcc/ww: match-as-expression unifies arm yields (coarse-family gate)
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.
2026-06-12 11:31:53 +09:00
780e680c1b wcc/ww: checktryprop flattens spread variants in the success count
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).
2026-06-12 11:28:31 +09:00
6f2ed0cc57 wcc/ww: reject non-tuple multi-assign rhs (resolvewalk N_MASSIGN)
ww accepted a non-tuple rhs in a destructuring assignment that cstage
rejects (check.c:2562); the destructure then read garbage words.
2026-06-12 11:24:58 +09:00
6c380a53ea wcc/ww: reject tuple-literal arity mismatch (checktuplearrfits)
A tuple literal with the wrong arity was silently accepted and the
extra/missing elements mis-stored. Mirror cstage type.c:416 +
check.c:2386.
2026-06-12 11:21:39 +09:00
bd86fa5f46 wcc/ww: size/align intercept unconditionally; unknown type is loud
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.
2026-06-12 11:18:19 +09:00
353b50489f wcc/ww: binoptype/unoptype operand-kind gates (arith/bitwise/ordered/logical)
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.
2026-06-12 11:14:48 +09:00
6f11763462 wcc/ww: reject non-constant array dimension (tinfofornode N_TARRAY)
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.
2026-06-12 11:11:33 +09:00
9f4626c546 wcc/ww: global float-struct by-value arg keys the SSE drain off the type
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.
2026-06-12 09:24:39 +09:00
e416885d96 wcc/ww: widen of a module-global tagged ident copies the whole box
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.
2026-06-12 09:21:16 +09:00
32848194d7 wcc/ww: widen of a module-global struct ident copies the full payload
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.
2026-06-12 09:16:52 +09:00
4b8c8fd9ed wcc/ww: return of a module-global struct ident copies the global's bytes
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.
2026-06-12 09:13:26 +09:00
b3a355744f wcc/ww: tagged GLOBAL reassign stores tag and payload
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.
2026-06-12 09:10:16 +09:00
94bfca761c wcc/ww: dotbaseaddr #128b probe gates on an untyped module-qualifier inner
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.
2026-06-12 09:06:51 +09:00
4961a91d14 wcc/ww: is/as on a module-global tagged ident loads from g(SB)
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.
2026-06-12 09:03:40 +09:00
1eceb46ac3 wcc/ww: global-str default-hi slice arg loads its len word
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.
2026-06-12 09:00:15 +09:00
fee84528da wcc/ww: global-struct slice-field store widens the str arm to slices
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.
2026-06-12 08:57:04 +09:00
e0df2adf47 wcc/ww: tagged-union normalization at tinfofornode (never-drop, dedup, collapse, nullable fold)
wwstage computed tagged sizes/tags off the raw variant list — size()
folded wrong constants (size((*u8|void)) 16 vs 8, (i32|never) 16 vs 4)
and duplicate variants got divergent tag numbering vs cstage, while
ww's own cgen layout folded nullable but its size() didn't. Make
tinfofornode's N_TTAGGED arm the normalization SSoT mirroring cstage
resolve_type (check.c:801-882): never-drop, duplicate dedup via
structural typeeq, single-variant collapse, nullable fold on the
normalized pair; astsize/astalign delegate, and voidvariantindex reads
the normalized ti.params (cgen.c:900-911) so construct/match/void tag
readers agree. Corpus-neutral (zero-move on all combineds);
989_tagnorm_run pins the folds dual-stage, red-proven. Review items
#1/#3; residual #45 filed (AST-keyed nullable gate at global emit).
2026-06-12 07:03:45 +09:00