cgexpr leaves float results in X0, not AX, but the struct-field paths
emitted MOVQ AX,off(BX) and MOVQ off(BX),AX — so every store wrote
garbage and every load read garbage, except by accidental register
survival across an unrelated call. examples/lisp only worked because
parsef's X0 happened to live across the broken MOVQ shuffle into
vfloat; any inserted f64 op between them would silently corrupt.
Wire MOVSD/MOVSS X0,… (and the matching loads) into eight field
sites on both compilers: alloc(T{...}), p.x = v through local/ptr/
global, chained r.sub.x = v, *p = v for *f64, let v: T = T{...},
base.x reads, *T.x reads, and chained a.b.c.x reads.
83/83 lisp_test probes still pass; bootstrap reaches a byte-stable
fixed point at ww3 == ww4.
Three gaps in the wwstage cgen relative to C w6c, plus a matching
C-side bug surfaced along the way.
cgmatch (selfhost) handles non-ident scrutinees: `match (foo())` now
spills the AX:DX:CX return triple into a 24B `@match_spill` slot
rather than reading garbage off BP+0. scanlocals counts the slot so
the prologue SUBQ stays in sync. For N_CALL we recover the return
type via fnretlookup so nullable dispatch picks the pointer-vs-null
discriminator. Mirrors @match_spill in cmd/w6c/cgen.c N_MATCH.
cgcall (selfhost) special-cases `alloc(structlit{...})`: lower to
rt_alloc(totsize) + per-field MOV* at the struct's field offsets,
mirroring cmd/w6c/cgen.c's existing path. Previously the structlit
fell into pushargsrev and produced wrong code.
check.ww's N_MCASE branch now pushes a fresh scope around each arm
body. Without this, `case let e: str` inside a fn with an outer
`let e: *T` collided with scopedefine's same-scope dedup, the inner
binding silently dropped, and references to `e` inside the arm
resolved through the outer type.
Both cgens save/restore the locals head around case bodies so arm
binds (and nested arm-body lets) don't leak past the arm — code
after the match resolves names back through the outer scope.
w6c gains `local_alloc`: same as `localoff` minus the dedup. N_MATCH
case-bind allocation switches to it. Previously `let e: *T` (8B)
shadowed by `case let e: str` (16B) reused the outer 8B slot and the
inner str.len store overflowed into the saved BP, segfaulting on
return.
Tests 26/26.
N_IDENT for a slice local now loads (AX=ptr, BX=len, CX=cap), matching
the existing global-slice load.
cgexpr learns N_SLICE: `base[lo:hi]` leaves the same triple in
registers, so callers (return, arg push, reassignment) all share
one shape. The let-init's pre-existing N_SLICE direct-store path
stays as a specialisation; the new generic slice let-init catches
fn-returning-slice and slice-ident initialisers.
N_ASSIGN gains a TY_SLICE branch parallel to TY_STR: store all
three halves to the local slot or, for globals, stash CX into DI
before LEAQ-ing the address (CX is both the new cap and the
address scratch).
Field write extends the existing TY_TAGGED branch with an is_global
arm: LEAQ name(SB),CX after cgexpr (no AX/BX clobber), then MOVQ
into slot+foff+0 (tag) and slot+foff+8 (value, plus +16 for str-
typed variants).
Field read now treats tagged fields specially — load AX=tag,
DX=val0, CX=val1 (when union >16B), mirroring the tagged-return
ABI that let-init and match dispatch already expect. Previously
the scalar-load path read 8B into AX and left DX/CX with junk,
which silently broke local tagged-field reads too.
f32 → 4B slot, f64 → 8B. C cgen bakes the FLOATLIT bit pattern into
DATAW directly; selfhost emits zero-init only (its parser doesn't
lex N_FLOATLIT yet). Read/write goes LEAQ name(SB),CX + MOVSS/MOVSD
since w6a has no D_EXTERN operand form for SSE moves.
Use the new DATAR mechanism so str-literal init on a top-level
mutable `let` lands in .data and links cleanly.
emit_lets, when it sees `let s: str = "lit"` (non-empty strlit),
emits:
DATAW s(SB),"<8 zero placeholder><8 LE bytes of len>"
DATAR s+0(SB),<strlit_label>(SB)
The linker patches the placeholder with the strlit's runtime VA at
program load time, so `s.ptr` reads as the real pointer and `s.len`
as the literal length. A new let_pre_intern pass scans top-level
lets ahead of emit_data so the strlit gets a DATA row in the same
.s file; running emit_lets after emit_data instead would have
flipped the (DATA strlits, DATAW lets) section order in the .s and
broken byte-identity with the wwstage cgen.
The wwstage cgen still emits the zero-init shape for str lets,
which only matters if the wwstage is asked to compile source that
uses str-literal init. None of the selfhost combined sources do
that today, so test 994 / 990 stay green. The selfhost mirror for
DATAR + DATAW + this w6c branch is a follow-up.
630_let_global gains two fixtures: a length-readback and a first-
byte readback through the patched ptr.
Unblock literal initialisers for str/slice/struct globals by wiring
an R_X86_64_64 relocation kind through both assembler and static
linker.
w6a:
- new A_DATAR directive, syntax `DATAR slot+off(SB),target(SB)`,
records an R_X86_64_64 reloc at slot+off in .data pointing at
target. The slot must be pre-defined by a prior DATAW;
- parse_operand learned the `name+disp(SB)` shape so the slot's
byte offset can be addressed explicitly;
- Areloc carries a `section` flag (0=.text / 1=.data) and obj.c
splits the reloc list into .rela.text and .rela.data, emitting
the latter conditionally with sh_info pointing at .data.
w6l:
- Lrel grows the same `section` flag; obj.c loads `.rela.data`
sections into the global reloc list with offsets shifted by
each input's data_off;
- pass.c handles R_X86_64_64: target VA is data_va+sym.val for
in_data symbols (else text_va+sym.val), addend is added, and
the 8-byte slot is patched in l->data (or l->text).
Inputs without DATAR are unaffected — bootstrap, 991 (selfhost .o
diff) and 992 (selfhost exe diff) keep their byte-identical
output. 520_datar covers the new path: asm a DATAW+DATAR pair,
verify .rela.data has exactly one R_X86_64_64 entry, link, run,
confirm the relocated pointer feeds a 5-byte write that prints
"hello".
Selfhost mirror + w6c emission for str/slice/struct literal init
land in follow-ups.
Extend top-level mutable `let` to cover structs. Same approach as
str / slice: take the field-access base through &name(SB) instead
of off(BP).
- emit_lets / emitletdataw: emit `sizeof(T)` zero bytes for any
struct global without a baked-in initialiser. Struct-literal
init is skipped → undefined symbol at link if used;
- cgdot read path: when the IDENT base's local lookup misses and
the name is a struct let, LEAQ name(SB), CX and load the field
at fi.foff(CX) with the width-aware op (MOVQ / MOVL /
MOVZBQ / MOVSXD; MOVQ pair for str fields);
- cgassign write path: parallel handling for plain `=` (incl. str
fields) and the compound ops (+=, -=) via load → push → eval
rhs → combine → store with a re-LEAQ between cgexpr clobbers.
Tagged-union fields on struct globals are unsupported in v1 — the
local path's tagged branch isn't generalised yet. Whole-struct
by-value flow through expressions remains NYI (matches the local
status). 630_let_global gains 3 fixtures (read/write, compound +=,
narrow u8 field); selfhost mirror keeps test 990 / 994 / 995 byte
identical; bootstrap fixed point holds.
Extend top-level mutable `let` to cover slices. Same shape as the
str work, with one more 8-byte field and the address holder CX
overwritten by the cap as the last load step:
- emit_lets / emitletdataw: 24-byte zero DATAW for `let v: []u8;`
(and the trivial `nil` init); no slice-literal syntax exists
so the no-init path is the only supported shape;
- cgident: LEAQ name(SB), CX → MOVQ (CX), AX → MOVQ 8(CX), BX →
MOVQ 16(CX), CX, so the slice ABI triple lands in (AX, BX, CX);
- cgdot: .cap delta 16 wired alongside .ptr / .len through the
same &name(SB) base.
Slice reassignment (`v = some_slice;`) is still unsupported — slice
values don't yet flow as a full (AX, BX, CX) triple through general
expressions even for locals — so reads/`&` are the supported surface
today. Manual fill through `(&v): *u64` continues to work.
Tests 630 (10/10), 990, 994, 995 stay green; bootstrap fixed point
holds.
Extend top-level mutable `let` to cover str. The cgen now:
- emits a 16-byte zero DATAW for `let s: str;` (and the trivial
`nil` / `""` inits); a non-empty strlit init is skipped because
a compile-time .data → .text reloc isn't supported yet, so the
user gets a clean undefined-symbol error at link;
- loads `s` as `(LEAQ s(SB), CX; MOVQ (CX), AX; MOVQ 8(CX), BX)`
so the (AX=ptr, BX=len) pair convention is preserved;
- stores via the same `&s` indirection for `s = expr;` and routes
the `.ptr` / `.len` pseudo-field N_DOT branch through it; and
- tracks the declared type on each LetVar so cgident / cgdot /
cgassign pick the right load/store shape.
Selfhost cgen mirrors all four paths byte-for-byte; test 990
(cgen-match on err.ww) and tests 994/995 (self-rebuild) stay
green. 630_let_global gains two new fixtures (`let msg: str;` +
runtime assign, plus reassign from a helper).
Slice and struct globals still NYI — same scope deferred.
Third step toward writable globals. The C cgen and its selfhost
mirror now:
- emit DATAW <name>(SB),"<8 LE bytes>" for every top-level `let`
whose type lands in the scalar set (i8..i64/u8..u64/bool/rune/
int/uint/uintptr/ptr; floats and multi-word types deferred);
- drop the "no writable .data" silent-drop guard at the N_IDENT
store path, replacing it with a RIP-relative MOVQ for `=` and
a load→combine→store sequence for the compound ops; and
- route `&name` through LEAQ name(SB) instead of dropping it.
Type aliases resolve via aliaslookup so `type counter = i32; let c:
counter = 0;` still emits a DATAW slot. Non-literal initialisers
silently skip, which surfaces as a clean undefined-symbol error if
the binding is ever referenced.
The selfhost mirror lands in the same commit because test 990
diffs the C cgen against wwdump_ww -c on err.ww (which has
top-level `let nerrors: i32 = 0; ... nerrors += 1;`). Any drift
between the two cgens makes 990 fail. Bootstrap stays at a fixed
point: ww2 == ww3 == ww4 byte-identical.
First step toward top-level mutable `let`. Adds a sibling directive to
DATA whose bytes land in a separate writable .data PROGBITS section
(SHF_ALLOC|SHF_WRITE, STT_OBJECT) instead of .text. The section is
emitted only when DATAW was used, so inputs without it produce a
byte-identical .o — tests 991 (selfhost .o diff) and 995 (self-rebuild)
keep passing unchanged.
w6l still treats data-resident syms as undefined; that's the next step.
`type Foo = enum [intT] { NAME [= expr], ... };`. Storage defaults
to i32; members auto-increment from 0 (or last+1) when `= expr` is
omitted, and value expressions can reference earlier siblings —
enough surface for io::mode-style flag enums (`RDWR = READ | WRITE`).
`Foo.MEMBER` folds to an N_INTLIT in the checker, typed as the
named enum. Binops on enum values yield the same enum (type_eq on
the named pointer), so `mode.R | mode.W` is a `mode`. Enum ↔ int
is a reinterpret-only `as` cast — same register, no tag wrap — so
`mode.RDWR as i32` and `1 as mode` both work without runtime ops.
`is`/`?`/`!` are still tagged-union-only. CSP runtime (chan/proc)
is unchanged; only the type-system slot is touched here.
`match (e) { ... }` can now sit in expression position, with each
arm using `yield expr;` to produce the match's value:
let v = match (r) {
case let n: i32 => yield n + 1;
case let s: str => yield s.len: i32 + 100;
};
TK_YIELD keyword + N_YIELD AST node, both appended at the tail of
their enums to keep prior numeric values byte-stable for the
wwdump-diff gates.
Checker: cexpr for N_MATCH walks each arm's body looking for the
first N_YIELD; the match's type is the unified yield type (or
ty_void if no yield, preserving the statement-form semantics).
Mismatched arm yields are flagged.
Cgen: a yield-target stack (separate from the loop break stack)
holds each enclosing match's end label. N_YIELD evaluates its
expression into AX (and BX for str) and JMPs to the topmost entry.
cgmatch pushes its end label on entry and pops on exit.
Selfhost mirror: lib/ww/lex/tok.ww kwtab+name, lib/ww/ast.ww
N_YIELD def+print, lib/ww/parse/stmt.ww yield-stmt; selfhost cgen
adds a yieldbuf to the cgen struct and a cgyield helper. Verified
end-to-end: a yield-using program compiled via the wwstage cgen
matches the C-cgen build's exit code.
A tagged union with exactly one `*T` variant and one `void` variant
collapses to a single 8-byte pointer slot, where the null bit
pattern is the void variant and any non-null is the *T variant.
Mirrors Hare's `(*T | null)` ABI optimisation.
Detected in resolve_type when the post-flatten variant list has
exactly two entries of the right shape; Type.nullable = 1 and
size = 8. Codegen branches every tagged-handling site on the flag:
- match: discriminator = pointer-vs-zero, not slot+0 tag word.
Binding for the *T case copies the same word (the pointer itself)
rather than slot+8.
- is/as: same ptr-vs-zero discriminator.
- ?: null = error (propagate AX=0 to caller's matching null
encoding); non-null = success (AX is already the pointer).
- !: null aborts; non-null falls through with AX = pointer.
- let-init / return: spill or set just AX (no tag/value pair).
- call-arg push: push only AX, not the now-unused DX/CX.
Prologue spill already pulled size/8 = 1 arg register via the
existing tagged-arg loop, so no change needed there.
Two existing helpers in cgen.c get nullable-aware spelling:
type_isnullable() and nullable_ptr_tag() (which variant index is
the *T side; the void side is the other one).
The Hare-style `(*T | null)` spelling isn't supported — `null` is
not a type keyword in ww. Callers use `void` instead, which is
already a real type. The result is the same bit-level layout.
A type prefixed with `!` is flagged as an error variant. When any
variant in a tagged union carries the flag, `?` propagation uses
those (and only those) as the error subset; the unflagged variant
is the success type. The legacy "first variant = success" rule still
applies when no `!`-flag is present, so existing code keeps working.
- TK_NOT in parsetype → N_TBANG wrapper (lhs = inner type expr).
Appended to Nkind tail for wwdump-diff byte stability.
- resolve_type N_TBANG: wraps primitives in a fresh Type copy so the
iserror bit doesn't taint shared globals like ty_str/ty_i32; flips
the bit in place on NAMED (already unique per alias decl).
- Type.iserror; type_named and typedecl inherit it from under.
- New check.c helpers: tagged_has_errflag, tagged_is_error_variant,
tagged_success_type. N_TRYPROP uses them to find the error subset
and verify each error variant is propagatable to the enclosing
return.
- cgen mirrors with cg_tagged_success_tag + cg_variant_is_error.
`?` compares AX against the success tag (no longer always 0) and
remaps each error variant's tag for the enclosing fn. `!` aborts
on any non-success tag.
strconv.invalid and strconv.overflow now use `!`-flagged shape
(`!i32` and `!void`) — visible signal in the API surface that they
are error types, matching Hare. The (i64 | invalid | overflow)
return shape and behavior are unchanged for callers; their match
arms still bind the same way.
Selfhost: lib/ww/parse/parse.ww recognises `!T` and emits N_TBANG.
The selfhost typechecker and cgen ignore the flag — none of the
selfhost sources use `!`, so byte-identity gates are unaffected.
The selfhost mirror catches up when there's a source using it.
`type invalid = i32` (payload: byte index of first bad rune; mirrors
Hare's strconv::invalid = !size) and `type overflow = void` (Hare's
overflow = !void). stoi64/stou64 now return these instead of the
str-error placeholder. atoi64 dropped — lib/CLAUDE.md says graduate
in one go, don't keep both shapes around.
To produce the void variant payload, `void` is now a real
expression literal (TK_VOID kw, N_VOIDLIT). It evaluates to ty_void;
codegen emits MOVQ $0, AX. Both kinds are appended at the tail of
their enums to keep prior numeric values byte-stable for the
wwdump-diff fixtures.
check_file reorder: USE declarations are now installed in pass 1
alongside the type-decl placeholders so dotted type references
(`strconv.invalid` from a typedecl body) resolve. DEF/FN/LET silently
overwrite a USE-occupied slot — matches the old behavior where USE
silently no-op'd when a same-name fn/def existed (the conflict
manifested in selfhost main.combined.ww at `use parse;` colliding
with `export fn parse(a)`).
selfhost mirror: lib/ww/lex/tok.ww kwtab+name; lib/ww/ast.ww
N_VOIDLIT def+print; lib/ww/parse/{expr,parse}.ww TK_VOID handling;
selfhost/cmd/wcc/cgenexpr.ww N_VOIDLIT codegen.
Replaces the -1 sentinel return on indexbyte/byteindex/rbyteindex/
index with Hare's optional-shaped tagged union. Callers `match` on
the result and bind the index from the i32 variant.
Two cgen fixes were needed first:
1. resolve_type for N_TTAGGED rounded value payload up to an 8-byte
multiple. (i32 | void) was sized 12 — tag (8) + payload (4) —
which made the reg-passing ABI compute size/8 = 1 word and drop
the value word.
2. The call-arg push path special-cased struct and slice args but
not tagged-return calls. A nested `f(g())` where g returns a
tagged union pushed only AX (tag); the matching pop loaded a
stale DX/SI for the value. Now pushes AX/DX[/CX] in order so
the pop side drains tag → arg-reg[0], value(s) → arg-reg[1..].
strings.contains rewritten to match on the new tagged result. No
other callers existed in lib/ — bufio/io still use their own
shapes.
`expr?` previously did a brain-dead RET through whatever AX/DX/CX
held — only safe when operand and enclosing fn had identical variant
ordering. Tests relied on that alignment by construction.
Now:
- Typecheck: each non-first variant of operand must appear as a
variant of the enclosing fn's return tagged union. Enclosing must
itself be tagged (a non-tagged return has no slot for errors to
land in).
- Cgen: on tag != 0, walk operand's error variants and emit a
conditional tag remap (cmp/jne/mov/jmp) for any whose index in
enclosing differs from operand's. Identity cases emit nothing,
so same-shape operands cost zero extra instructions.
Selfhost cgen doesn't implement N_TRYPROP at all (no selfhost source
uses `?`); byte-identity tests still pass.
One existing e2e row used `?` with main returning i32 — relied on
the old loose semantics. Switched to `!` (abort-on-error); it was
exercising success-unwrap, not propagation.
`e is T` returns bool (variant tag == T's index); `e as T` unwraps
to T or exit(1) on mismatch. Postfix, same precedence as `:` cast.
TK_IS / N_TYPETEST / N_TYPEASSERT appended at the tail of their
enums so every prior numeric value stays unchanged — the
990_selfhost wwdump-diff stays byte-clean.
Cgen mirrors the match-case slot-based load (tag at +0, value at
+8/+16), so an N_IDENT tagged-union local works just like a
match scrutinee. Selfhost cgen inlines the slot resolution
because the wwstage cgen drops sign bits on `*i32` output
parameters in this position.
Renames `errors.is` -> `errors.equal` (the only naming collision;
the existing comment already noted it shared shape with
strings.equal/bytes.equal).
Plan 9-style w-prefix on the per-arch tools, disambiguating from the
real Plan 9 6c/6a/6l in ref/plan9front/:
cmd/wwc/ → cmd/wcc/ libwwc.a → libwcc.a
cmd/6{c,a,l} → cmd/w6{c,a,l} binary names too
test/wwc/ → test/wcc/ 6 test files w/ w6 prefix
selfhost/cmd mirror in lockstep
bootstrap/amd64/{w6c,w6a,w6l} snapshot binaries (gitignored)
WW_6{C,A,L} → WW_W6{C,A,L} env-var overrides
Plan 9 source-tree refs ("Plan 9 6c shape", ref/plan9front/, etc.)
preserved. Hare-style driver, both C and ww sides:
ww test [path] discover *_test.ww in a directory module, run
each; single-file mode for `ww test foo.ww`
Module-by-name `ww build foo` resolves to foo.ww or foo/foo.ww
via search path (cwd : -I dirs : $WW_LIB)
Default-to-cwd `ww build` / `ww test` build the cwd module
Run pass-through `ww run path arg1 arg2` reaches the program
lib/os: getcwd (79) and getdents64 (217) syscalls power `.` resolution
and directory enumeration on the ww side.
Makefile: wwstage tool deps now include lib/os/os.ww (+ lib/strconv
for wwdump_ww) so lib/* edits force their rebuild instead of leaving
stale binaries — surfaced when test 995 first failed against a stale
w6c_ww built before the lib/os additions.
Test 993 byte-identical parity gate (C-side ww vs ww-side ww_ww on a
build corpus) stays green; all 19 tests pass.