Commit Graph

53 Commits

Author SHA1 Message Date
4ab530c24d rt+lib/os+test: capture envp; add os.getenv
Capture envp from the kernel-supplied stack into a DATAW slot during
_start's prologue (before CALL main), and expose it via a `rt_envp`
TEXT getter. lib/os.getenv binds the getter as `@symbol("rt_envp")
fn rtenvp() **u8` — the getter-fn pattern works around @symbol-on-let
not being supported by the compiler yet (silent miscompile otherwise).

`os.getenv(name: str) (str | void)` matches Hare's os::getenv surface:
walks the NUL-terminated envp table, "name=" prefix-matches with an
explicit `=` boundary check so prefixes don't false-match longer
names, returns the value as a borrowed str view. Empty value (env
"FOO=") returns len=0 str, not void — void is reserved for "name
not present at all".

Cohort coverage in lib/os/ostest.ww + test/wcc/974_getenv_run.c:
set / empty / unset / prefix-no-match (4 @test fns).
2026-05-15 17:13:32 +09:00
f2643a846a cstage+selfhost+test: cgen N_ASSIGN whole-STRUCT (call+structlit, 5 sites)
Receive side of #4's cgreturn ABI (aee8149) for TY_STRUCT lvalues of
size <=24B. Producer materialises rhs into AX=bytes[0..7], DX=[8..15],
CX=[16..23], zero-padded to 24B; receive sites here read the regs and
write only `declared sz` bytes — MOVQ for full 8B chunks plus a sized
tail (MOVL/MOVW/MOVB) by the *declared* struct size. ASYMMETRY: do NOT
mirror the sender's three uniform MOVQs, else trailing 1..7B chunks
overrun the next local slot. Tail chunks in {3,5,6,7} are unreachable
under WW struct align rules (size%align==0) and fall through.

Five sites wired in each stage (cstage cgen.c, wwstage cgenexpr.ww +
cgenstmt.ww), call-result + structlit rhs at each:
  - N_LET   `let s: T = bar()` / `= T{...}`           cgenstmt cglet
  - N_ASSIGN N_IDENT-lhs   `s = bar()` / `= T{...}`   cgenexpr cgassign
  - N_ASSIGN single-DOT local-base   `o.f = ...`
  - N_ASSIGN single-DOT ptr-base auto-deref   `p.f = ...`
  - N_ASSIGN single-DOT global-base   `g.f = ...`
  - N_ASSIGN chained-DOT depth>=2   `o.m.in = ...`
(The four dot-flavors share one shape pattern, hence "5 sites".) Where
the dst addr needs scratch (ptr-base/global-base/via_cx), it is loaded
into BX after the call so CX stays as the third value word; for
structlit field-walks BX is reloaded before each store since cgexpr
clobbers AX/BX between fields.

wwstage needed a new `structnaturalsize(si)` helper (cgenutil.ww):
si.totsize is mis-named — it's slot-padded to 8 by registerstruct for
stack-slot use, while the receive ABI wants the type's natural size
(max(foff+fsz)). Splitting si.totsize into naturalsize + slotsize is
tracked as the wwstage struct sizing follow-up (task #15); until that
lands, the helper recovers the natural size at receive sites.

Test 701_cgassign_struct.c (18 rows, 3 checks each — cstage value,
wwstage value, asm byte-identity), wired in Makefile after 698. The
headline ASYMMETRY case is the 20B `{i32×5}` row: sender pads to 24B
via three MOVQs, receiver writes MOVQ AX +0, MOVQ DX +8, MOVL CX +16.
A regression to a MOVQ tail there overruns 4B past the slot and
flips the exit-code check.

smoke.combined.ww is the auto-regen ride-along of strings.freeall
landing in 714d089 (worker-shlex).

Pre-existing gaps surfaced and tracked separately (not fixed here,
out of scope):
  - task #16: silent drop of `(*p).f = ...` explicit-deref dot lhs.
  - task #17: silent zero of nested STRUCTLIT field in N_LET / N_ASSIGN
    initializer — the field_chain and field_global test rows use
    explicit field writes (`o.m.t = 10i64;`) rather than nested
    literals as a fixture-level workaround.
  - task #9: module-name-mangle for fn labels avoided in the
    field_global_call fixture by `let g: outer;` (no init).

make test: 59/59. 994_w6c_ww + 995_self_rebuild PASS — bootstrap
byte-identity is the load-bearing proof for this commit's scope.
2026-05-15 16:43:07 +09:00
714d089e31 lib+test: add shlex (POSIX split/quote) + strings.freeall
Surface mirrors ref/hare/shlex/{split,escape}.ha:

  shlex.syntaxerr            !void
  shlex.strerror(syntaxerr)  str
  shlex.split(str)           ([]str | syntaxerr)
  shlex.quote(*io.stream, s) (i32 | io.closed)
  shlex.quotestr(s) str

strings.freeall([]str) added as the natural disposer (placed next
to strings.dup, the natural creator). Skips empty {nil,0} elements
and the header free when cap==0.

POSIX rules:
- whitespace separators ' '/'\t'/'\n' (collapse runs).
- single-quote: literal until closing "'" (no escapes inside).
- double-quote: '\<c>' processed inside, any <c> (Hare-faithful;
  more permissive than POSIX strict). Unterminated → syntaxerr.
- outside quotes: '\<c>' → literal <c>; '\<newline>' deleted
  (line continuation); trailing bare '\' → syntaxerr.
- "" / '' preserve a literal empty-string token (dirty flag).

Divergences from Hare (all documented in shlex.ww header):
- drop nomem (os.alloc aborts on OOM, same precedent as
  strings.dup, getopt.appendoption).
- byte-wise cursor instead of strings::iterator (no UTF-8 rune
  iteration in the language stack yet; same precedent as fnmatch).
- *io.stream (not io::handle); (i32 | io.closed) (lib/io's
  stream vtable doesn't model wider io::error yet).
- appendstr / dupstr workarounds graduate when task #17
  (cgen mod-mangles fn labels) lands.

Test: 4 @test fns (test_split / test_quote / test_quotestr /
test_strerror), table-driven via check1/check2/check3/checkerr/
checkquote helpers. 12 split rows + 4 quote rows ported verbatim
from ref/hare/shlex/+test.ha; empty-input ([]) and empty-quote
('') edges added per documented behaviour. @test fns prefixed
test_* to avoid the use-shlex flat-concat namespace collision
on bare split / quote / quotestr / strerror names.
2026-05-15 15:48:13 +09:00
218913eb16 build: wire test_use_promote_alias
The 699 test source landed with the SK_USE→SK_X promotion fix in
7f60ebb but Makefile wiring was deferred so it wouldn't collide with
parallel fnmatch + cgreturn WIP. Tree is clear; wiring it now.

make test: 57/57.
2026-05-15 15:21:50 +09:00
aee8149754 cstage+selfhost+test: cgreturn TY_STRUCT <=24B via AX/DX/CX
Whole-struct return ABI for sizes <=24B. Both stages materialise rhs
into a zero-padded 24B @retscr scratch slot, then load AX=bytes[0..7],
DX=bytes[8..15], CX=bytes[16..23] unconditionally — three MOVQs
regardless of declared struct size, so the receive side (landing in
task #5) can read all three words and mask by the declared size. R8
stays reserved for the tagged-return 4th word; the uniform-MOVQ shape
is cheap over a size-conditional partial-load and keeps the producer
diff vs the existing tagged-return AX/DX/CX/R8 path minimal.

Two rhs shapes wired this pass: N_IDENT (word-copy from rhs local slot,
MOVQ pairs + MOVL/MOVB tail bounded by declared struct size) and
N_STRUCTLIT (field-walk; tagged fields delegate to the existing tagged
widening helper, float fields go through X0, int fields use MOVQ/MOVL/
MOVB by field size). Sizes >24B fall through to the existing scalar
path (only AX gets the first qword), pending sret in a future task.
N_CALL chain-return (`return otherfn()`) is deferred to task #5's
receive side — until that lands the call-result lives in caller regs.

The wwstage mirror in cgenstmt.ww matches cgen.c byte-for-byte on the
new branch; cgendecl.ww's scanlocals pre-reserves 24B for @retscr under
the same predicate (N_RETURN, fnret is N_TNAME, structlookup hit,
totsize<=24, rhs is N_IDENT|N_STRUCTLIT) since wwstage writes its
prologue SUBQ from the upfront frame total — cstage patches SUBQ at fn
end so it can allocate inline.

Latent fsz==2 MOVW divergence between stages (cstage structlit int-
branch only special-cases fsz 1/4, wwstage's fieldstoreop also returns
MOVW for fsz==2) tracked as task #13; not exercised by the new fixtures
or by any current selfhost <=24B struct return.

main.combined.ww files also pick up worker-checkfix's wwstage
architectural comment from 7f60ebb (auto-regen ran after that commit).
2026-05-15 15:19:38 +09:00
1d5ff201ee lib+test: add fnmatch over Hare sea-of-stars
Port of ref/hare/fnmatch/fnmatch.ha. Public surface mirrors Hare:
`flag` enum (NONE/PATHNAME/NOESCAPE/PERIOD) and `fnmatch(pattern,
string, flags) bool`.

Algorithm is the three-phase sea-of-stars (also used in musl):
exact-match the prefix before the first `*`, exact-match the tail
after the last `*`, then greedily match each star-delimited middle
segment with backtrack on inner failure. No exponential corner —
each star anchors a "match found" at strictly increasing positions.

Bracket expressions: Hare-strict — `!` for negation, `^` rejected
as invalid; `]` as first member legal, trailing `-` literal, all
12 POSIX classes ([:alnum:] … [:xdigit:]) via direct streq + the
ascii.is* predicates.

Divergences from Hare (documented in fnmatch.ww docblock):
  - byte-indexed cursors in place of strings::iterator (no UTF-8
    rune iter yet); ASCII-only meaningful, multibyte matches
    byte-identically. Graduates "in one go" per lib/CLAUDE.md
    when the language stack grows rune iteration.
  - invalid pattern collapses to `false` at the public boundary
    (Hare's `b is bool && b: bool;`); a try-shaped diagnostic
    entry can be added later without churning the surface.
  - tail-match uses a forward cursor at `string.len - cnt`
    instead of riter/prev — same byte sequence either way.

Test fixture follows the project's helper-per-row table-driven
shape (precedent: lib/encoding/base32/base32_test.ww). 8 @test
fns clustered by feature (basic / brackets / ctype / period /
noescape / musl_basic / pathname / combined), ~95 rows total
adapted from Hare's +test.ha plus musl-derived edge cases.

Wired as 972_fnmatch_run alongside 970_fmt_run / 971_log_run in
the stdlib-runtime band.

Unblocked by 7f60ebb (cstage+wwstage SK_USE→SK_X promotion
missing use_alias), which is what let the module name `fnmatch`
coexist with an exported leaf fn `fnmatch`.
2026-05-15 15:11:59 +09:00
e7f173cde0 lib+test: add log over io.stream sink
Hare-shaped lib/log v1: logger vtable carrying one println slot,
stdlogger forwarding to a *io.stream, plus *logger globals (silent
/ default / global), setlogger, lprintln / println, lfatal / fatal.
Default sink is stderr through a private rt_syscall write callback;
graduates with #17 (cgen mod-mangle for fn labels), at which point
the rt_syscall stub disappears the same way fmt's will.

Module-scope struct/pointer-literal init isn't constexpr in cstage
emit_lets, so silent/default/global are wired lazily by ensureinit
on the first exported-fn entry (lib/temp rnginit pattern). Callers
that read the globals directly must call some lib/log fn first.

Skipped this round: printfln / lprintfln / fatalf / lfatalf and the
matching printfln vtable slot — they need a fmt {n}-placeholder
parser that isn't shipped yet. fatal / lfatal's exit(255) arm has
no test fixture (needs fork+wait for WEXITSTATUS); left as TODO.

971_log_run wraps the @test fixture under `ww run`, mirroring
970_fmt_run. make test: 54/54; bootstrap fixed-point (990-997)
holds.
2026-05-15 14:05:55 +09:00
9d85aa4142 selfhost: structlookup/enumlookup mod-filter (mirror aliaslookup)
Tags structinfo/enumtype with originating module; exact-match first,
then split pkg.X and filter by smod/emod. Without this, two modules
with same-leaf-name struct/enum types collapsed to whichever entry
appeared first in the chain.

Wired into 696_modtype_leaf_collision via a wwstage run_pos using
ww_ww (negative case omitted: w6c_ww has no checkfile pass). Updated
the test's Makefile deps to include the wwstage binaries.

Audited the rest of the lookup family — fnretlookup, fnparamslookup,
deflookup don't need the same treatment: the parser emits N_DOT.str
(call/field name) as the leaf only, and fnparamslookup is only
invoked with N_IDENT.str. Dotted module-qualified function calls go
through the module-mangling path instead.
2026-05-15 14:04:44 +09:00
0ef94eef04 lib+test: fmt rename fprint→fdprint; add io.stream fprint sink 2026-05-15 11:34:47 +09:00
e6045f3ada w6c+selfhost: same-module preference for bare-leaf lookup 2026-05-15 11:24:33 +09:00
66d6408cbe w6c+selfhost: cross-module same-leaf type disambiguation via Sym.mod 2026-05-15 10:26:20 +09:00
e349536f62 test: wire 8 stdlib tests into make test 2026-05-15 09:00:34 +09:00
df74d9c429 selfhost: cgmatch bsz for TY_STRUCT variant bind (closes #31)
Match-arm bind size in wwstage hardcoded str=16, []T=24, else=8 in
both cgmatch (emit) and scanlocals (frame pre-scan). A TY_STRUCT
variant fell into the 8B fallback: only the first quadword reached
the bind, and the prologue SUBQ underbooked the frame so the
emit-time localalloc(bsz=24+) wrote past SP.

Replace the hand-rolled table with slotsize(c, pat) at both sites.
slotsize already covers N_TNAME named structs (returns si.totsize),
str (16), []T (24), tuples, aliases, and primitives (8). Mirrors
cstage cgen.c cgmatch which falls through to bu->size for TY_STRUCT.

Cstage is correct; no mirror needed.

Test 695 covers seven shapes: the 3xi64 headline repro, a 4xi64
struct via let-init scrut (exercises >24B bind), a mixed-quadword
struct (i32+i32+i64+i64), str/slice/i32 negative controls, and a
direct let-init scrutinee variant. The wider 4xi64 row uses the
let-init shape because the N_DOT spill path in cgmatch tops out at
AX/DX/CX/R8 — a separate, unrelated gap from the bind size.
2026-05-15 02:14:00 +09:00
19fb3a3b3c selfhost: parser tsuffix plumb + cgen N_UN peel for tagged-store variant index (closes #32)
Typed-int literal assigned into a tagged-union slot (`h.e = 42i64;` where
e: (i32 | i64)) wrote tag = 0 (the i32 slot) instead of tag = 1 (the i64
slot). Cstage was correct: parse.c parseprimary copies tok.tsuffix onto
N_INTLIT, check.c stamps node.type = ty_i64, and cg_widen_tagged_store →
cg_tag_for_variant walks variants matching by structural type_eq —
ty_i64 lands at index 1. Wwstage had two gaps:

1. The parser (lib/ww/parse/expr.ww parseprimary) read p.curuval and
   p.curtext from the current token but never the tsuffix field. Token-
   side capture has been in place since the lexer's `i8/i16/.../u64/f32/
   f64` glue suffix landed (lib/ww/lex/lex.ww sets out.tsuffix); the
   parser side was missed. So an N_INTLIT for `42i64` carried tsuffix=""
   into cgen. Mirror of cmd/wcc/parse.c parseprimary's `n->tsuffix =
   t.tsuffix` line. Same plumb for N_FLOATLIT.

2. Wwstage has no checker stage to stamp N_UN's type from its inner
   expression's type. `-42i64` parses as N_UN(MINUS, N_INTLIT(42,
   tsuffix="i64")) and rhstargetname stopped at N_UN, returning "" and
   falling through to taggedvariantindex's "first non-str variant"
   fallback — which picked tag 0 (i32) for any numeric rhs in an
   (i32|i64) union. Cstage's cunop returns the inner type for
   TK_MINUS / TK_PLUS / TK_TILDE so the N_UN gets ty_i64 stamped
   naturally; wwstage gets the equivalent via an explicit peel in
   rhstargetname, recursing into rhs.lhs for these three ops. The
   recursion also covers nested unary (`- -42i64`), which parseunary
   builds as N_UN over N_UN over N_INTLIT.

The lib/ww/parse change is mirrored in selfhost/cmd/{w6c,wwdump}/
main.combined.ww so the bootstrap snapshot stays consistent with the
working frontend source. parser.curtsuffix is a new str field; refill
copies t.tsuffix into it; parseprimary TK_INT / TK_FLOAT copy it onto
the new node before advance.

Cstage handled both `42i64` and `-42i64` correctly already; no cstage
mirror needed.

Test 694_tagged_store_intlit — eleven rows running on both stages: i64
lit in (i32|i64); i32 lit (existing-working pin); i64 lit in
(i32|i64|str) with the str fallback at tail; u8 lit at head of
(u8|i32|i64); i64 lit at tail of (u8|i32|i64) with a +100 marker so
mis-binding into u8 can't masquerade as success; negative-i64 lit
(N_UN MINUS peel + sign extension through match-arm bind);
unary-plus i64 lit (N_UN PLUS peel); bitwise-not i64 lit (N_UN TILDE
peel; `~0i64 == -1i64`); nested unary `- -42i64` (recursion through
two N_UN levels); direct `let x: ev = 42i64;` (cglet's tagged-init
code path, separate write site from cgassign's field-write);
negative-control str field (pins the existing str-fallback path
through rhstargetname).

Pre-fix run on wwstage: 8/11 rows fail (every typed-i64 case including
all three unary operators, nested unary, and the direct let-init);
cstage 11/11 pass. Post-fix: 22/22 across both stages. make test
41/41. Bootstrap ww2 == ww3 == ww4 byte-identical.
2026-05-15 01:39:37 +09:00
bacbf4b845 w6c+selfhost: cgdot N_DOT tagged-field source ABI (closes #28)
cgdot of a tagged-union struct field previously dropped the AX/DX/CX/R8
payload-register convention used by tagged-union returns: cstage's
direct-struct branch stopped at CX (size > 16) and never loaded R8
(slice-payload variants, slot 32B); the via_ptr branch had no TY_TAGGED
handler at all, falling through to fldloadop and yielding only the tag
in AX. The N_DOT scrutinee fallback in N_MATCH similarly stored only AX
into the spill slot. Wwstage cgdot had no TY_TAGGED branch in any of
the direct, *struct, or top-level-global field-load paths, and cgmatch's
non-ident scrutinee branch didn't recognise N_DOT — dispatch always
computed want = 0 and the spill scratch was hardcoded 24B. The combined
effect: any code reading `s.taggedfield` and consuming more than one
quadword of the payload saw garbage in the upper halves.

Cstage: extended the direct-struct TY_TAGGED branch with an R8 load for
size > 24 (CX still loaded last so global LEAQ-into-CX rooting
survives), added a parallel TY_TAGGED handler to the via_ptr (TY_PTR
inner TY_STRUCT) field branch, and extended the N_DOT scrutinee spill
fallback in N_MATCH to write DX/CX/R8 alongside AX.

Wwstage: new cgloadtaggedfield helper emits the four-register load with
CX-last ordering, and dotfieldtnode resolves a field's declared type
node for a local-ident or *struct base. cgdot grew three TY_TAGGED
branches (direct local, *struct deref staging in BX, top-level global
through CX). cgmatch's non-ident-scrutinee branch grew an N_DOT type-
extraction path mirroring the N_CALL / N_INDEX shapes and now sizes the
@match_spill slot from slotsize(scrutt) so slice-payload variants don't
overflow the historical 24B alloc. rhstaggedabicall accepts N_DOT so
`let copy: ev = h.e;` and tagged-arg call sites pass through the
tagged-source spill branch of cgwidentaggedstore.

Out of scope for #28 and left as separate latents: wwstage's match-arm
bind for a TY_STRUCT-typed variant copies only 8B (cstage falls back
to bu->size; wwstage's bsz=8 default), and the variant-index lookup
for an i64 literal in (i32 | i64) picks the wrong tag on the write
side. Both surface in struct-payload tagged unions and merit their
own tasks; the new test rows steer clear so #28's fix verifies
end-to-end on scalar / str / slice payloads.

Test 693_dot_tagged_source — three variant shapes (16B i64, 24B str,
32B slice) read from direct local, *struct param, top-level global,
and let-init round-trip. The 32B-slice rows verify v.cap (R8 / +24)
so dropping the upper-word load isn't masked by len-only checks; the
top-level-global row routes the write through *p because the direct
global-LHS tagged store is a separate wwstage gap (followup). Three
negative controls (untagged i32 / str / slice fields) keep the new
TY_TAGGED guard from shadowing the existing field-load paths. Wired
into make test; 37 tests total. Bootstrap ww2 == ww3 == ww4
byte-identical.
2026-05-15 01:08:52 +09:00
49e39a4cf3 test: wire lib/getopt/getopttest.ww into make test
getopttest.ww existed and passed under `out/bin/ww run` but no
test/wcc/*.c ran it under `make test`. New 982_getopt_run runs it
and propagates exit; matches the 980_memio_run / 981_temp_run /
998_bufio_run pattern.
2026-05-15 00:28:59 +09:00
edb3350f54 test: wire lib/temp/temptest.ww into make test
temptest.ww existed and passed under `out/bin/ww run` but no
test/wcc/*.c ran it under `make test`. New 981_temp_run runs it
and propagates exit; matches the 980_memio_run / 998_bufio_run
pattern.
2026-05-15 00:27:38 +09:00
2cd88f1707 test: wire lib/memio/memiotest.ww into make test
memiotest.ww existed and passed under `out/bin/ww run` but no
test/wcc/*.c ran it under `make test`. New 980_memio_run runs it
and propagates exit; matches the 998_bufio_run pattern.

980-989 is the new sub-band for stdlib-run integration tests.
2026-05-15 00:26:10 +09:00
6402d8deb7 w6c+selfhost: cgen N_DOT slice-field through *T root in call args (closes #29) 2026-05-14 23:45:14 +09:00
9706513e59 selfhost: nodeisstr chained-str through value-struct in call args (closes #30) 2026-05-14 23:26:49 +09:00
79c758c046 test: wire lib/bufio/bufiotest.ww into make test (closes #23)
Closes the coverage gap that hid task #22's pointer-rooted N_DOT bug:
test/wcc/900_stdlib.c claimed "Coverage lives at lib/bufio/bufiotest.ww"
but no test/wcc/*.c ran bufiotest. New 998_bufio_run runs it and
propagates exit; matches 910_at_test pattern.
2026-05-14 02:51:49 +09:00
6354f5267c w6c+selfhost: cgassign N_DOT N_INDEX-lhs branch (closes #16)
Symmetric write-side counterpart of #8, bundled across both stages.
cstage [N]Struct write was broken (1986 gated on TY_PTR); wwstage had
no N_DOT(N_INDEX) write branch at all. New branch covers both
[N]*Struct and [N]Struct via viaptr flag, uses fldstoreop for
scalar/sub-word, MOVSS/MOVSD for float, two-MOVQ for str rhs.
Compound (PLUSEQ etc.) wired for integer scalar.
2026-05-14 00:09:46 +09:00
9ef9bef340 w6c+selfhost: cgen N_DOT N_INDEX-lhs branch (closes #8)
cstage cmd/w6c/cgen.c gained the missing N_DOT N_INDEX-lhs branch.
Covers both [N]*Struct and [N]Struct via fldloadop. wwstage already
handled [N]*Struct since 7c75dd2; refactored to mirror cstage exactly
and added [N]Struct. The spill workaround in dotchainresolve stays
(Pike rule); task #14 retires it as a follow-up.

Wwstage cgassign N_DOT(N_INDEX,...) silent store-drop discovered in
scope, filed as task #16.
2026-05-13 23:45:35 +09:00
d053560b80 w6c+selfhost: TK_AMP through chained N_DOT (closes #9)
Three shapes handled: value-struct chains (&o.i.a), pointer-field
(&p.f), slice/str pseudo-fields (&s.len). cstage inlines #6's spine
walker; wwstage reuses dotchainresolve unchanged. Silent-drop fallback
preserved.

Slice-header width mismatch in *&s.len writes filed as task #13.
2026-05-13 23:17:15 +09:00
5c8724845a selfhost: cgfn pre-scan SysV-class accounting (closes #7)
Mirrors cgen.c §5130-5223 / cgfnparams: reg-spill, tagged-partial-fit,
pure-stack. Pure-stack does not bump cursor (cstage semantics). Fixes
16B over-allocation on 7+ arg functions; bootstrap stays byte-identical.

Slice/str at reg/stack straddle is deferred to task #11 (cgfnparams
doesn't stitch them either); pre-scan stays symmetric until then.
2026-05-13 22:40:43 +09:00
c4b3aca5e4 w6c+selfhost: principled sub-word signedness (closes #5/#10)
type_isunsigned recurses TY_ENUM and includes TY_RUNE on both stages.
13 LOAD + 6 STORE ladder sites (cstage) plus 4 more wwstage stragglers
in cgindex/cgforrange collapsed to fldloadop/fldstoreop helpers. N_CAST
narrow gate symmetrised; task #1's literal-kind workaround retired.
bool kept out of type_isunsigned, special-cased in field helpers.

Retroactively fixes a u32 mis-sign-extend in deref-compound (sz=4
hardcoded MOVSXD), pinned by new 660_field_signed row.
2026-05-13 22:23:00 +09:00
461a448d5f w6c+selfhost: cgen chained N_DOT/N_ASSIGN spine walk
Loop-shaped spine walker for value-struct chains (o.i.a) and slice/str
pseudo-fields (s.buf.len), read+write, both stages. SB-fallback at the
catch-all preserved for unresolved module-qualified idents.

Follow-ups filed: tasks #7-#10 (wwstage >6-arg frame over-alloc, chained
array-elem field BX loss, & through chained DOT, signed sub-word field
loads zero-extend).
2026-05-13 21:44:50 +09:00
388ab8a707 w6c+selfhost: cgen N_CAST signed narrow + peel !T on unsigned narrow
TY_RUNE excluded — wwstage primsize skips it; task #5 will collapse
both gates back to symmetric once type_isunsigned recurses TY_ENUM.
2026-05-13 20:06:24 +09:00
e16634baec lib: add missing Hare-stdlib functions (ascii/bytes/strings/path/endian)
ascii: valid, validstr, ispunct, isprint, iscntrl, isgraph, isblank,
strcasecmp.

bytes: hasprefix, hassuffix, rindex, rindexbyte, contains, reverse,
zero.

strings: rindex, sub, trimprefix, trimsuffix, ltrimbyte, rtrimbyte,
trimbyte. The byte-set trim is a single-byte subset of Hare's
`trim(input, exclude: rune...)`; no variadic ABI yet.

path: dirname, basename, extension, join. Owned-str returns where the
result isn't a borrowed view of the input (join).

endian: full Hare table — be/le get/put for u16/u32/u64 plus the
network-order htonu/ntohu pair extended to 32/64.

lib/CLAUDE.md rewritten to reflect the post-graduation policy
(tagged-union returns, owned-str returns, plan9 names, documented
deviations for non-graduating modules).

Makefile picks up lib/ascii and lib/fmt as wwdump_ww / w6c_ww deps so
lib-only edits regenerate the affected binaries.
2026-05-13 04:03:55 +09:00
f4743dc5d5 lib/strconv: add f64tos 2026-05-13 03:10:25 +09:00
d998425391 w6a+w6l: DATAR directive for absolute-address relocs in .data
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.
2026-05-12 12:45:11 +09:00
3c812faa08 w6c+selfhost: codegen for top-level mutable let
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.
2026-05-12 11:56:51 +09:00
38e0b6510a w6l: route writable globals into a second PT_LOAD
Second step toward top-level mutable `let`. The static path now loads
.data PROGBITS sections from input .o files, page-aligns them after
.text, and emits a second PT_LOAD (R+W) covering them. Relocations
targeting data symbols compute against the data VA; text→text
displacements still cancel the absolute VAs and stay correct.

Inputs without any .data keep the original single-PT_LOAD layout
byte-for-byte — 992 (selfhost w6l .o diff) and 995 (self-rebuild)
depend on that invariant.

Dynamic-link path (-l/-L) rejects .data for now with a clear error;
folding writable globals into the existing R+W segment alongside
.got.plt/.dynamic is a follow-up.
2026-05-12 11:42:30 +09:00
1b0955c97b w6a: DATAW directive for writable .data section
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.
2026-05-12 11:35:50 +09:00
22999cd3fa selfhost: mirror @test runner via ww_ww (997_at_test_ww)
@test parsing already works under ww_ww (parseattrs lives in the
shared lib/ww/parse/decl.ww, picked up by both Cstage and wwstage),
so this is the test-side parity: 997 is 910 with `ww run` swapped
for `ww_ww run`, exercising the selfhost driver+compiler+assembler+
linker end-to-end on the same attest_pass.ww fixture. Suite is now
22 tests.
2026-05-12 03:58:55 +09:00
68bd8197d5 selfhost: port match exhaustiveness, ?-subset, !-flag checks to check.ww
The selfhost checker did name resolution only — anything tagged-
union-shaped sailed through silently. The C check.c implements
three structural checks; this commit mirrors them at the AST level
in selfhost/cmd/wcc/check.ww:

1. Match exhaustiveness: every variant of the scrutinee's tagged
   union must be covered by a case arm (incl. multi-pattern alts)
   or a default arm. Operates on the scrutinee's declared type
   (N_TTAGGED via N_IDENT's sym.decl.lhs).

2. ? subset propagation: each error variant of the operand's type
   must be a variant of the enclosing fn's return type. Enclosing
   return must itself be a tagged union when the operand has any
   errors.

3. !-flag semantics: in flag-aware mode (any variant marked `!T`),
   error subset = flagged variants. Legacy mode (no flags) =
   everything-but-first. is_error_variant unifies both rules.

No tinfo / type-inference work: the checks read declared AST type
nodes directly. `resolvealias` chases N_TNAME → typedecl body to
handle aliased tagged unions. `type_eq_ast` does structural
comparison on the subset of type-expression shapes the checks
encounter (TNAME by string, TPTR/TSLICE/TCHAN recursive).

Folded into resolvewalk rather than a separate second pass, so the
checks see the same per-statement scope state as resolve. fnret is
threaded through resolvefnbody so ? can find the enclosing return.

New test/wcc/950_selfcheck.c — five rows exercising each error path
(missing variant, non-tagged enclosing, missing error subset
member, the flag-aware happy path, the flag-aware missing-error
case). Test suite now reports 21 ok.
2026-05-12 03:24:25 +09:00
906e17b128 wcc: @test marker attributes + runner
Hare-style `@test fn check_foo() void = { ... }` now parses. The
attribute is recognised by making the args list optional in
parseattrs: `@symbol("rt_syscall")` still requires the parens;
`@test` doesn't. Same change mirrored in lib/ww/parse/decl.ww.

The runner (test/wcc/910_at_test.c) scans a fixture for
`@test fn IDENT(`, synthesises a wrapper `main()` that calls each
test fn, builds it via `ww run`, and asserts exit 0. A failing
@test would either explicitly call abort or trip a runtime trap
(div-by-zero, etc.) and the whole driver exits non-zero.

The 910_at_test target sits alongside the existing C-side test
binaries; `make test` now runs 20 tests instead of 19.

Fixture: test/wcc/data/attest_pass.ww exercises two passing tests
(simple arithmetic and a match-with-yield).
2026-05-12 03:14:20 +09:00
9e383b7ba0 w6c: lower append() to rt_ensure + inline store (hare model) 2026-05-11 17:02:32 +09:00
5408160d49 rt: move slice append helpers from lib/slices/ into libwwrt.a 2026-05-11 16:56:46 +09:00
8ffe6dbee6 ww: split parse.ww into parse/{parse,expr,stmt,decl}.ww submodule 2026-05-11 16:48:58 +09:00
72dfb6ac8d ww: group lex.ww + tok.ww into lib/ww/lex/ submodule 2026-05-11 16:42:13 +09:00
1e7be36577 wcc/cgen: drop underscore from cgen_*.ww filenames 2026-05-11 16:19:46 +09:00
177862fb29 ww: lift introspection files to lib/ww/ (ast, lex, tok, parse, typ, sym) 2026-05-11 16:17:23 +09:00
ebcc8f2d09 wcc/cgen: lift helpers→cgen_util, fn/file→cgen_decl 2026-05-11 15:39:39 +09:00
328bcd743c wcc/cgen: split cgexpr→cgen_expr.ww, cgstmt→cgen_stmt.ww (rob pike #5) 2026-05-11 15:35:16 +09:00
2c33228b7e ww: rename toolchain to w-prefix + hare-style build/run/test driver
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.
2026-05-11 13:49:27 +09:00
e217cd32d1 6l: port ET_DYN dynamic linking to the ww side
Ports cmd/6l/{dyn,dynout}.c into selfhost/cmd/6l/{dyn,dynout}.ww:
ET_DYN .so loading + PT_INTERP/PT_DYNAMIC ELF emission with .rela.plt,
.gnu.version_r, BIND_NOW. lsym grows dyn fields; pass.ww promotes
undefs to dyn; out.ww dispatches; main.ww takes -L/-l. The ww driver
forwards -L/-l to 6l_ww so 'ww_ww build snake.ww -L /usr/lib -l ncurses
-l c' runs without cc.

Test 996 pins byte-identical output to C-6l on snake.

'make bootstrap' gains a fourth stage with cmp ww3 == ww4, proving
ww3 is byte-stable when used as a compiler — not just a coincidental
two-stage equilibrium.

Four wwstage 6c cgen quirks surfaced and are documented in dynout.ww's
header (two-level field-write through a pointer field, (scalar, str)
tuple returns, def : str, ≤6 arg calling convention).
2026-05-11 12:47:36 +09:00
edfc4273f6 nocc: stage-0 bootstrap path (binaries gitignored)
Sets up the cc-free fresh-checkout flow without yet committing the
stage-0 binaries. The pieces are in place; flipping the switch is
one `git add -f` away when the compiler is judged stable.

  bootstrap/<arch>/{ww,6c,6a,6l}   stage-0 binaries (gitignored)
  bootstrap/README.md              layout + workflow

Two new targets:

  make bootstrap-snapshot   populates bootstrap/$(ARCH)/ from the
                            currently-built wwstage
  make nocc                 starts from bootstrap/$(ARCH)/, rebuilds
                            the wwstage from source, gates on
                            stage-0 == rebuilt byte-for-byte. Lands
                            in $(OUT)/nocc/ so $(OUT)/ stays
                            untouched. cc is never invoked.

The fixed-point gate has the same shape as `make bootstrap`, just
the entry point flipped: that target trusts cstage; nocc trusts
the checked-in (or local-snapshot) stage-0 binaries.

Implementation notes:

- libwwrt.a is assembled with stage-0 6a, members ordered to match
  $(RT_OBJ) so the embedded symbol table reproduces the cstage
  build byte-for-byte.
- The driver's default lib path is $self_dir/../../lib, which under
  $(NOCC_BIN) resolves to $(NOCC_OUT)/lib (libwwrt only). Each
  ww-build invocation passes -I $(CURDIR)/lib so `use os` etc.
  resolve to source.
- Stage-0 binaries are duplicated under both natural names (ww/6c/
  6a/6l) and _ww-suffixed names so the wwstage driver's
  hard-coded join_path_lit(self_dir, "6c_ww") still finds them.
  When cmd/wwc/ gets deleted at v1.0, that duplication and the
  _ww suffix in the driver both go away.
2026-05-11 11:51:34 +09:00
37bffa5284 test: 995_self_rebuild — wwstage rebuilds itself byte-identical
Drives ww_ww (which already shells to 6c_ww/6a_ww/6l_ww) over each
wwstage tool's source and diffs the resulting binary against the
cstage-built canonical in $BIN. A green run means the toolchain
can recompile itself end-to-end without invoking cc, modulo the
cold-start binary that brings the wwstage into existence.

Stricter than `make bootstrap`: that loop pins wwdump's cgen
self-stabilising; this pins all five wwstage tools (6c, 6a, 6l,
ww, wwdump) round-tripping through the wwstage pipeline.

The .combined.ww refreshes are the expander picking up the
parser/cgen changes from the prior commit. selfhost/cmd/6c/
gains its main.combined.ww for the first time — 995 builds it,
994 reads it.
2026-05-11 11:41:12 +09:00
218d8469ff 6c: ww-side compiler binary, dup2 syscall, test 994
selfhost/cmd/6c/main.ww is a thin packaging of the wwc cgen — slurp
a .ww file, run lex+parse+cgen, write Plan 9 amd64 asm to the path
given by -o. The cgen routines in selfhost/cmd/wwc/cgen.ww write
directly to fd 1, so we use dup2 to redirect stdout into the
output file rather than thread an fd through every emit helper.
Adds the SYS_DUP2=33 wrapper in lib/os.

Makefile wires $(BIN)/6c_ww alongside the other wwstage tools and
adds $(BIN)/test_6c_ww to the TESTS list.

test/wwc/994_6c_ww.c diffs 6c_ww byte-for-byte against
`wwdump_ww -c` on five in-source programs plus the four selfhost
main.combined.ww files: same cgen reached through two binaries, so
any divergence is a packaging bug in selfhost/cmd/6c.

We deliberately don't diff against C-side 6c here — 990 probe 5
already covers that on the subset the ww cgen handles today.
2026-05-11 11:20:06 +09:00