Commit Graph

977 Commits

Author SHA1 Message Date
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
427b67f656 lib/dirs: abort loudly on over-long path, not silent truncation (#69)
dirs build() capped the composed path at the 256B pathbuf with a silent
break, so a HOME (or XDG_*) near/over ~240 bytes produced a truncated
path that lookup() then mkdir'd and returned rc=0 — a silently-wrong,
freshly-created directory. ref/hare/dirs/xdg.ha routes through
path::set/push whose too_long error the `!` aborts loudly. Precompute
the composition length in build() and rt_abort when it won't fit; drop
the now-dead silent caps. (Shape (a); routing dirs through lib/path is
the filed fidelity follow-up.)

975_dirs_toolong_run pins the abort + no-stray-dir on both driver twins.
2026-06-13 10:38:51 +09:00
bccd111a16 lib/fmt: encode rune args as UTF-8, not a truncated low byte (#66)
writeone and formatraw's rune arm did `buf[0] = r: u8; putbytes(...,1)`,
emitting only the low byte — invalid UTF-8 for any rune > 0x7F, with a
success return. rawlen's rune arm hardcoded 1, desyncing width-padding
for multibyte runes. Route both write arms through utf8.encoderune and
rawlen through utf8.runesz (ref/hare/fmt/print.ha:84). Uses the current
in-tree encoderune(out: []u8, r) caller-buffer signature; report-#168's
signature realignment is a separate item, not folded here.

fmttest gains fprintrune (1/2/3/4-byte runes) + fprintf_rune_width
(multibyte pad).
2026-06-13 10:36:11 +09:00
69e31355ce lib/fmt: render i64::MIN via math.absi64, not in-i64 negate (#67)
i64dec computed the magnitude with `n = -n` inside i64, which wraps at
i64::MIN (-MIN == MIN stays negative), so the `for (n > 0)` loop never
ran and println(i64::MIN) emitted only the bare '-'. The whole print/
println/fprint family was affected at the single boundary value (and
int MIN, since int is 8B). ref/hare/fmt/print.ha:124-129 takes the
magnitude through math::absi64 into a u64; do the same (math.absi64
exists, lib/math/math.ww:12). ww's own printf path was already correct
((-v): u64), so print and printf now agree.

fmttest gains fprinti64min + fprintintmin pinning the boundary.
2026-06-13 10:33:57 +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
77747061c6 w6c: global *struct base field store loads the pointer from g(SB)
cstage dereferenced 0(BP) for the base — a SEGV on every store
through a module-global struct pointer; wwstage was already correct
(the inverse-set member). Mirror ww's global-pointer load. New
989_globptrfield_run pins cs==ww both stages. Task #47.
2026-06-12 22:48:37 +09:00
e09a8c1775 w6c: 'as' on a module-global tagged ident loads the full box
cstage's N_TYPEASSERT assumed cgexpr filled the registers and spilled
an uninitialized payload (cs=0 for any stored value); mirror the
landed wwstage emission (tag/payload/cap from g(SB)). Flips the
residual row to cs==ww==correct. Task #46.
2026-06-12 22:45:10 +09:00
fc47c3d0f2 w6c: widen of a module-global tagged ident copies the whole box
cstage spilled frame garbage as the box; mirror the landed wwstage
emission (copy from gi(SB)). The F8-era divergence-only rows gain
pinned cs==ww values. Task #44.
2026-06-12 22:41:45 +09:00
ca4ff4b882 w6c: widen of a module-global struct ident copies the full payload
cstage zero-filled the payload; mirror the landed wwstage emission.
Flips the residual row to cs==ww==correct. Task #43.
2026-06-12 22:37:56 +09:00
960b1e796d w6c: return of a module-global struct ident copies the global's bytes
cstage zeroed the return scratch; mirror the landed wwstage emission
(copy from the g(SB) base). Flips the F8-era residual row to
cs==ww==correct. Task #42.
2026-06-12 22:34:31 +09:00
5ff5f4b4fe w6c: tagged GLOBAL reassign emits the store
cstage silently dropped the store on reassigning a module-global
tagged union; mirror the landed wwstage emission (tag+payload to
g(SB) via the widener). Closes the cs half of the F8 #263 pair; the
repro row flips to cs==ww==correct. Task #41.
2026-06-12 22:31:09 +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