A `let t: [N][]u8 = [a, b]` / `[N]str` literal init lowered each
element's {ptr,len,cap} header into AX/BX/CX (cgexpr) but stored only
some words: a slice element fell through to the scalar 1-word MOVQ
(dropping .len AND .cap), a str element stored 2 words (dropping .cap,
latent). Each element is 24B (post-#1) and must be copied whole.
wwstage was worse — a slice element matched no esz branch, so esz
stayed the 8 sentinel: the per-element stride collapsed (element i+1
overwrote element i's tail), the -96-vs-80 cs!=ww frame divergence.
This is the str/slice arm of the #270 aggregate-element-store family.
struct/array/tuple already copy correctly via the #270-1c is_agg
multi-word path; str/slice were the documented follow-up (cgen.c:9037,
cgenstmt.ww deferral). They can't join is_agg (that path word-copies
from a source slot and rejects non-ident/structlit elements, whereas
str/slice elements are commonly exprs cgexpr lowers into registers) —
the correct mechanism is the existing register header store, extended.
Fix (BOTH stages, converged byte-identical): cstage adds
is_slice_el = type_isslice(esub) and stores 3 words (incl CX->base+16,
the cap) for `is_str_el || is_slice_el`, in the main loop and the
repeat-fill. wwstage adds isslicel (esubti.kind == TY_SLICE -> esz =
esubti.size, fixing the stride) and the matching 3-word store. Closes
[N][]u8 (the bug) and the latent [N]str cap-drop in one branch.
The latent str cap-drop is now stored, but the indexed-element `.cap`
READ (`t[i].cap`) stays broken — a distinct cgindex/dot-selector bug,
cs!=ww divergent, filed as task #13. The new test validates the stored
cap via a whole-element copy (`let q = t[i]; q.cap`), which reads
through the correct ident-load path. [N]tagged literal init is the
remaining sibling (is_agg excludes TY_TAGGED), task #12.
Test 683_arr_strslice_elem: table-driven, dual-stage runtime + asm
byte-id; slice/str .len, 3-element stride-24, cap-via-copy, .ptr deref,
plus a [N]struct regression pin proving the is_agg path is untouched.
Three sibling arms of the #10 global-str/slice INDEX miscompile (23670d7,
the READ path) shared the identical N_TARRAY/N_TPTR tnode-KIND whitelist in
their global-ident resolution arm and were still LIVE and silently cs!=ww:
- cgun `&s[1]` / `&g[1]` (cgenexpr.ww N_INDEX addr-of) — a global str
(tnode N_TNAME) / slice (N_TSLICE) matched neither arm, so esz stayed at
the default 8 and the base fell to the complex-base fallback: a wide
{ptr,len,cap} header + 8-byte stride instead of MOVQ name(SB) (.ptr) +
ADDQ.
- cgassign `g[1] = v` store AND `g[1] OP= v` compound (two arms) — same
whitelist; a global slice store emitted a full-word MOVQ at an 8-byte
stride: an 8-BYTE OUT-OF-BOUNDS WRITE past a 1-byte element (memory
corruption) instead of MOVB at .ptr+1.
cstage (cmd/w6c/cgen.c) is the runtime-correct reference and was already
uniform across all three: esz off idx_eff(base->type)->sub->size and the
base load gated by is_arr (TY_ARRAY -> LEAQ name(SB), every other -> MOVQ
name(SB), since a str/slice's .ptr IS the symbol's first word). Align the
wwstage UP to that, mirroring the just-landed cgindex template (#10): resolve
esz via elemsizeofc with no kind gate, dispatch the base by N_TARRAY ? LEAQ :
MOVQ name(SB). The store/compound arms also resolve elemtn exactly like their
local branch (element node for ARRAY/SLICE/PTR; nil for str so tnodestoreop
picks MOVB) so a global []str store routes to the 3-word header store and the
compound arm's str/slice hard-error still fires.
Close-by-construction: the global element base/stride is now computed off the
resolved type at every wwstage index site — read (cgindex, #10), addr-of
(cgun), store + compound (cgassign) — with no remaining tnode-kind whitelist.
cgslice/cgbaselen already resolved via elemsizeofc.
803_globalidx_run extends from 9 to 18 rows: global str/slice addr-of (read
back through the pointer), global slice store AND compound store `g[i] OP= v`
(the distinct third fixed arm, with adjacent-element addends as the OOB-write
guard on both), a WIDTH>1 signed variant of each (esz=4 stride/store-width pin),
and local addr-of/store regression pins. Runtime (cstage build+run) + cs==ww
byte-id per row. combined.ww embeds (w6c + wwdump) regenerate.
Indexing a GLOBAL `str` or GLOBAL slice (`s[i]` / `g[i]` where s/g are
module-level lets) read a wide {ptr,len,cap} header with an 8-byte stride
and a full-word MOVQ load instead of the .ptr + element-width load. So
`s[1]` over a global str read 8 bytes at ptr+8 rather than the single byte
at ptr+1 (cstage emits MOVZBQ). LOCAL str/slice index was already clean.
Root: wwstage cgindex (selfhost/cmd/wcc/cgenexpr.ww) dispatched the element
size + base-materialisation off the base tnode KIND, enumerating only
N_TARRAY (global `[N]T`) and N_TPTR (global `*T`). A global str (tnode
N_TNAME "str") and a global slice (N_TSLICE) matched NEITHER arm, so esz
stayed at the default 8 and the base fell through to the wide-header
fallback. cstage `case N_INDEX:` (cmd/w6c/cgen.c) dispatches esz off the
RESOLVED base type (`idx_eff(lhs->type)->sub->size`), uniform across
local/global/str/slice/ptr.
Fix aligns cgindex's global-resolution arm UP to cstage's uniform type-
driven dispatch — the same template the sister fn cgslice already uses:
resolve esz via elemsizeofc(c, tn) with no kind gate, then drive the base
load by tn.kind == N_TARRAY ? LEAQ : MOVQ name(SB). A global str/slice now
resolves esz=1 off the type table (elemsizeofc, just fixed in #8 to read
stamped tinfo) and routes through the EXISTING isglobalptr emission
(MOVQ name(SB),BX; ADDQ; MOVZBQ (BX),AX) — byte-identical to cstage. The
element-kind flags (elemisstr/elemisslice) for a global `[]str`/`[][]u8`
element are still set by the downstream block, so those route to cgslicehdr
unchanged.
Close-by-construction: cgindex's one global-ident resolution arm is the
single site computing a global element base for the read-index path (the
&arr[i] address-of in cgun and the arr[i]=v store in cgassign are separate
node paths, out of scope). Any indexable global base now resolves esz off
the type table, exactly like cstage and like cgslice.
combined.ww embeds regenerate (w6c + wwdump). New 803_globalidx_run pins
runtime (cstage build+run) + cs==ww byte-id across global str index
(positions 0/1/2 + sum), global slice index (TEXT-only byte-id — a bare
`let g: []u8;` decl emits a divergent zero-header DATAW orthogonal to the
index read, the #7/#18 static-init family), and local str/slice/array
index regression pins. A stride-8 regression re-fails the 5 global rows.
Replace kwlookup's 30-arm streqn if-ladder with two parallel module-level
tables — kwnames: [30]str + kwkinds: [30]tkind — scanned linearly via
strings.compare, and delete the hand-rolled streqn. Rides #18 (module-
level [N]str static-init + relocations) for the kwnames data and #8
([N]enum element sizing) for the kwkinds[i] read, which is itself the
construct that surfaced the #8 elemsizeofc/array-init-store miscompile.
Two parallel arrays rather than a [N]kwent array-of-struct: a str inside
an aggregate element is the filed #18 follow-up. Mirrors the C twin
cmd/wcc/tok.c kwlookup (N=30, linear, no hash). Source re-applied on top
of the #8 cgen fix and regenerated fresh: the wwstage-compiled toktest
now runs correctly (exit 0, no segfault) where pre-#8 it smashed the
frame on the local [N]tkind init store.
wwstage sized a named-enum array element (`[N]tk`, tk = enum i32) as a
raw 8-byte slot instead of its i32 backing (4), via two sibling code
paths that both derived the element width structurally and missed the
enum's underlying size:
- elemsizeofc (cgenutil.ww) was the odd-one-out among the elem*c
helpers: elemissignedc/elemisfloatc already read the checker-stamped
tinfo (t.type_.sub), but elemsizeofc went elemsizeof->primsize->
slotsize, and primsize("tk")=0 fell through to 8. This drove the
cgindex READ: `a[i]` strode by 8 (MOVQ) where cstage strode by 4
(MOVSXD), reading the wrong/out-of-bounds element for i>=1.
- the array-literal init STORE (cgenstmt.ww) computed its own esz the
same way (primsize=0 -> stayed at the 8 sentinel, enum is not an
aggregate), so a local `[N]enum` literal stored at stride 8 into a
stride-4 frame slot, overrunning it and smashing the saved BP /
return addr -> wwstage-built binary SEGFAULTED.
Both align UP to cstage, which reads the stamped element size uniformly
(N_INDEX idx_eff(bt)->sub->size; N_LET array-init lu->sub->size,
cgen.c:6387). The read fix brings all four elem*c helpers onto the same
tinfo SSoT; the store fix takes the stamped element size for a narrow
scalar. Closing both close-by-construction at the size source.
No in-tree [N]enum / aliased-narrow element existed before kwtab, so
this was byte-id-gate-blind until now. test/wcc/682_arr_enum_elem.c
pins it table-driven: global+local reads, local init-store, signed
sign-extend, and a frame-smash row, each run through both stages with
exit-code and cstage==wwstage asm-byte-id checks.
`len(xs[i])` over a [N]str/[]str (and []T slice) element returned the
element's .ptr, not its length, on BOTH stages (shared gap, not rule-10):
the len() builtin had no N_INDEX arm, so it fell to the bare-cgexpr
fallback, where the N_INDEX str/slice load (cgslicehdr) leaves AX=.ptr,
BX=.len, CX=.cap — and len() returned AX (the ptr) as the length.
Add an N_INDEX arm gated on a (TY_SLICE||TY_STR) element in both stages:
cgexpr the element, then MOVQ BX,AX to shuffle the len word into the
result reg — the same shape as the #14 .len pseudo-field fix. Byte-id
neutral (no bootstrap source uses len(indexed-element)); regenerated
w6c + wwdump combined.ww. New 802_lenidx_run pins runtime + cs==ww.
A module-level `let xs: [N]str = ["a","b",...];` static init emitted no
.data: a str element carries a ptr->rodata relocation, not just bytes, so
it fell through the byte-only array-emit path and left the table symbol
undefined (w6l: undefined reference). Shared gap on both stages, not
rule-10.
emit_strarray_data / emitstrarraydata apply the scalar-str-global pattern
per element at offset idx*esz: a DATAW row of {0-ptr placeholder, LE len,
cap} plus a per-element DATAR sym+idx*esz,_S_n reloc. let_pre_intern /
letpreintern pre-intern each element strlit so the _S_ rodata rows precede
the DATAR references. Stride routes through etype->size (rule 13). Scoped
to the DATAW (`let`) directive: A_DATAR requires a DATAW holder, so
`def [N]str` and str-in-aggregate stay a filed follow-up.
919_strarray_static_run pins runtime (len-sum, element .ptr deref, var
index, empty slot, repeat suffix) + cs==ww byte-id. w6c + wwdump
combined.ww regenerated.
The open-coded len-guard + byte-by-byte equality loop in localfind is
exactly strings.compare(ln, name) == 0 (==0 demands equal length AND
equal bytes; no prefix false-match). Matches cstage cgen.c:1669
strcmp(l->name, name) == 0. The @-fallback streq + localfindnode streq
calls are shared-helper calls, left for the wholesale swap (#17).
Regenerated w6c + wwdump combined.ww (the only amalgamations embedding
cgen.ww's localfind).
main.ww's 33 piecewise stderr diagnostics carried hand-counted byte
literals (os.write(2, "lit".ptr, NNu64)). Route them through a
tool-local cerr(m: str) that takes m.len, removing the hand-count
hazard. strictpkgmismatch + `ww test` FAIL keep their runtime-length
*u8 writes (no literal to count). Regenerate main.combined.ww.
Not byte-id-neutral (error-path asm), but stderr + exit codes are
byte-identical old-vs-new across the driver diagnostics: verified by
wwstage-vs-wwstage oracle (cstage ww diag wording diverges pre-fold).
The three byte-id-critical import scanners in cmd/ww/main.ww drop
their hand-rolled byte-pyramids for strings.has{prefix,suffix} over
constructed str views, keeping every boundary guard that the lifted
helpers do not subsume:
- dirfilekeep: nlen<=3 guard kept (bare ".ww" len-3 stays rejected,
which strings.hassuffix(".ww") alone would wrongly accept).
- scanuse: i+7>len guard kept (hasprefix("import") covers 6 bytes;
the separator read at src[i+6] still needs i+6 < len).
- peekpackage: s+8<=q guard kept (hasprefix("package") covers 7
bytes; the separator read at buf[s+7] needs s+7 < q).
bytecmp (the enumeratedir sort comparator) is left as-is: it is a
magnitude-returning 3-way memcmp at a read boundary, not a spell-out.
Regenerated ww/main.combined.ww. All 6 corpus .combined.ww remain
byte-identical (cstage ww == wwstage ww_ww); 990-997 byte-id + 993
self-rebuild green.
57 piecewise stderr diagnostics in the checker spelled out
os.write(2, m.ptr, NNu64) with hand-counted byte literals. Replace the
38 hand-counted literal sites and 19 var sites with a tool-local
cerr(m: str) helper that takes .len off the str, eliminating the
off-by-one hazard. All 38 prior hand-counts were already correct, so
stderr is byte-identical. cerr lives in check.ww (not err.ww, which
ports err.c for the dead C-driven path); regen w6c + wwdump
combined.ww.
23-arm top-level if (k == nkind.N_X) dispatch ladder becomes one
switch (k) with an empty-label default case for the AX=0 fallback.
N_RUNELIT stays a separate arm (no float check, unlike cstage's
INTLIT grouping). Not byte-id-neutral (if-chain -> switch); 990-997
cstage==wwstage byte-id is the functional-equivalence gate. w6c +
wwdump combined.ww regenerated.
A string literal is TY_UNTYPED_STR, not TY_STR, so `"abc".len` missed
the typed slice/str pseudo-field gate in cgen.c's N_DOT and fell to the
final base-eval fallback, which left AX=.ptr — `.len` returned the
pointer instead of the length. wwstage's cgdot catch-all already did the
BX->AX shuffle, so the two stages diverged (rule-10). Align cstage UP:
the N_DOT fallback emits MOVQ BX,AX for `.len`. `.ptr` is unchanged
(already returned AX); `.cap` deliberately not added (wwstage catch-all
is ptr/len only — mirror exactly).
byte-id was blind here: no bootstrap source uses literal `.len` (lengths
are hardcoded around literals), so the gate never exercised it. New test
801 pins both dimensions (cstage run + cs==ww byte-id) over
len/empty/multibyte/ptr-deref/arg-passthrough rows.
perr() in parse.ww wrote the "w6a: " prefix with a hand-counted length
of 4, but the string is 5 bytes — the trailing space was dropped, so
every assembler diagnostic printed as "w6a:<file>" with no separating
space. Replace the prefix length (and the ": " / "\n" literal writes in
the same function) with the string's own .len via the local-binding
idiom, fixing the off-by-one and closing the hand-count class here. Uses
str-variable .len (correct on both stages), not "literal".len (cstage
miscompile, #14), so this is byte-identical cs==ww.
Verified: w6a_ww on a bad input now writes "w6a: <file>: <msg>\n" with
the space restored; w6c and w6c_ww emit byte-identical asm for the
regenerated main.combined.ww.
The 5 literal os.write diagnostics in obj.ww ("cannot read object",
"missing .text", "missing .symtab", and two "duplicate symbol") passed
hand-counted byte lengths that were each short by one, dropping the
trailing '\n' so every diagnostic printed without its newline. Replace
each magic length with the string's own .len via the local-binding
idiom (let m: str = "..."; os.write(2, m.ptr, m.len: u64);) — the
established wcc/err.ww + w6c/w6l/main.ww pattern — which fixes the
off-by-one and closes the hand-count class by construction. Uses
str-variable .len (correct on both stages), not "literal".len (cstage
miscompile, #14), so this is byte-identical cs==ww.
Also fold two trivially-safe nested-if collapses in the same file:
the archive-member skip guard (three sequential `if (first != ...)`
with no else → one &&-chain) and the text/data exclusivity guard
(`if (intext) { if (indt) ...`→ `if (intext && indt)`).
Verified: w6l_ww on a missing object now writes the full
"w6l: cannot read object\n"; w6c and w6c_ww emit byte-identical asm
for the regenerated main.combined.ww.
The argv-error diagnostics in w6c/main.ww (7 sites) and w6l/main.ww
(13 literal sites) passed hand-counted byte lengths to os.write that
were systematically short by one — every length dropped the final
byte (usually '\n'; "w6l: cannot find -l" dropped the 'l'), so the
diagnostics printed truncated. Replace each magic length with the
string's own .len, which both fixes the off-by-one and closes the
hand-count class by construction.
Uses the local-binding idiom (let m: str = "..."; os.write(2, m.ptr,
m.len: u64);) — the established wcc/err.ww pattern — rather than
"literal".len directly: string-literal .len is miscompiled on cstage
(returns the pointer, not the length; cstage != wwstage), filed as
#14. str-variable .len is correct on both stages, so this is byte-
identical cs==ww and independent of #14. The w6l runtime cstr write
(os.write(2, nm, cstrlen(nm))) is unchanged.
The local doexit reimplemented os.exit via a raw rt_syscall(60) decl
with no documented divergence, while the file already imports + uses
os. os.exit (lib/os/os.ww:68) is byte-identical (syscall1(nr.EXIT=60));
ostest/stattest siblings already use os.exit.
strings.bytesub two endpoint guards, wcc cgdot/cgassign 4-deep
allptr/N_IDENT/localfindnode pyramids, and w6l isarchive's 8 sequential
magic-byte rejects. The isarchive len<8 read-guard stays a separate
statement before the || chain so the byte reads remain bounded. Not
byte-id-neutral (short-circuit emits tighter branches / renumbered
labels) but functionally identical; cs==ww stage-parity holds.
Regenerated all embedding combined.ww.
Fold the 13-arm Jcc else-if pyramid in encode() to a single switch (op)
with the terminal else (isjcc=false) as the empty-label default case.
Pure op->cc value mapping, so a switch expresses it exactly.
Not byte-id-neutral (if-chain -> switch dispatch changes the asm). The
ladder<->switch byte-emission equivalence is pinned by 991_w6a_ww, whose
corpus (wwdump/w6l/w6a main.s) emits all 13 Jcc conditions, and was
independently reproduced at landing (all 13 mnemonics assemble
byte-identical C-w6a vs w6a_ww).
Regenerates the w6a combined.ww amalgamation (Jcc region only).
Fold the 69-arm `if (k == nkind.N_X) return "..."` ladder in nkname to a
single `switch (k)` with the terminal `return "?"` as the fall-past
default. The other ast.ww ladders stay: pr()'s kind dispatch is
side-effecting (emits output, recurses) and uses ||-grouped multi-kind
predicates, not a pure value->value mapping a switch can express.
Not byte-id-neutral (if-chain -> switch dispatch changes the asm), so
the ladder->switch equivalence is pinned by a new table-driven test:
lib/ww/asttest.ww drives nkname over every nkind plus the out-of-band
"?" fallback, wired as 905_nkname_run (same `ww run` @test shape as
904_tok_run). The 990_selfhost wwdump diff only covers kinds that
appear in its corpus.
Regenerates the w6c + wwdump combined.ww amalgamations (nkname region
only).
Fold the ~88-arm `if (k == tkind.TK_X) return "..."` ladder in tokname
to a single `switch (k)` with the terminal `return "<?>"` as the
fall-past default. kwlookup stays an if-ladder: it dispatches on
streqn() string compares over distinct literals, which a value-switch
can't express.
Not byte-id-neutral (if-chain -> switch dispatch changes the asm), so
the ladder->switch equivalence is pinned by a new table-driven test:
lib/ww/lex/toktest.ww drives tokname over every tkind plus the
out-of-band "<?>" fallback, and kwlookup over every keyword plus
non-keywords, wired as 904_tok_run (same `ww run` @test shape as
904_ascii_run). The 990_selfhost wwdump diff only covers kinds that
appear in its corpus.
Regenerates the w6c + wwdump combined.ww amalgamations (tokname region
only).
Drop 112 redundant `let x: T = rhs` annotations where the rhs already
infers T (newnode→*node, p.curfile/curtext→str, p.curline/curcol→i32,
accepttok/== →bool, parse*→*node). stmt.ww (75) + parse.ww (37).
Regenerate the two embedders' combined.ww (w6c, wwdump). Byte-id-neutral:
cstage-w6c asm of each combined.ww is identical pre/post.
61 over-annotations dropped where the rhs unambiguously infers the
declared type: 22 overflow:bool comparison binds in checked.ww, and
the mem/s/sl memio.stream/io.stream/log.stdlogger triplet across 13
@test sites in logtest.ww. Sub-word res:/fullres: binds with casts or
truncation are kept. Byte-identical asm in both stages, both files
non-embedded.
byte-id-neutral: every magic-decimal sub is value-equivalent to its
ASCII char-literal. 0x7F (ELF byte-0), ELFCLASS64=2, NUL sentinels,
and hex/itoa radix arithmetic kept decimal (no char identity).
Regenerated w6a/w6l main.combined.ww (the only two embedders).
Pure byte-id-neutral substitution of ASCII magic decimals with char
literals across lex.ww (131) + tok.ww (19); regen w6c + wwdump
combined.ww (lex is embedded in those two only).
escape() out-values for \a (7) and \b (8) are now '\a'/'\b' — both
the C bootstrap lexer (cmd/wcc/lex.c:160-161) and ww's own escape()
map them, so the literals are value-equivalent and stage-symmetric.
Digit-value arithmetic (parseint/parsef64/hexchar: c - '0' over u8)
is left decimal: a rune/i32 char literal would promote the u8 operand
and is not byte-id-neutral. Multi-byte type-suffix comments (i8../f64)
are kept — they label more than a single char.
Replace magic ASCII decimals with char literals in ascii/fnmatch/shlex
predicates (e.g. `c < 48` → `c < '0'`). Byte-id-neutral: ascii params are
rune, so rune<rune emission is unchanged; fnmatch/shlex compare u8 against
value-preserving (<=126) rune constants. Range bounds (0/31/127), the ±32
case offset, the 128 high-bit mask, and fnmatch 0u8 sentinels stay decimal.
Regenerate the three combined.ww that embed ascii (w6c, wwdump, smoke).
Add functional rows pinning predicates reachable only via fnmatch ctype
classes / shlex split: [[:space:]]/[[:print:]]/[[:graph:]] + the '\t' arm
of [[:blank:]] (fnmatchtest), '\t'/'\n' split separators + issafe's
special-char set (shlextest) — so a wrong substitution would be caught.
An empty `[]` carries no element type; ww gets it only from a let
annotation (the #45 retype). Both stages used to silently default the
element to u8, and in value-form positions (return / call-arg) the
lowering miscompiled — malloc(8) ignoring n, a 16B *u8|nomem where a 24B
slice was expected (#5). Now every empty alloc that isn't a
let-annotated binding fails to infer with a loud error, aligning ww DOWN
to harec (ref/harec/src/check.c:1801-1802).
Mechanism: clet / checkletassign flags the single alloc call node that a
`let x: []T =` rescues (save/restore around the init walk); the alloc
branch errors on any empty alloc that isn't that node. The #45 wide-T
retype path is kept. wwstage needs an extra not-yet-stamped guard because
resolvewalk re-types value nodes context-free after checkletassign.
Tests: negative cstage-driver 729 (table-driven: bare-let, return,
call-arg, assignment) + positive @test in attest_pass.ww exercising the
u8 and the wide-i32 (#45) paths at runtime. Both stages reject
symmetrically; byte-id verified on []u8 and []i32.
The wwstage checker rejected a match-bound binder used in a `yield` arm
of a match-AS-EXPRESSION (`let v = match (x) { case let p: *T => yield
*p; ... }`) with asserttyped:un/bin/index; cstage compiled it.
resolvewalk stamps the yield operand's type_ during the in-scope N_MCASE
arm walk. exprtype's N_MATCH arm then derived the match's type by
re-running exprtype on the same operand to recover a type NODE — but the
arm binder's scope is already popped, so the re-derive returned nil and
the N_UN/N_BIN/N_INDEX restamp arms overwrote the good in-scope stamp
with nil. cstage never re-runs: match_yield_type reads the operand's
cached ->type (cmd/wcc/check.c:121).
Root fix (align wwstage UP): matchyieldtype now returns a *tinfo and, at
the post-walk call, READS the operand's cached node.type_ instead of
re-running exprtype — so no operand shape can be clobbered by
construction (deref/bin/index all vanish, no per-arm guards). The
exprtype N_MATCH consumer stamps e.type_ from that tinfo directly (no
tinfofornode round-trip). The pre-walk call (checkletassign L302 /
checkretassign L303 run before the in-scope arm walk, so the operand is
nil there) keeps the nil-safe re-derive — benign and load-bearing: it
types the void-arm literal so let/return-assign has a usable node. The
re-derived node (or btype for the bare-binder idiom) is carried back via
an out-param for the assignability check and for the N_MLET/N_MASSIGN
tuple-destructure consumers (`let (a,b) = match { case let t => yield t
}`, test 945). cstage is single-pass so its else is dead; eliminating
the pre-walk call is #279.
Supersedes the narrow N_UN non-clobber guard (removed — its match
consumer is gone). @test check_match_ptr_deref extended to pin the whole
operand class (deref / bin / slice-index / deref-then-field), dual-stage
(910 + 997) with correct runtime + cs==ww byte-id. The *[N]T ptr-to-
array index variant is blocked separately by #278. Both compiler-
imported combined.ww regenerated. smoke + test-unit (242) + 994 w6c_ww
byte-id (18 corpus incl. selfhost combined.ww) green.
Review fixes for the #272 fold (reviewer272b gate; rob+ken ruling). Bundled
because the wwstage catch-all message carries the citation and the combined.ww
regen covers both .ww edits.
- wwstage cgreturn close-by-construction catch-all keyed on the SYNTACTIC
return-type node (N_TARRAY / N_TNAME+structlookup), so a named-alias
aggregate return type (type a=[N]T / type a=struct) bypassed both the
handling arms AND the loud-stop, falling to the scalar default = silent
segfault/truncation; cstage (type_chase_named at all 4 N_RETURN sites)
stayed correct. Re-key the catch-all on the RESOLVED tinfo (chase
TY_NAMED -> TY_ARRAY/TY_STRUCT) so wwstage LOUD-STOPS (rule 7) instead of
miscompiling. cstage stays correct; the full wwstage tinfo-kind dispatch
(align UP, byte-id) is #277. Established wwstage-stricter divergence
(cf #264), no bootstrap consumer (990-997 green).
- #276 citations at-site (both stages): the cstage >24B array-literal return
loud-stop and the <=24B STRUCT global-receive residual now cite #276. The
wwstage >24B array-literal routes through the tinfo-keyed catch-all
(#272/#276/#277). Correction: ALL <=24B struct globals truncate
symmetrically (byte-id-clean), not only float-bearing -- #276 broadened.
- Cosmetic: fix a double-encoded U+2264 (mojibake) in the cgen.c commit-2
comment.
combined.ww regenerated (#110).
The caller-half of the global case: `g = mk()` into a GLOBAL array
stored only the first word — a ≤24B reg-return landed `MOVQ AX, g(SB)`
(8 of 24 bytes); a >24B sret-return hit the #220 sret-to-symbol gate
which was TY_STRUCT-only and fell through to the same truncation.
≤24B: the local aggregate-receive arm was `off != 0`-only, so a global
array fell to the scalar IDENT store. Add a global ARRAY arm — LEAQ
name(SB), DI then store the full+tail words from AX/DX/CX (an array is
never float-class, so AX/DX/CX is always the transport; no `g+8(SB)`
operand form exists). Mirrors the str/slice global arm.
>24B: add TY_ARRAY to the #220 sret-to-symbol gate (cg_sret_dest_sym /
sretdestnode) — the callee writes the whole array through RDI.
A ≤24B STRUCT global receive can be float-class (X0/X1, not AX/DX/CX),
so it is left at its pre-existing symmetric behaviour — no consumer.
949_aggret_source_run gains global_recv (c → 15) and global_recv_sret
(>24B → 22), both with per-row byte-id.
The N_RETURN aggregate arms gated the return source on N_IDENT ||
N_STRUCTLIT; every other aggregate rvalue (array literal, o.field N_DOT,
a[i] N_INDEX, *p deref) fell through to the scalar-AX default = a silent
8-byte truncation. Both stages emitted byte-IDENTICAL wrong asm, so the
byte-id gate could not catch it (#263 class) — the fix converges on the
runtime oracle.
Mirror the arg-side closure #271 landed: both arms (≤24B @retscr and
>24B sret) now funnel N_ARRLIT through the literal element fill and
N_DOT/N_INDEX/deref through aggarg_srcaddr + the #265/#268 whole-
aggregate copy. Type-agnostic, so struct AND array returns are closed.
A close-by-construction loud-stop (rule 7) guards any future unhandled
aggregate source from reaching the scalar default.
Closes the callee-half of (b)/(c) and the addressable siblings. The
g = mk() global-receive caller-half is commit-2.
949_aggret_source_run pins the class: array-literal / N_DOT / N_INDEX /
deref / named-ident control / >24B-sret-deref / struct-field / struct-
deref, each summing all members (full readback) with per-row byte-id.
Passing an aggregate BY VALUE as a call argument worked ONLY for a ≤16B
struct from an IDENT source; every non-ident source — CALL mk(), N_DOT
o.f, N_INDEX a[i], DEREF *p — and every array / >24B-struct (even as an
ident) fell to the scalar default: one PUSHQ for a multi-word aggregate,
stack-imbalancing against the type-based multi-word drain. cs!=ww, both
garbage (f(mk()) cs4/ww236, f(o.f) cs8/ww108, f(a[i]) cs4/ww28, f(*p)
cs4/ww140; arrays + 32B sret struct same).
The arg-pass twin of the #265/#268 let-init copy. A new aggregate-arg
push arm materialises the source into the arg convention: the source
ADDRESS in SI (ident LEAQ / deref operand / dotchainaddr #253 /
&base[i] spine #252-270) then its ceil(sz/8) words pushed high→low; a
CALL receives first — ≤24B in AX/DX/CX pushed straight, >24B sret'd
into a per-fn @aggargscr then pushed from there. The pop-forward drain
gained a matching array / >16B-struct arm and the callee prologue an
is_bigagg receive (ceil(sz/8) GP eightbytes), so caller and callee
agree on the multi-word layout. The ≤16B-struct-IDENT fast path is
untouched (byte-id preserved).
The new-arm exclusion is TYPE-keyed (the stamped tinfo, mirroring
cstage node_isstructarg over args[i]->type), not the name-keyed
structparamsize — a name-keyed gate re-opened the #211/#13 cross-module
same-leaf collision (784 symmetric: an 8B `sa.s` struct whose
name-resolution collides with `sb.s = *vtable` would miss the struct
fast path and wrongly enter the new arm, diverging from cstage's
1-word push). A float-bearing ≤16B struct from a non-ident source
loud-stops in both stages (the #165 SSE eightbyte transport the GP
push/drain can't model; out of scope). A const array/struct `def`
global as an aggregate arg is aligned DOWN to the leaner wwstage
(both loud-stop) per rule-10.
#110: cgen is compiler-imported by w6c + wwdump — main.combined.ww
regen'd for both.
949 rows: arg_{struct16,arr16,struct32}_{call,dot,idx,deref,ident},
full member readback (struct 16B reg-class + 32B sret-class + array
[4]u32, each non-ident source + ident control); byteid=1 throughout
(master both-broken-and-divergent → converge on the correct full
push, #263). All 111 dotbaseaddr + 3/3 784 pass; test-unit 241 green;
sizelint + smoke OK; the full w6c compiler source (214705 asm lines)
self-compiles cs==ww byte-id.
`let x: [2]inner = [inner{..}, inner{..}]` left the array unpopulated:
the N_ARRLIT per-element store handled scalar/str/float ONLY, so a
struct/array/tuple element hit the multi-word-store gap and stored just
the first 8 bytes (cs0/ww0). Both stages symmetric-broken; converge on
the populated result (#263).
Fix: an aggregate element of an array literal fills each element slot
from its source — cg_structlit_fill_bp for an N_STRUCTLIT element,
word-copy for an N_IDENT element (reusing COMMIT 2's per-element copy
shape). esz is the element's natural size (cstage esub->size). cgen.c
N_ARRLIT arm + cgenstmt.ww cglet. An aggregate `...` repeat and other
element shapes hard-stop loud (rule-7).
949 rows: arrlit_structlit, arrlit_structident (8B struct, byteid=1,
full readback). All 96 pass; test-unit 241 green; smoke OK.
The array-of-struct element store/copy family — one primitive (&(array
element) for an AGGREGATE element, used as address, never deref/truncate)
across three consumers. Both stages were symmetric-broken; converge on
the runtime-correct full-address/full-copy (#263).
(1a) `a[i].m[j] = v` (a:[N]struct) segfaulted: the `arr[i].field` arm
computed &a[i] then DEREF'd it (loaded the struct's first 8 bytes as a
value) for an `[N]T`-typed field → garbage base. Now an array-typed
field of an array element leaves the field ADDRESS (the #135 read-side,
applied to the array-element base). cgen.c arm + cgenexpr.ww cgdot
N_INDEX-lhs branch.
(1b) `a[i] = aggregateval` truncated the copy to an 8B MOVQ. New
aggregate (struct/array/tuple >8B) element-store branch word-copies the
element from the rhs source address (ident / N_DOT field / `*p` deref) —
the WRITE-twin of the #268 let-init loop. cgen.c N_INDEX store +
cgenexpr.ww cgassign.
(3a) `let c = x.arr[i]` (N_DOT base) / `let c = a[i][j]` (nested) dropped
the copy: the #268 let-init N_INDEX source-addr arm was N_IDENT-base-
gated. Now computes &base[idx] via cg_dotbase_addr (N_DOT field) or the
&abase[bidx] spine (nested N_IDENT-array base). cgen.c N_LET +
cgenstmt.ww cglet.
949 rows: elemfield_store, elem_struct_store, elem_arr_store,
letcopy_{dot,nest}_prim, letcopy_subarr (byteid=1); letcopy_{dot,nest}_
struct (byteid=0 — run-correct, byte-id blocked by the orthogonal
value-nested-struct frame divergence #254). All 94 pass; test-unit 241
green.
elemsizeofc drilled a 2D `[N][M]T` base's OUTER-index stride down to the
scalar T (the documented elemsizeof FOOTGUN: it bottoms out at the inner
prim size, 4 for [M]u32). The `direct != 8` short-circuit then returned
that scalar size, so wwstage emitted esz=$4 where cstage emits $12 (the
sub-array size, idx_eff(bt)->sub->size = sub.size*elen, type.c:121). The
runtime stayed self-consistent (write+read the same wrong stride) so it
masked until a CROSS-CELL access — a[0][j] and a[1][j] alias.
Fix: detect a nested-array element ([M]T inside [N][M]T) before the
short-circuit and return the element-array tinfo's natural .size, the
sub-array stride. wwstage-only; aligns up to cstage. w6c unchanged.
949 rows: nest2d_u32/u8/i32 (cross-cell write+readback, byte-id).
The fold-1b unified arm (bb2f4e1) added an N_IDENT addressable-rhs source
setup, but the two stages gated the GLOBAL case differently: cstage used
let_islet || def_isarraydef, wwstage used isletvar || deflookup (ANY def).
On a struct-typed `def` used as an aggregate-copy rhs (`let c: T = G`)
wwstage copied the whole value (correct) while cstage truncated to the 8B
scalar tail — a cs!=ww divergence (rule-10). A struct-LET global already
copies on both, so the def gap was also an internal cstage inconsistency.
Struct defs are first-class laid-out aggregates (DATA storage + field
load, #129 A.2/A.3), so converge on the correct full copy on both: add
def_isstructdef to cstage's predicate and replace wwstage's broad
deflookup with the def_is{array,struct}def pairing already held identical
in defisaddressable. 949 +2 rows (array-def + struct-def global, full
readback, byteid=1).
#265 fold-1 landed the deref-rhs aggregate copy as one slot→slot memcpy
loop fed from a source address in SI. fold-1b adds the remaining
addressable-rhs source-address setups, all routed into that SAME loop:
- array IDENT `let c: [N]T = s` — LEAQ the source slot into SI.
Pre-fix both stages truncated to the 8B scalar tail.
- N_DOT field `let c: A = o.i` — cg_dotchain_addr / dotchainaddr
(#253) lands &(o.i) in SI. Pre-fix truncated to 8B.
- N_INDEX element `let c: A = a[i]` — the &base[i] spine (#252:
scaled index + LEAQ base) lands the element address in SI. Pre-fix
scalar-loaded the element address as a value → segfault.
Size (the #254 non-slot-padded ABI extent) comes from the declared let
type for every shape (lu->size / structabisize|tinfo.size), independent
of the rhs; only the per-rhs address setup differs. The deref arm
becomes one branch of the unified arm. Struct-IDENT keeps its own #32
slot-copy arm above (unchanged). With those, the whole addressable-rhs
let-init-copy family is closed by construction: struct-ident / array-
ident / deref / N_DOT / N_INDEX all full-copy, both stages byte-identical
(rule-10).
949 gains 9 full-readback rows (every member written distinct + summed,
so a partial copy fails): array-ident 16B/32B + 12B(MOVL)/11B(MOVW+MOVB)
tails; N_DOT struct-field 16B + array-field 32B + 11B-tail struct field;
N_INDEX struct element 16B/32B. The N_INDEX source array is populated
through a `*inner` to `&a[i]` (the #135/#252 store path) because the
array-of-struct element direct store (`a[i].m[j]=v` / `a[i]=s` / struct-
array literal) segfaults on a SEPARATE pre-existing bug, reported
alongside this fold. w6c+wwdump combined.ww regen (#110). 70/70 949,
test-unit 241, sizelint, smoke green.
The literal array dimensions ([64]u8/[8]u32/[64]u32) where Hare uses the
BLOCKSZ def / [_]u32 are forced by ww rejecting a def in array-dimension
position. rule-7 requires a retained divergence carry a filed-task
pointer; add the #269 cite to the header divergence list and the state.x
at-site note (previously described the limitation but cited no task).