Surfaced via examples/lisp, which had to work around the following in
source. Each lowering now matches cstage on the same shape.
- cgassign / cgdot: two-level field through a non-pointer sub-struct.
`(*L).cur.kind = k` (cur a struct-by-value field of L) silently
dropped the store; the corresponding read fell into the SB-symbol
fallback and the linker reported `undefined reference to kind`. The
two new branches resolve outer-field offset + inner-field offset
and emit a single direct store/load at the combined slot, both for
T-by-value and *T-base shapes.
- cgdot: `xs[i].field` chains the trailing field load through the
N_INDEX result for [N]T / []T / *T element-of-struct-ptr. The
cgforrange loop variable now carries the elem tnode so the same
fast path covers `for (let x .. xs) { x.field }`.
- cgindex / cgassign: top-level `[N]T` array and `*T` pointer used
as an index base. cgindex now emits LEAQ name(SB) (array) or
MOVQ name(SB) (pointer) with the correct element scaling; without
this the fallback emitted neither base and walked off the saved
BP slot. Adds letvartnode() helper, an N_TARRAY branch to
letemitsize so the array shows up in c.lets, and an N_TARRAY
initialiser path in emitletdataw that lays the literal bytes into
DATAW.
- cglet / scanlocals: infer the local's tnode for an unannotated
`let x = f()` / `let x = f()?`. inferletcalltype() reads the
callee's declared return; `?` and `!` strip to the success variant
so a tagged-union let allocates the full 24B slot and the
struct-field dispatch in cgdot/cgassign sees the right type.
letslotsize now defers to slotsize on the inferred type.
- slotsize: follow type aliases for tagged-union variants. With
`type parserr = !str;`, the variant slot was 8B instead of the
required 16B; the tagged let stomped on the next slot at the
AX/DX/CX spill.
- cgreturn: tagged-union return forwarding. `return f();` where f
also returns a tagged union now passes the (tag, payload1,
payload2) triple through unchanged instead of re-wrapping it.
- cgreturn / cglet / taggedvariantindex: dispatch by variant name
with module-qualified-vs-bare matching, and recognise N_STRUCTLIT
as the variant tag for `return eof{};`. cgexpr default emits
`MOVQ $0, AX` so the surrounding return shuffle isn't left with
a stale AX.
- isstrtype / nodeisstr: resolve through `!T` aliases. `parserr =
!str` was not propagating the str-shape to the rhs check and the
MOVQ BX,CX shuffle was being dropped from str-typed local
returns.
- exprfloatkind: recognise `p.field` as f64/f32 when the struct
field is so declared, so `v.fval: i64` lowers to CVTTSD2SI on X0.
- cgassign: str field on a direct struct local writes both halves.
`L.src = s;` previously dropped s.len.
- cgcall: pop into the int reg window only up to 6 (DI..R9); rest
stays on the stack and the caller emits ADDQ to clean up.
cgfnparams accepts >6-arg signatures by registering the overflow
params at positive BP offsets (16+8*k(BP)), no spill instruction
emitted.
All 26 harness tests pass; bootstrap reaches a byte-stable fixed
point at ww3 == ww4.
Lexer: `lexnum` now parses the digit/exponent tail into an f64 via a
new `parsef64` (decimal-only, integer-arith driver + pow-10 multiply,
no strtod). The IEEE bits are also stashed in tok.uval via pointer
reinterpret so cgen consumers stay integer-only.
Parser: TK_FLOAT → N_FLOATLIT, carrying both fval and uval. Parser
state grows curfval to plumb the lexer's f64 through refill.
cgen:
- cgfloatlit reads n.uval and materialises X0 via the standard
MOVQ-PUSHQ-MOVSD-ADDQ trampoline.
- cglet, cgident, cgassign learn float-typed branches: MOVSS/MOVSD
for locals; LEAQ-indirect MOVSS/MOVSD for globals.
- cgbin handles ADDSD/SUBSD/MULSD/DIVSD (+ SS variants) and
UCOMISD/UCOMISS-based comparisons. cgun handles float negate
via the `0 - X0` shape C cgen uses.
- cgcast routes int↔float and f32↔f64 through CVTSI2SD/CVTTSD2SI/
CVTSD2SS/CVTSS2SD and their SS twins.
- cgcall + pushargsrev push float args via SUBQ+MOVSD and pop into
the X0..X7 stream, tracked by a per-class counter alongside the
int DI..R9 stream. cgfnparams loads float params from the same
stream.
- emitletdataw bakes FLOATLIT init bits into DATAW (4B for f32,
8B for f64).
Tests: smoke programs (literal init, reassign, arithmetic, fn args/
returns, casts) produce byte-identical asm through `w6c` and
`wwdump_ww -c`, and the resulting binary exits with the same value
whether compiled by the C or wwstage toolchain. Full `make test` is
26/26 and `make bootstrap` still reaches its byte-identical
ww2==ww3==ww4 fixed point.
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.
Bring the wwstage toolchain to parity with C-side DATAW / DATAR /
.data / .rela.data support. With this, w6c_ww + w6a_ww + w6l_ww can
compile, assemble and link `let g: str = "lit";` (and the scalar /
str / slice / struct globals that landed earlier) end-to-end, with
output that's byte-identical to the C-side pipeline.
w6a (types.ww / parse.ww / asm.ww / obj.ww):
- A_DATAW + A_DATAR opcodes; parser learns `name+disp(SB)`;
A_DATAR records an R_X86_64_64 reloc in .data via the new
addrelocdata helper; areloc gains a `section` flag and asym
an `isdata` flag; obj.ww splits relocs into .rela.text /
.rela.data, emits .data PROGBITS + .rela.data conditionally,
and shuffles section indices the same way cmd/w6a/obj.c does
so byte output stays identical when no DATAW/DATAR are used.
w6l (sym.ww / obj.ww / pass.ww / out.ww / dynout.ww / main.ww):
- lrel grows `section`; lsym grows `indata`; lobj tracks
dataoff / datasize; lnk grows combined .data buffer;
- obj.ww loads .data and .rela.data, registers data symbols
with indata=1 and val shifted by the input's data_off, and
the archive scanner includes both .text and .data globals;
- pass.ww adds R_X86_64_64 (patch 8 bytes in .text or .data
with sym_va + addend); relocate's signature becomes
(textva, datava);
- out.ww emits a second PT_LOAD (R+W) when datalen > 0, with
.data at the page-aligned offset after .text;
- dynout.ww refuses .data + -l/-L cleanly (matches the C-side
error message);
- main.ww computes text_va / data_va and passes both to
relocate.
wcc cgen (cgen.ww / cgendecl.ww):
- letpreintern walks top-level str-lets and interns the strlit
BEFORE emitdatasection emits its DATA row, so emitletdataw
can later look up the same label;
- emitletdataw's 16B branch detects non-empty strlit init and
emits the 8-zero + 8-LE-len DATAW plus a DATAR slot+0,strlit
reloc, mirroring cmd/w6c/cgen.c.
Verified: `wwdump_ww -c` byte-matches `w6c` on a `let g: str =
"hello world\n";` fixture; `w6a_ww` and `w6l_ww` produce a
binary byte-identical to the C-side pipeline that runs and
prints "hello world". Bootstrap fixed-point holds (ww2 == ww3 ==
ww4), 26/26 tests green.
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.
Cascades the four enum kinds through every signature and local that
holds one of their values, then removes the type_assignable /
unify_arith relaxation that previously let bare i32 mix with the
named enum types.
Signature updates:
- kwlookup() now returns `tkind` (not i32); tokname() takes `tkind`
- accepttok / expecttok / bprec / isassignop take `tkind`
- parsearglist's closekind is `tkind`
- newtype / prim take `tykind`; scopedefine takes `skind`
- newnode / nkname take `nkind`
Struct fields:
- tok.kind is `tkind`; parser.curkind is `tkind`
- node.kind is `nkind`; node.op is `tkind`
- tinfo.kind is `tykind`; sym.skind is `skind`
Locals holding kinds across lex/parse/check/cgen are now typed with
their enum, including sentinel patterns like `let lkind: nkind =
nkind.N_NONE; if (...) lkind = tn.kind;`.
The selfhost cgen had a load-width bug exposed by this: fieldsize()
fell back to 8 bytes for any TNAME that wasn't a struct or primitive.
For a tkind-typed field that gave `MOVQ (BX), AX` instead of `MOVL`,
diverging from the C cgen on tok.kind / parser.curkind / etc. Two
fixes:
- fieldsize now consults the enum registry and returns the storage
type's size (4 for `enum i32`)
- collectenums runs before collectstructs in cgfile so the registry
is populated when registerstruct asks for field sizes
All 22 tests stay green; 990/993/995 byte-identity probes pass with
the strict typing in place.
`type tkind = enum i32 { TK_NONE = 0, TK_EOF = 1, ... TK_LAST = 86 }`
replaces the 87-line `def TK_*: i32 = N` cluster in lib/ww/lex/tok.ww.
Numeric values explicit so 990_selfhost's byte-diff against the C-side
`Tkind` enum still passes.
All ~270 reference sites in lib/ww and selfhost/cmd/{wcc,wwdump}
sed-renamed `TK_X` → `tkind.TK_X`. Struct fields (`tok.kind`,
`parser.curkind`) intentionally kept as `i32` — making them `tkind`
shifted some byte-positions in the cgen output and broke 990/993/995
byte-identity probes without an obvious win.
To make the rename non-cascading on every signature, type_assignable
and unify_arith in cmd/wcc/check+type relax to allow enum ↔ int
mixing when storage matches (a `tkind` value flows into an `i32`
slot and vice versa, no explicit cast). This deviates from Hare's
strict enum semantics; doc'd as an explicit pragmatic relaxation
for the compiler's internal enum-shaped kinds. External user code
can still get the type-safety benefit if they declare their
parameters with the enum type.
combined.ww files regenerated by ww build.
Driver-side concatenation flattens module names, but enum member
lookup keyed off the exact lhs ident — so `whence.CUR` worked while
`os.whence.CUR` fell through to w6l with `undefined main.whence`.
C side: fold TY_ENUM members in the post-cexpr cascade too, not
just the early SK_TYPE shortcut. The recursive cexpr lands the
inner N_DOT(os, whence) on the named enum type; the outer access
then folds normally.
Selfhost: cgdot now treats `N_IDENT.MEMBER` and `N_DOT.MEMBER` the
same way, keying off the leaf name. enumlookup strips a trailing
`.`-prefix from the lookup key.
Adds e2e test 700: `use os; os.whence.CUR as i32 == 1`.
w6c_ww now compiles enum end-to-end and emits byte-identical
asm to the C w6c on the new 994 corpus case (`type mode = enum u8
{ R, W, RW = R | W }; main() { return (mode.RW): i32 }`). Mechanism
mirrors the C side:
- collectenums walks every `type X = enum {...}` at file scope and
pre-resolves each member's u64 value (auto-increment from prior,
sibling-ref folding for `RDWR = READ | WRITE`).
- cgdot recognises `EnumName.MEMBER` before the local lookup and
emits MOVQ $value, AX directly.
- cgtypeassert short-circuits when either side is enum: cgexpr on
the LHS lands the value in AX with the right integer width; no
tag/unwrap.
main.combined.ww (wwdump/ + w6c/) regenerated by ww build.
C cgen has carried defer for a while (defers[] global + reverse
walk on every return). Selfhost cgen now mirrors:
- cgen struct: deferbuf (**node, LIFO stack) + defertop counter.
- cgstmt N_DEFER: push n.lhs.
- cgreturn: rundefers() at entry — same as the C cgen pattern.
- cgfn fall-through return: rundefers() before zero-AX+RET.
DEFER_MAX = 16 matches C cgen.
Two new e2e rows: defer with an explicit `return acc;` (321 mod 256
= 65), and defer firing on an implicit void-fn fall-through (87).
Both rows verified via the wwstage cgen too.
Defer's semantics: queued exprs fire LIFO before the return expr
is evaluated, so a return that reads memory mutated by a deferred
call sees the post-defer state. Matches C cgen and Hare.
`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.
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.