A slice/str ELEMENT of an indexed expression passed as a call-arg
pushed one word instead of the 24B/16B header — the predicates had no
N_INDEX arm, so element-typed args fell to the scalar path (review
findings #45/#46). Read the element-type stamp; dual-stage rows in
989_idxarg_run pin cs==ww (red 2/8 on pre-fix binaries).
wwstage's desugarcallargs ran no general per-arg typecheck (only the
narrow #258 array-to-slice arm): any mistyped scalar call-arg silently
miscompiled (int read as a 24B slice header; the -T face was a user
const __wwtests building a garbage test binary). Route every call-arg
through the predicate union isassignable()||assignableaddrfn(),
mirroring cstage type_assignable||assignable_addrfn and the check.c:1869
diagnostic. Confident scalar/aggregate and aggregate/aggregate
kind-mismatch rejects live in shared isassignable; the concrete-to-
tagged arm is shape-matched-lenient via tagshape() (AST mirror of cgen
taggedvariantindext) so genuine variant members keep flowing while
shape-mismatched aggregates reject. Reserve __wwtests under -T in both
stages (mirror the main reservation, check.c:2996). New table-driven
989_callarg_typecheck, 31 fixtures, reject rows proven red on pre-fix
binaries.
Deferred, filed, site-commented: the assign seam rides #178->#36
(typeeqast cannot compare variadic/module-qualified fn sigs); the
same-coarse-shape same-leaf nominal collision over-accept rides #37
(#10/#66 — the distinguishing module is absent from the AST surface
isassignable operates on).
`ww test <file> <pattern>` runs only the @test fns whose names match the
fnmatch glob; no pattern runs all (byte-for-byte the pre-filter path);
zero matches prints "No tests run" and exits 0 (Hare ground truth
ref/hare/test/+test.ha:114-117). A pattern in directory mode is rejected
"ww test: pattern needs a single test file" (rc 2), identical wording in
both twins (cmd/ww/main.c do_test + selfhost/cmd/ww/main.ww dotest).
Mechanism (a): rt/start.s stashes argc/argv into rt_argc/rt_argv getters
(rt_envp twin shape, -T synth untouched so 990-997 byte-id holds);
lib/os.args() rebuilds the []str view, build-once-cached; lib/test/run.ww
imports fnmatch and filters av[1..] (argv[0] is the binary path). The
driver forwards the 2nd positional as argv[1] via fork/execv (cstage) /
procrun (wwstage) so glob metachars aren't shell-expanded.
os.args() is the first `alloc`-caller in the base os module, so os.ww now
imports rt — the `alloc` builtin's malloc lowers to rt_malloc only when
the rt binding is bundled (mirror lib/strings/strings.ww:30); without it a
plain `ww build` of any os-importing program links bare libc `malloc`
(undefined). os is bundled by ~every program, so this is load-bearing.
The lib/test floor rises os-only -> os+fnmatch+ascii+strings in every -T
build; the bundled `ascii` module vs a `@test fn ascii` collision that
exposed is closed by the preceding #30 promote commit. 989_test_filter
pins the full matrix on both twins byte-identically; 949 gains the
dir-mode reject row. (#17)
lib/test/run.ww: fork+wait4 runner; each @test runs in its own child,
abort/SEGV/FPE decoded from wait-status, failures recorded and the run
continues; exit = fail count. Tests are hermetic: module globals do not
persist test-to-test (fresh fork image; sanctioned divergence from
harec's shared-process __test_main, no setjmp/signal layer needed).
-T synth (both stages) emits a module-global (str,*fn() void) table +
return run(table) instead of straight-line calls. Driver twins bundle
lib/test under test mode and gain ww test -c/-o (go test -c) so the
byte-id gates diff the same artifact the real path builds. Gates
989/910/997 rewired onto it; new 911 pins record-and-continue across
all three fault classes; 949 +3 rows. (#17-team commit-2)
Lone -I/-L/-l/-o now error "ww <cmd>: -X needs an argument" (rc2),
matching selfhost main.ww. do_test rewritten to an -I-only loop that
loud-rejects -l/-L/-o/unknown instead of silently swallowing them.
949_driver_flagargs: table-driven, 10 rows, each run against both
twins asserting identical rc+stderr. (#15-team)
A bare 'let x: T;' with 1 <= size(T) <= 7 matched no zero-fill arm in
either stage (8B and >8B were already zeroed) - 'let c: [3]u8;' read
stack garbage. User-ruled zero-value semantics: cstage gate sz>8 ->
sz>0; wwstage zsz==8 arm hoisted above the fill-run arm (required -
8B would otherwise route into the run and diverge) and run gate
zsz>0. New 840 pin: dirty-frame probe rows, dual-dim (run + cs/ww
byte-id); discriminators fail exit-154 on pre-fix binaries.
Fused with the lib/bytes test conversion (rule 11): either half alone
turns 967 red. The old exit(signalled+10) wrapped a real 1782-count
ltrim failure to exit 0 - green depended on the garbage. Converted to
assert form (completes the 35/35 @test conversion); ltrim rows keep
the bare 'let c: [3]u8;' as the consumer proof of the fix.
The 990-997/950/989_lib_byteid gates are pure functions of (stage
binaries, tracked sources, fixtures, harness, Makefile) and eat 87% of
full-test wall (measured, .ai/testperf-baseline.md). test-commit hashes
exactly those inputs; a match with the last green full run reports the
gates as 'cached' (counted separately, never silently) and runs
everything else - 25s vs 212s on a hit, full run on any miss. A red
run never records the key; make test itself is never cached and stays
the pre-push bar (rule 14 updated). Key gap caught in review:
examples/ feeds 996_dyn_ww - included.
Both driver twins: an import that neither locates as a file nor is
satisfied by an inline 'package <name>' declaration in the unit is now
fatal "ww: cannot find package <name>" (was a silent skip that masked
dead imports and typos). The inline scan is a new every-line helper on
the uncapped comment-skip core - peekpackage stops at the first decl,
and single-file multi-package fixtures declare several. 949 pins both
branches (miss->fatal, inline->build+run); 993 adds ww_ww parity.
check-(c): a package importing itself (any spelling) is a hard error,
mirroring Go. Predicate is leaf==owner at the N_USE/installdecl seam —
sound only after the PREP commits (dotted-test renames, package-less
boundary directive). Identical wording both stages; diagnostics-only,
byte-id-neutral. 948 pins the reject in both compilers; 708's
pos_selfimp (which pinned the abolished self-import skip) converts to
neg_selfimp + new pos_crossmod preserving the param-shadow tolerance
the case existed for. Checks (a) unused and (b)/(d) name-membership
stay deferred to the multi-package arc: imports are filename-keyed
pulls, so those need import->file provenance this compiler lacks.
@test was parsed then dropped (no consumer); `ww test` needed a hand-written
main listing each test by hand, so adding a @test and forgetting the call
silently skipped it. -T makes the checker collect @test N_FNDECLs in source
order, loud-reject a user main, and append a synthetic
`export fn main() i32 { t0(); ...; return 0; }` at the install->body-check seam;
the existing cgfn emits it (cgen untouched) -> byte-identical by construction.
Mirrors harec's checker-side is_test placement.
Plan-9-lean reduction (user-sanctioned, reinstatable post-CSP): sequential,
abort/nonzero=fail; no setjmp isolation, no fnmatch filter, no file:line.
910/997 rewired from a regex scanner to driving `w6c -T` directly (thin trusted
drivers; the @test content stays ww), with a cross-stage byte-id assert on the
-T output. attest_userman/attest_badsig pin the user-main and bad-signature
rejects.
wwstage's tagged→tagged subset-assign arm accepted all (lenient escape), silently miscompiling implicit wide→narrow: a (int|bool|str) holding a str, assigned to (int|bool), ran the int arm and read the str pointer as int. cstage rejects loud; this escape was the lone divergence.
Replace the escape with cstage's subset walk (src ⊆ dst): a src variant is covered iff typeeqast matches (structural — []u8/nested/primitives) OR both are N_TNAME with equal leaf names. The leaf bridge covers cross-module forwards where a callee's bare inline-union variant (utf8's `done`) meets the consumer's qualified `utf8.done` — raw typeeqast can't, and the escape was masking it for every forward. Mirrors casecovers (check.ww:4136). cstage untouched (align-up); spread-bearing unions keep the escape (#199b orthogonal).
Honest floor: leaf-only defers true module identity to #10 — a callee's defining module for an inline-union return is unrecoverable at check-time (the call node is gone; #211 fnretlookupmod is cgen-only). Retained divergence = cross-module same-leaf-collision over-accept (absent from bootstrap), documented at the site and filed as #10-A7 / census cat-A.
Test 989_tagged_subset_reject is table-driven with composition-discriminating rows: []u8 subset (typeeqast-only), bare↔qualified xmod forward (leaf-only), xmod named genuine-absence (reject). 352 green; w6c unchanged, w6c_ww 4b4496b8→4b316f01.
typeeqast's N_TNAME arm compared variant types by SURFACE string
(streq(aa.str, bb.str)), so a cross-module type referenced bare
(oserror) vs qualified (os.oserror) mis-compared unequal -> A6:
concrete->tagged silently mis-identified the variant. Resolve each
name to its canonical type sym before comparing: keep the streq
fast-path, else aliassym(c,aa)==aliassym(c,bb) (ww's existing resolver
maps bare [#53] and qualified [#51] to the same SK_TYPE sym). AST
analog of cstage type.c:278 TY_NAMED a==b / harec types.c:579
ident_equal -- NO interning. `c` threaded into 21 typeeqast sites.
B-full Layer 1: closes A6/#14; Layer 2 (A7 tagged->tagged subset
reject) stays deferred to the #199b flatten arc. Byte-id-neutral;
graduates shlex #59.15 (989 M_WWREJECT->M_ID). test/wcc/839 pins
both-stage symmetry (discriminating teeth = 989 #59.15, per 839 doc).
A module-global tagged init via cast (`let g: u = true: u;`) silently
emitted tag=0; cstage louds. Route emittaggeddata through flatvariantidx
(the cstage cg_tag_for_variant twin) so an unsupported variant louds while
valid str/slice cast-inits still accept+run. test/wcc/838.
A same-type tagged cast return (`return x: u`, u == fnret) emitted tag=0
(a 2nd-variant value returned the wrong payload). Route it through the
widener (needswiden when N_CAST && ru1==fu1, beside S1's ru1!=fu1) which
peels the identity cast internally -> correct; same-type non-cast stays on
the passthrough. Byte-id-neutral. test/wcc/837.
isassignable's untyped-float (A4) and nil (A5) arms over-accepted a source
into a tagged dst where no variant accepts it -> silent over-accept then
tag-0 miscompile; cstage louds. Reject (cerr + c.errs+=1). Part of the
isassignable tagged-dst over-accept class (#23 fixed arm3; A6/A7 deferred
to the B-full nominal arc). test/wcc/836.
isassignable's untyped-int arm fell through *confident=false/return true,
silently accepting an untyped int into a union whose variant is itself a
nested (non-flattened) union; ww emitted tag=0 (wrong arm) where cstage
louds. Reject unless a direct variant is numeric-or-enum (N_TENUM accept
mirrors cstage type_isnum). Faithful flatten+rebox deferred (nominal
identity, post-CSP). test/wcc/835 (new) + Makefile.
An inferred-type array global -- let xs = [1,2,3]; -- hard-failed wwstage
with 'asserttyped: int' exit 1, while cstage compiled+ran it. The
array-twin of the inferred-global family (#135 inferred-float, #150-B
inferred-Sym-repoint).
exprtype's N_ARRLIT arm synthesizes the array type for an unannotated
literal but left two synthesized child nodes unstamped: the count literal
(asserttyped trips on it -> the loud failure) and the element TNAME (cgen
then drops a non-scalar element's header load -> the silent miscompile
that merely accepting on alone would introduce: an inferred str-array's
xs[1].len read 24 not 3). Both are now stamped at the synthesis site:
cn.type_ (asserttyped facet) and elt.type_ (cgen facet). Inferred int /
u8 / str / struct / multi-dim array globals + locals + args now compile
byte-identically to the explicit-typed form (== cstage).
cstage unchanged (w6c md5 unchanged); selfhost has no inferred array
globals so byte-id 990-997 8/8, no lib pin flips. test/wcc/833.
An overlong array literal as a tuple element -- let t: ([2]int, i32) =
([1,2,3], 5) -- was silently accepted by wwstage; cstage loud-rejects it.
The #12+#106 over-fill coverage wired checkarrlitfits for direct-array,
slice and alias lhs positions but not the tuple-element position.
wwstage-only checker, reject-align: checkletassign gains an N_TTUPLE arm
that walks the lhs element types (llhs.list) lockstep with the rhs values
(n.rhs.list), calling the existing alias-aware checkarrlitfits per array
element (no-ops scalars, recurses nested arrays). cstage unchanged (w6c
md5 unchanged); reject-only, 990-997 8/8, no lib pin flips. test/wcc/832.
Two sibling tuple-element positions stay open (filed, not folded -- they
are reject-aligns on invalid programs, no selfhost byte-id impact): #25
tuple-RETURN overlong, #26 nested tuple-in-tuple.
Indexing a non-ident pointer-yielding base -- a direct cast
((&a):*[4]u32)[i], a call result mk(&a)[i], a slice, a type-assertion --
used wwstage's default 8-byte element stride/load instead of the real
element type's, reading garbage (cast-base i32 index: cs=30, ww=0). The
cgindex esz derivation gated on a whitelist of base node-kinds (DOT /
UN-deref / INDEX); an N_CAST/N_CALL/N_SLICE/N_TYPEASSERT base matched none.
Rather than extend the whitelist (whack-a-mole), this mirrors cstage's
uniform idx_eff read: N_INDEX keeps its own arm (chained-index byte-id
preserved), and every other non-ident base now derives esz/stride/load-
width/signedness from the stamped n.type_ -- closing the class by
construction (base set ident/dot/un/index/cast/call/slice/typeassert).
cstage was already correct (uniform); w6c md5 unchanged. byte-id 990-997
8/8, no lib pin flips. test/wcc/830 (9 base shapes, byte-id per width,
signed + unsigned). Folds the N_CALL sibling #22.
A let-global slice or str passed by value as an argument was silently
field-dropped by wwstage: pushargsrev's global branch had only the #150-A
struct arm, and wwstage's nodeisslice/nodeisstr are local-keyed (false for
a global), so a global slice/str ident fell to the scalar single-PUSHQ,
pushing one of the three header words {ptr,len,cap} -> the callee read
garbage for .len/.cap. cstage was already correct.
wwstage-only, caller-side only (slice/str params already received
correctly). A global slice/str arm in pushargsrev, type-keyed on
tichase(arg.type_).kind, with two arms byte-matching cstage's two distinct
sequences -- str via cgslicehdr (CX-base, cgen.c:1866), slice per-word
(BX-base, cgen.c:9124). Gate is isletvar-only (a def has no name(SB)
holder; cstage const-folds it -- def-str/slice-by-value is the residual
task #21). cstage unchanged (w6c md5 unchanged); byte-id 990-997 8/8,
no lib pin flips. test/wcc/829 table-driven, byte-id per type.
An overlong array literal (more initializers than the declared length) is
invalid -- cstage loud-rejects it everywhere -- but wwstage silently
accepted (and truncated) it in several positions; #9 wired only the decl
position. This folds the remaining three (one class: checkarrlitfits
over-fill coverage), all wwstage-only reject-align:
- #12a return fn f() [2]int = [1,2,3] -- silently accepted.
- #12b call-arg g([1,2,3]) -- louded only late via cgen #271.
- #106 alias type A=[2]int; let g: A = [1,2,3] -- silently truncated;
checkarrlitfits bailed on the N_TNAME alias without chasing.
Four inserts in check.ww: an alias-chase (resolvealias) at the top of
checkarrlitfits (makes all callers alias-aware), the over-fill check wired
into checkretassign (hoisted above the isassignable short-circuit) and
desugarcallargs, and the alias-let-global guard made alias-aware. cstage
unchanged (w6c md5 unchanged); reject-only, so no asm moves -- 990-997 8/8,
no lib byte-id pin flips. A 5th position (tuple-element overlong) is a
separate pre-existing hole, filed (#20). test/wcc/828 table-driven.
wwstage compiled str ==/!= as a single CMPQ on the eager-eval'd ptr word
(len ignored), so two distinct-pointer equal-content strings compared
unequal. cstage was already correct (CALLs rt_streq, the #154 cbinop fix).
The wwstage cgbin had no str-awareness -- every comparison fell to the
generic CMPQ tail; the #154 fix was never mirrored.
wwstage-only: a cgstreqpush helper + a str ==/!= branch at the top of
cgbin (before the generic eval collapses the header), byte-matching cstage
cbinop:4564-4623 -- push rhs/lhs (len,ptr), POPQ DI/SI/DX/CX, CALL
rt_streq, XORQ $1 for !=. Gated on typeisstr (= cstage node_isstr, which
also catches module-global str idents). cstage cgen unchanged (w6c md5
unchanged). The str== .s is byte-identical cs==ww for local, global,
aliased, chained, and condition operands.
Graduates 3 lib byte-id pins (test/wcc/989_lib_byteid #59.1 asciitest,
#59.11 toktest, #59.12 asttest) M_DIVERGE->M_ID -- they used == on str and
were pinned divergent because of this bug; now byte-identical. byte-id
990-997 8/8. test/wcc/827 table-driven.
type pair = (int, int); let x: pair = (3, 4) -- an alias of a tuple
initialized from an untyped literal, and passing such a value to a fn --
was a both-stage bug, mirror-twins of the same TY_NAMED-not-chased root:
cstage CHECKER over-rejected the init (not assignable to declared pair):
type.c's tuple-assignable arm gated on the un-chased dst kind, so a
TY_NAMED alias skipped the per-element untyped->int coercion the direct
tuple path applies. Fix: chase TY_NAMED both sides (mirrors the #258
slice-borrow arm). Direct and typed-alias tuples already worked; only
alias+untyped was rejected.
wwstage CGEN dropped the second word of an alias-tuple fn-arg: the
tuple-param spill at cgendecl.ww gated on the syntactic N_TTUPLE, so an
alias param (N_TNAME) fell to the scalar path and spilled one slot ->
t.1 read frame garbage. Fix: chase the alias via aliaslookup to the
resolved N_TTUPLE and spill all its slots. cstage cgen was already
correct -- the bug was checker-only there. Converges cs==ww byte-id.
One commit: same construct, the two halves must ship together (either
alone leaves cs!=ww). test/wcc/826 (init/fn-arg/return, 2-field byte-id);
test/wcc/944 4 rows graduated err->run-correct. byte-id 990-997 8/8.
An error-first tagged union -- error variant at tag 0, success at tag 1+,
e.g. (myerr | u16) -- was silently miscompiled by wwstage: the try/propagate
codegen hardcoded success = tag 0, so the actual success value (tag 1)
failed the CMPQ $0 and fell to the error path -> exit(1) instead of the
value (44). cstage was correct (computes the success tag via
cg_tagged_success_tag = first non-error variant).
wwstage-only: a successtag/successvariant helper (mirroring cstage) replaces
the hardcoded tag-0 / first-param assumption at all four try sites --
cgtryprop (?), cgtryunw (!), and the two latent shift sites cgtrytupleshift
+ cgtrytaggedshift (which bite an error-first union with an aggregate
success payload). Success-first unions (the Hare idiom + what the selfhost
uses) keep successtag=0 -> CMPQ $0 unchanged -> byte-id-neutral on 990-997.
cstage untouched (w6c md5 unchanged).
byte-id 990-997 8/8. test/wcc/825 table-driven (errfirst must/prop +
tuple-success + success-first control). A separate nested-tagged-union
construction divergence is filed (#10/#125).
let pi = 3.5; pi * 2.0 (an inferred-type float module-global) was silently
miscompiled by wwstage: untyped_float wasn't defaulted, so letemitsize
sized it 0 -> no DATAW emitted -> the pi load was dropped, X0 kept a stale
spill -> 2.0*2.0 = 4 not 7. cstage became correct via #150-B's sym-repoint
(stamps f64 -> MOVSD), so this aligns wwstage UP, byte-identical.
wwstage-only: cgen.ww defaultinferredlets gains the untyped_float->f64 arm
(mirrors the untyped_int->int arm; the codebase's own #135-deferred
carve-out at cgen.ww:1079-1082, unblocked now that #150-B killed the
rule-10 divergence it feared), and cgenexpr.ww cgident gets a letfloatprim
fallback (the same primitive-TNAME SSoT letemitsize already uses, since a
renamed primitive TNAME carries no tinfo stamp). cstage cgen unchanged
(w6c md5 unchanged). int-inferred globals stay integer.
byte-id 990-997 8/8. test/wcc/824 table-driven. The N_CAST-no-recurse
parity (check.c:1276) is filed separately (#19).
Every inferred-type module-global -- let n = 5; ... return n, or
let g = pt{...}; g.a -- yielded <nil> downstream in cstage: the module
N_LET pass-2 stamped d->type from the initializer but never repointed the
Sym, so later references resolved the still-unstamped Sym. The wwstage
checker already repointed correctly, so this aligns cstage UP (cstage-only;
no combined.ww / check.ww change, w6c_ww unchanged).
Mirrors the #11 [_]-array repoint. The fix is shape-agnostic (keyed on the
unstamped Sym, not the use site) -- verified across scalar/field/arg/index/
str/match/nested inferred-global shapes. Commit B of #150 (Commit A 5c37648
fixed the by-value struct-arg cgen). A pure inferred ARRAY global is now
correct in cstage but trips wwstage asserttyped -- opposite-stage, filed
#125-class. byte-id 990-997 8/8. test/wcc/823 table-driven.
Passing a module-global struct by value -- let g: pt = pt{...}; take(g)
-- was silently miscompiled, mirror-opposite on the two stages. cstage's
by-value struct-arg arm hit localfind(g)->0 and read 2 words from the
frame (MOVQ (BP)), never main.g(SB) -> returned garbage. wwstage used the
correct main.g(SB) base but fell through to the scalar single-PUSHQ
default, pushing one word for a 2-word struct -> dropped a field.
Both stages now take the off==0 global branch: LEAQ main.NAME(SB) and copy
all struct-size/8 eightbytes (reusing the GAP-A.ptr/#231 global-base
predicate), converging to one byte-identical sequence. The local path
(off!=0) is unchanged; >16B aggregates (#271) already resolved globals.
Commit A of the cluster; the cstage-only inferred-global-type Sym-repoint
(every let g = ... module-global yields <nil> downstream) is Commit B
(#18). Slice/str global-by-value args have the same wwstage field-drop --
filed (#10 G-valglobal-arg; struct closed here). byte-id 990-997 8/8.
test/wcc/822 table-driven, byte-id per stage.
def S:str = "hi"; S[0] silently segfaulted: a def is a compile-time
constant, never materialized as DATA (unlike let), so indexing it emitted
an unbacked main.S(SB) reference -> cstage ran into frame garbage,
wwstage link-failed. str[i] itself is valid ww (a deliberate Go-like
str[i]->u8 byte-index that lib/strings compare/dup depend on), so the fix
is narrow: the N_INDEX TY_STR arm now rejects an index whose operand is a
bare SK_DEF scalar-str symbol, both stages -- 'cannot index a def-constant
str; bind it to a let'. INDEX-ONLY: len(S) and &S are already loud, and a
def's .len/.ptr field reads (the load-bearing w6l INTERP) are N_DOT, a
different arm, and stay valid.
A rule-9 WHY-comment records str[i]->u8 as a sanctioned divergence from
Hare's strings.toutf8. The full make-it-work fold (len(S)->2, S[0]->byte)
is deferred (#16). byte-id 990-997 8/8. test/wcc/821 table-driven.
An explicit [N]T = [init] with more initializers than N silently
mis-compiled for N==0: the over-fill length-mismatch check was suppressed
when alen==0, because alen==0 doubles as the [_] infer-sentinel after
resolve_type collapses the two. So def/let [0]int=[1,2] silently resized
(cstage exit 2) or OOB-read/segfaulted (wwstage) instead of the loud
length-mismatch that [N]=[init>N] gets everywhere else.
The AST keeps the distinction the Type loses: [_] leaves the N_TARRAY
length-child NULL, an explicit [N] carries N_INTLIT. cstage adds an
is_infer_arr() helper, drops the alen>0 exemption at the over-fill check,
and gates the 4 infer-resize/no-init sites on is_infer_arr so an explicit
[0] flows to the over-fill -> loud. wwstage flips the one shared count
gate (checkarrlitfits) from declen>0 to arrtn.rhs!=nil, which also
dissolves a wwstage local-resize/module-OOB inconsistency.
[_] inference, [0]=[] empty, and [_]-no-init louding all preserved.
Under-long (count<N) stays out of scope (#10). byte-id 990-997 8/8.
test/wcc/820 table-driven; its one empty-[0] global row carves out
byte-id (pre-existing spurious-DATAW divergence, task #15).
def C:[N]str; C[i] was loud (undefined main.C) both stages. Three folded
fixes, one commit (splitting would ship a bisect point where wwstage
silently returns an element address instead of .len):
P0: the str-array static-init emitter dropped its vestigial directive
=="DATAW" gate so a def table rides the same DATAW-header + DATAR-reloc
path as let. A def str/slice table lives in DATAW by w6a's A_DATAR-holder
constraint -- placement only; def immutability stays checker-enforced.
P1: let_pre_intern / letpreintern walked N_LET only, so a def str-array's
element string-literals were never interned (dangling _S_n). Extracted a
pre_intern_strarray SSoT helper, called for a def str-array arm too, both
stages. Scoped to str fixed arrays; def []T / def [N][]T stay loud (#270).
P2: wwstage cgenexpr lacked a defvartnode fallback in the indexed-element
classify, so a def str-array element load returned the element address
instead of the slice header -- a silent miscompile. One line, aligning
wwstage up to cstage (which was correct). C[1].len now = 3 both stages,
byte-identical.
byte-id 990-997 8/8; w6c/w6c_ww move. test/wcc/819 table-driven. The
def-global scalar str index sibling (def S:str; S[0]) stays task #14.
A global fixed array's .ptr (= &A[0]) must take the SB base, but cstage
emitted frame-relative LEAQ off(BP) for BOTH let- and def-global arrays
-> *A.ptr read frame garbage (0 instead of the element). cstage-SILENT;
wwstage def-global was a loud link-error. The .ptr read arm now gates
off==0 && (let_islet || def_isarraydef) -> LEAQ name(SB), reusing the
def-array index base predicate (cgen.c:4367, the #94/#231/#48 class).
Locals (off != 0) stay BP-relative -- the 14 toolchain backing-ptr sites
unaffected.
wwstage let-global was already correct; this adds the missing def-global
arm (cgenexpr.ww), converging cstage/wwstage byte-identical across all
three flavors (local / let-global / def-global) and closing a latent
cstage-only let-global cs!=ww divergence.
Byte-id 990-997 8/8 (corpus has no global .ptr); w6c/w6c_ww binaries move
(cgen changed). test/wcc/818 table-driven, build+run+byte-id per flavor.
.cap on a fixed-size array is invalid (Hare has no capacity-read; arrays
can't grow) -> both stages now loud-reject at the checker. wwstage was
silently returning frame garbage for a local array's .cap; cstage typed
it then vaguely rejected at use. Unified to one early checker reject with
an identical diagnostic both stages.
.ptr on a fixed-size array is ratified VALID: array.ptr is &A[0], a
sanctioned ww spelling divergence from Hare; see task #13. The toolchain
already relies on it in 14 backing-pointer sites. WHY-doc added at both
checker .ptr-on-array sites. The def-global .ptr cgen base-selection bug
(#11) is a separate following commit.
Valid-program asm unchanged (byte-id 990-997 8/8); w6c/w6c_ww binaries
move (checker code changed). test/wcc/817 table-driven, model 684.
wwstage .len on a def-global array fell to the cgdot SB-fallback (w6l: undefined reference to 'len') — the #7 let-array arm gates on letvartnode (c.lets only), so def-globals (c.defs) missed it. Add a def .len-only arm in cgdot using the existing defvartnode (the def-side mirror of letvartnode), emitting the length immediate from the #11-stamped N_TARRAY length child. cstage cgen.c was already correct, so this is a wwstage-only source change: w6c unchanged, w6c_ww + wwdump regen'd (they embed the wcc cgen).
.ptr (cstage itself buggy — emits LEAQ (BP), filed GAP-A.ptr) and .cap (wwstage silent garbage; arrays have no cap, filed GAP-A.cap) are NOT folded (rule-11, separate concerns). Pin: table-driven test/wcc/816_def_arr_len (def [3] + [_] inferred + 1-elem + u8 stride .len, both stages + byte-id), teeth-proven.
ww had narrowed Hare's formattable (types::numeric) to a lone i64 arm, so a bare int/uint was not printable: cstage CORRECTLY rejected it, wwstage leniently accepted via the #128 size-keying. Append int|uint LAST (existing tags 0-4 frozen, zero byte-id churn) so BOTH stages accept by membership — closing the #128 int-path leniency by construction. int renders via the signed i64 path; uint via strconv.u64tos unsigned (+ a rawlenu64 width twin) — i64dec would render a high-bit uint negative. Staged cut: narrower widths + full types::numeric graduate per-caller (mirrors the f32-arm precedent).
fmt is NOT embedded in the selfhost compiler tools (grep-verified: 0 fmt fn-defs in w6c/wwdump combined.ww) — no combined.ww regen, compiler binaries unchanged. Pin: table-driven test/wcc/815_fmt_int_run (bare int, high-bit uint 2^63+1 positive, i64/str regression guards, byte-id).
def xs:[_]T=arrlit was sized 0 (no DATA emitted, garbage indexed reads) on BOTH stages, byte-id-identical: #7 wired [_] length-inference only on the let decl path, never def. cstage check.c N_DEF pass-2 infers the length from the initialiser and re-points both d->type and the SK_DEF Sym (an indexed read resolves the def through its Sym); wwstage check.ww runs inferarraylen before resolvewalk. Checker-only — cgen lays the DATA correctly once the length is stamped. w6c and wwdump combined.ww regen'd (both embed the wcc checker).
Pin: table-driven test/wcc/814_def_arr_infer_len (index reads int/u8/2d + 1-elem edge + negative build-fail), teeth-proven against a reverted inference. Filed separately, not folded (rule-11): def-global .len GAP-A (#7 cgdot twin), def str-array element DATA GAP-B (#270), [0]T-vs-[_] alen==0 conflation (pre-existing in the #7 let path too).
The str==/!= arm of cbinop had an N_IDENT fast-path that assumed the operand
was a local: localfind returns 0 for a module-global str, so it loaded
(BP)/8(BP) — saved-BP/retaddr garbage — into rt_streq. `p == sepstr` silently
compared garbage (returned wrong). Mirror #148's global branch at both sub-sites
(rhs/lhs): off==0 && let_islet -> LEAQ name(SB) base, load ptr/len. Distinct
per-site fast-path, not a shared choke (the by-value-global-arg family
#148/#150/#151 closes separately). cstage-only; the wwstage str== twin is #146
(-> #125 batch).
Pin test/wcc/989_strglobeq (table-driven: const+let globals, rhs+lhs ident,
==/!=, unequal + len>1 rows; teeth-proven). Surfaced by the lib/path c3
buffer-ops gate-1 oracle.
A let's own name was visible during its OWN initializer: cgen prepended the
new local into the name-keyed localfind chain BEFORE emitting the init, so
`let x = f(x)` read the fresh UNINIT slot, not the outer/param x. Both-wrong-
identical silent miscompile (gate-blind byte-id). Surfaced by path
dirname/basename (was the c3-posix path->p rename).
Align to Hare (harec check.c:1439 evals the init, then scope_insert). Fix,
both stages, IDENTICAL asm: reserve the frame slot BEFORE the init emits,
link the binding's name into the localfind chain only AFTER.
- cstage cgen.c: split localoff -> localslot(reserve)+link; N_LET's 12
case-level breaks -> goto letlink (tail links once); the inner-for break
is preserved; the 4 fatal() arms untouched.
- wwstage cgen.ww/cgenstmt.ww: new localreserve (= localalloc minus the
chain-link); cglet -> cgletbody(c,n,off) + a cglet wrapper that
reserves -> calls body -> links after.
Byte-id-safe on existing code: localfind is by-name, so deferring the link
is a no-op on every non-self-shadow let (grep = 0 self-shadow sites) — 990-997
stay green. Because both stages emit identical now-correct asm, byte-id
CANNOT catch this; the pin is a RUNTIME test, teeth-proven (revert -> pin
fails). test/wcc/989_letshadow{.ww,_run.c}: param-shadow, let-in-init shadow,
rename control, arrlit self-ref.
Embedded regen: selfhost/cmd/{w6c,wwdump}/main.combined.ww. Gate: all 325
passed, byte-id 990-997 green, w6c c587f4a1 / w6c_ww 7a69f898 (deterministic).
Faithful realignment of lib/path to Hare's buffer-centric API
(ref/hare/path/{stack,buffer}.ha). Replaces the old str-only path.ww
wholesale (zero consumers). Scope = stack-core: init() deferred (returns
~4KB (buffer|error), rides the #40 arc / #147); abs/dirname/basename land
in c3; extension/join dropped.
appendlit uses the #145 slice-copy-assign arm (buf.buf[lo:hi]=bs), no hand
loop. dot/dotdot are faithful module-global []u8 (D2 #148). MAX =
os.PATH_MAX-1. Divergences (size->i32 indices, frombytes, module-global
consts) cited inline.
Tests: lib/path/pathtest.ww (table-driven), wired at test/wcc/989_path_run.c;
push rows mirror stack.ha:107-111 verbatim incl the restored "/d"
intermediate. Slot 989 (overflow bucket) since 970 is taken.
C-first: cstage @test green; wwstage byte-id deferred to the #125 batch.
path classified M_WWREJECT in 989_lib_byteid (w6c_ww rejects the module-
global slice consts = #120/#29; + the #148 twin #151) — self-graduates back
to M_ID the day wwstage accepts. path moved off the 900_stdlib standalone
list (import-dependent: os.PATH_MAX def-dim + match over imported error
types; coverage at 989_path_run), per the bytes/fmt/os precedent.
Gate: all 324 passed, byte-id 990-997 green, w6c/w6c_ww unchanged
(lib-only, non-embedded).
The slice-IDENT call-arg fast path pushed the header words off off(BP)
where off=localfind(name); for a module-global slice localfind→0, so it
read saved-BP/RIP/caller garbage instead of name(SB). Add the global
branch (LEAQ name(SB) base, push 16/8/0 off it) mirroring the sibling
N_SLICE arm; local path unchanged. cstage-only: wwstage checker-rejects
the shape (#120), so byte-id-safe and the twin defers to #125. Unblocks
path c2-stack (dot/dotdot are faithful module-global []u8). Sibling
structarg fast-path filed #150.
Probe-first find for the path c2 appendlit (buf.buf[lo..hi]=bs): a
slice-copy-assign into a struct-field array sub-range emitted ZERO code —
silent NO-OP, both stages, both-wrong-identical (#263), so runtime is the
only net. N_ASSIGN gains an N_SLICE-LHS arm (cgen.c + cgenexpr.ww
slicebaseesz twin) reusing the N_SLICE-read base/esz cascade and copying
(hi-lo)*esz bytes from rhs.ptr via a runtime loop (len is runtime; no
REP/MOVSB). esz routed through the type table (rule 13; [N]u8->1). Hare
len(bs)==hi-lo assert deferred to #149.
A def-dimensioned array [MAX]u8 used as a struct field was BOTH-WRONG: cstage
loud-rejected ("array length must be an integer literal"); wwstage silently
sized the dim to 0, so the next field overlapped it (frame-smash). The
reference is neither stage — it is Hare: accept + fold the def.
cstage: fold the def into the dim via eval_def_const. The fold needs def NAMES
visible when resolve_typedecl walks struct bodies, so a stub loop binds
def-name stubs (type=NULL, filled in place by the existing def loop) before
resolve_typedecl — this extends check_file's existing names-first USE+TYPEDECL
pass to DEFs; def-TYPE resolution stays in its original order, and the
kind-filtered type lookup (#225) keeps the SK_DEF stub out of type position.
wwstage: one shared arrayelen(c, rhs) (INTLIT -> uval; else evaldefconst;
else 0) routed through astsize / tinfofornode / checkarrlitfits.
Closes#13's def-dim cstage-reject half (the slice-repeat clause stays open).
Pin test/wcc/951 (5 rows incl a cross-module os.PATH_MAX dim + a ~4KB shape;
teeth = cstage loud-reject + ww frame-smash). cgen-first blocker for the
path::buffer arc (type buffer = struct{[MAX]u8, ...}).
A void (size-0) error-singleton type-name used as a VALUE (return / let-init /
assign / call-arg) all share the N_IDENT non-local global-value load; the load
emitted MOVQ main.<singleton>(SB),AX for a payload symbol that never exists →
w6l undefined reference. Guard TY_VOID && !let && !def at the non-local
fallthrough so nothing is emitted; the enclosing widen arm stamps the variant
tag. wwstage was already tag-only correct — this aligns cstage up to it.
Pin test/wcc/949_void_error_singleton_run.c (6 rows, teeth = link-fail
pre-fix). cgen-first blocker for the path::buffer arc (error.ha is all !void).
An inferred-literal scalar module-global (`let s = 42;`) was wwstage
silent-wrong: the checker stamps the annotation N_TNAME("untyped_int"),
which letscalarprim does not recognise, so letemitsize returns 0 — the
global is dropped from collectlets (no DATA emitted) AND cgident falls to
the silent module-leaf (no load), running garbage. cstage defaults
untyped_int to an 8B int before emit (DATAW + MOVQ), which is correct.
Fix (wwstage-only, align up to cstage): defaultinferredlets in cgen.ww,
called from cgfile (cgendecl.ww) before collectlets, rewrites the
annotation "untyped_int" -> "int" (8B machine word, NOT i32 — the #108
truncation trap is the opposite polarity) for a module-level N_LET whose
rhs is N_INTLIT. All three consumers (letemitsize, emitletdataw, cgident
global-read) then resolve a concrete int. cstage is untouched.
Scope: N_INTLIT only. A const-expr inferred global (`let s = 7*6;`, N_BIN)
stays on its existing path — that is a separate live cs!=ww silent
miscompile tracked as #133, out of scope here.
Pin: 947_inferred_scalar_global_run — inferred `let s=42` (42, base
wwstage garbage) + typed control, cs==ww byte-id.
append/insert of a struct-LITERAL value evaluated the literal's field
exprs AFTER the grow, so a field reading the destination (e.g. len(xs))
saw the grown length. Both stages, #263 gate-blind (cs==ww byte-identical,
both wrong — runtime is the only net). #50 fixed the scalar/boxing value
arm; the struct-lit arm still post-grew.
Fix (mirror #50, both stages): resolve the struct, fill the literal into a
fresh per-site scratch (@appendstructscr, sized esz, survives rt_ensure +
nested-append clobber) BEFORE the grow, then copy scratch -> post-grow slot.
The copy uses the precise descending 8/4/2/1 ladder (the proven N_IDENT
struct arm directly below), NOT a raw 8B-word block copy: a struct's size
rounds to maxalign (check.c:916), so a sub-8B struct packs at a 4/2/1B
slice stride and an 8B copy over-writes past the slot — at a power-of-2
capacity boundary that clobbers the adjacent allocation (heap corruption,
both stages). The ladder never reads past esz (no uninit high bytes) nor
writes past the slot; esz=8 stays a single MOVQ (byte-id preserved).
insert() rides by construction: both stages desugar it to append and
re-dispatch into this arm. The #49 aplace path already uses the precise
ladder (verified, not exposed). #59 closes the last composite-value
eval-order hole in append/insert.
Pin: 946_append_structlit_evalorder_run — append / insert / narrow-neighbor
(i32-field at the cap boundary with an adjacent-allocation survival assert)
rows, each base-fail at 39432f7 and post-pass with cs==ww byte-id.
A tuple LITERAL with a declared-tagged element reached the cursor-fill
helper (cg_tuple_lit_to_cursor) through the generic cgexpr(N_TUPLE) arm
with no declared type, so the element was stored stamped-keyed at its
constructed scalar width rather than widened into the declared tagged box.
Both consumers ran silent and wrong on both stages (#263 gate-blind:
cs==ww byte-identical, both wrong — runtime is the only net).
#64 massign: N_MASSIGN derives a declared tuple type from the lvalue
binding types and threads it into cg_tuple_lit_to_cursor + the receive
loop (mirror of the #57 N_LET wire); a `_` target falls back to the rhs
literal element type for cursor stride.
#68 call-arg: the send is made param-aware (fill over the PARAM tuple) and
the restage guard graduates a declared-tagged element to a real widen
(reusing cg_widen_tagged_store); nested tuple/struct/array elements and
tagged elements with no param decl stay rule-7 loud. The matching
pop/drain is made param-aware too so push count == pop count: a
param-aware send pushes the box's N words, so the drain must pop N or the
SysV arg sequence skews. This is a push/pop balance requirement of the
send change, not a separate latent under-drain (the standalone trailing-
arg drain is already correct at HEAD).
Closed by construction: the only remaining cg_tuple_lit_to_cursor caller
passing NULL/nil is the generic cgexpr(N_TUPLE) arm, provably non-widening
(constructed type == governing type). The four widening consumers — LET,
RETURN, MASSIGN, call-arg — are all decl-wired. Whole-tuple single-ident
reassign from a tuple literal is rule-7 loud (task #49), not a silent
widening consumer, so the residual NULL arm stays non-widening.
Pin: 945_tuple_lit_declblind_run — massign / call-arg / `_`-control /
call-arg-drain / nested-tuple-ERR rows, each base-fail at abd97e6 and
post-pass with cs==ww byte-id.
A cross-module `&module.fn` in a const emitted no static reloc (the
const was never defined -> w6l undefined-reference, both stages) and
wwstage's checker rejected the const fn-table. #117/#119 wired the
&fn->DATAR const-data reloc for SAME-module &fn only; charclass_map
(fold-6) needs cross-module (12x &ascii.isXXX).
cgen: add the N_DOT arm to the &fn->symbol helper (node_fnptr_sym /
nodefnptr + the two ww emit sites), emitting mafn(leaf, module-ident)
-- exactly the symbol a runtime &mod.fn or a direct cross-module call
already emits. The helper is the SSoT for both the scalar (#119) and
tuple-row (#117) const-data paths, so one arm closes both.
checker: type a cross-module `&mod.fn` as `*fn(...)` in the TK_AMP arm
(the N_DOT twin of #206's N_IDENT fn-ptr synthesis, gated on a resolved
SK_FN/N_FNDECL leaf), so isassignable affirmatively accepts the const
table -- aligning wwstage UP to cstage's actual acceptance reason
rather than by abdication. The SK_FN gate keeps a non-fn `&mod.var`
from synthesizing a fn type (the one pre-existing nonfn-scalar cs!=ww
slip is N_IDENT-base, untouched and reproduces same-module).
One consumer-coupled commit (the checker accept gates wwstage cgen, so
neither half is independently testable). Narrow: slice-row + scalar
only; fixed-array (#118) and struct-field (#129) stay separate. Both
stages emit the correct cross-module symbols at the right tuple-slot
offsets -> byte-identical (990-997 green). Pin 949_xmod_fnptr_const_run
(distinct fns so a wrong reloc is caught + the SK_FN-gate axis). This
was the last fold-6 cgen blocker; charclass_map is now unblocked.
Reading or storing a tuple element of an indexed array element was
broken across the board (the fold-6 read-path). One fused commit,
both stages, four faces of indexed tuple-element access:
- FIELD read `tbl[i].N`: was loud ("unsupported field-read shape" --
the field-read dispatch keyed on an N_IDENT base; an INDEX base fell
to a fatal). Now resolves &tbl[i] via the place-spine and reads the
field at addr+foff through the existing per-kind arms (str-triple /
scalar / fn-ptr).
- WHOLE read `let e = tbl[i]`: was a silent word0-only truncation
(plain-tuple kin of #37/#58, which covered only tagged). Now a full
cursor fill from &tbl[i].
- STORE `a[i] = (3,4)` (N_TUPLE-literal rhs): was a silent word0-only
store -- the write face of the read. The aggregate-store-into-index
site handled ident/dot/deref tuple rhs but not the literal; now it
materializes the literal and word-copies. Narrow: N_IDENT base only
(N_DOT/chained stay deferred, #270).
- for-range over a const-slice-of-tuple: was a divergent SEGV; now a
symmetric loud-stop on both stages (filed #122).
The store and read were a round-trip that passed test 809 only by luck
(broken store XOR broken read canceled). Fixing the read alone exposed
the silent store; rule-7 obliges fixing both, so 809 is now genuinely
correct, not luck-correct. Both faces are byte-id-blind (#263) -- the
net is a runtime round-trip pin with distinct-per-word values and a
real call clobbering the cursor registers between store and read, so a
word0-only store or read is caught. Both stages byte-identical
(990-997 green). Pin 947_tuple_index_read_run.