Commit Graph

34 Commits

Author SHA1 Message Date
b9443b1f33 os: graduate O_*, SEEK_* defs to flag and whence enums
Mirrors Hare's `fs::flag` and `io::whence`:

    export type flag = enum i32 {
            RDONLY  = 0,
            WRONLY  = 1,
            RDWR    = 2,
            CREATE  = 64,    // 0o100
            TRUNC   = 512,   // 0o1000
    };

    export type whence = enum i32 { SET = 0, CUR = 1, END = 2 };

open/tryopen/lseek signatures take the enum types (`flags: flag`,
`w: whence`) so callers get type-checked: `os.open(p, os.flag.RDONLY,
0)` is the correct shape, and `os.flag.WRONLY | os.flag.CREATE |
os.flag.TRUNC` typechecks as a `flag` via the same-named-type rule.

Callers in selfhost/cmd/{ww,w6c,w6a,w6l,wwdump} updated from
`os.O_RDONLY` etc. to `os.flag.RDONLY`. SYS_* syscall numbers kept
as `def` for now (internal-only, ABI surface, no Hare analogue in
this scope).

selfhost/test/smoke.ww keeps its standalone-compile property by
using a numeric literal (`0`, RDONLY's value) for the open flags
arg — probe 6 in 990_selfhost compiles smoke.ww with no `use`
expansion, so cross-module type refs like `os.flag.RDONLY` can't
resolve there. Untyped 0 → flag via type_isnum.
2026-05-12 04:41:50 +09:00
5149d10618 wcc+selfhost: pkg-qualified enum access (os.whence.CUR)
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`.
2026-05-12 04:30:20 +09:00
f597ce67f6 selfhost: cgen for enum (member fold + as pass-through)
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.
2026-05-12 04:25:28 +09:00
34817eedcd wcc: Hare-style enum [storage] { ... } type
`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.
2026-05-12 04:15:31 +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
cb78abf9e9 selfhost: is/as validity + let/return assignability checks
Three more structural checks from C check.c ported to selfhost,
at the AST level (no resolved tinfo).

is/as validity: e is T / e as T require e's declared type to be a
tagged union and T to name a variant. Mirrors the case-variant
check that just landed.

let init-type and return-type assignability: a new exprtype helper
infers an AST type-node for literal/ident/call/cast/?/as/is
expressions; isassignable approximates C type_assignable on the
shapes we can resolve — exact match, untyped numeric → typed
numeric, untyped nil → ptr/slice/chan/fn, variant inclusion, and
two-primitive-mismatch.

isassignable returns (ok, confident). When confident=false the
check emits no error — better to miss a real bug than fire a
false positive on a binary-op expression we can't infer. This
keeps existing selfhost code clean while still catching the
common typo cases (let x: bool = 42; return "hi" from i32 fn).

Naming: all new helpers follow Plan 9 run-together convention per
CLAUDE.md (`typeeqast`, `isassignable`, `exprtype`, ...). Earlier
work that used snake_case helpers (`case_variant_in`,
`check_match_exhaustive`, ...) got the same treatment — bulk
renamed in this commit.

Five new rows in 950_selfcheck exercise the new checks
(is-not-a-variant, two let mismatches, return mismatch, plus the
case-variant row already there).
2026-05-12 03:49:09 +09:00
751271a6bd wcc: case T => variant validity check (C + selfhost)
`match (u) { case T => ... }` where T isn't a variant of u was
silently accepted by both checkers. The cgen would emit a tag
comparison against an index that never appears, leaving the arm
unreachable — wasted code that's almost always a bug or typo.

C check.c now mirrors the existing is/as rule for match arms:
each `case T` and each alt of multi-pattern `case T1 | T2` is
checked against the scrutinee's variant list via variant_present.

selfhost check.ww gets the same shape with AST-level type_eq_ast
comparison. Both checks land in the same scope-aware pass that
already runs exhaustiveness and ? subset.

New test rows in 300_check (C side) and 950_selfcheck (selfhost
side) exercise both single-pattern and multi-pattern alt typos.
The 950 driver's err_present detector picks up the new
"case: not a variant" prefix.
2026-05-12 03:37:21 +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
404705b6fd selfhost: mirror defer; e2e tests for LIFO ordering
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.
2026-05-12 03:11:13 +09:00
f267f99a2b wcc: match-as-expression with yield
`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.
2026-05-12 03:08:00 +09:00
f4efaac144 wcc: nullable pointer folding for (*T | void)
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.
2026-05-12 02:53:47 +09:00
d9041ab45e os: graduate tryopen/trywrite/tryread to (T | oserror)
Final piece of the os module graduation: the three try* wrappers
move off the (T | str) placeholder shape. `oserror` becomes a real
Hare-style error type (`!i64` instead of plain `i64`), so it's
picked up by ?-propagation as the error half without callers having
to name it.

tryread:  (i64 | oserror)   was (i64 | str)
trywrite: (i64 | oserror)   was (i64 | str)
tryopen:  (i32 | oserror)   was (i32 | str)

Callsites updated: wwdump uses tryopen; the e2e trywrite probe
matches on os.oserror and validates -EBADF for a bad fd (-9 instead
of the old "write failed" string length).

selfhost/test/smoke.ww switched to raw os.open(2) instead of
os.tryopen for probe 7 — same reason as the os.readall switch in
the prior commit: probe 6 in 990_selfhost compiles smoke.ww
standalone, and cross-module type refs like `os.oserror` don't
resolve in that mode.
2026-05-12 02:42:49 +09:00
594a2bad62 wcc: Hare-style !T error marker on tagged-union variants
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.
2026-05-12 02:39:54 +09:00
4085742853 strconv: graduate to (T | invalid | overflow); add void expression
`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.
2026-05-12 02:02:08 +09:00
1e2f55aed8 lib: graduate bytes/strings find-funcs to (i32 | void)
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.
2026-05-12 01:49:00 +09:00
41a82021a3 wcc: ? error-subset propagation typecheck + tag remap
`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.
2026-05-12 01:40:02 +09:00
fa070b6d07 wcc: tagged-union foundations (never, void, flatten, exhaust)
- `never` bottom type: TY_NEVER, assignable to anything; size 0.
- Type-set normalization for N_TTAGGED in resolve_type:
  - flatten nested anonymous (A|B)|C → (A|B|C); named aliases stay
    nominal (not flattened through)
  - dedup duplicates (NAMED pointer-id; others structural)
  - drop `never` variants
  - collapse single-element set: (T|never) → T, (T|T) → T
- Match exhaustiveness: error when a variant is unhandled and no
  default arm covers it. Multi-pattern `case T1 | T2 =>` counts
  each alt.
- (T | void) optionals: bare `return;` from a tagged-union-returning
  fn emits the void variant's tag (payload undefined; void size 0).

selfhost mirrored: TY_NEVER constant + tynever in tctx + seedprim
entry; voidvariantindex helper; cgreturn bare-return handling.
2026-05-12 01:31:35 +09:00
1ac1d985f6 lib: rename stdlib surface to Hare names; add endian/math
Sweeping rename so the lib/ surface mirrors Hare's stdlib spellings.
- ascii: rune-taking predicates; ishex -> isxdigit
- bufio: rinit -> init; take1/takeline -> readbyte/readline
- bytes: indexsub -> index
- encoding/utf8: runelen -> runesz
- errors: eEOF/eShortRead/... -> eof/underread/...
- fmt: errln -> errorln; println/fprintln return i64
- os: readfull/writefull -> readall/writeall; unlink -> remove
- path: isabs -> abs; drop lastindex (now strings.rbyteindex)
- strconv: u64toa/i64toa -> u64tos/i64tos; parse64/parseu64 -> stoi64/stou64
- strings: drop len/isempty; equal -> compare; indexbyte -> byteindex; +rbyteindex
- types: drop numeric helpers (moved to math)
- new lib/endian (htonu16/ntohu16), lib/math (absi32/absi64)
- net: drop htons (use endian.htonu16)

Callers in selfhost/, lib/ww/, cmd/w6c/cgen.c, and test/wcc/700_e2e.c
updated to match.
2026-05-12 00:45:18 +09:00
35421f2561 wcc: Hare-style struct embedding (anon + bare-name)
Struct members can be `struct { ... }` (anonymous nested) or a bare
named type, in addition to `name: type`. The inner struct's fields
are promoted to the outer scope with offsets shifted by the embed
base; codegen already keys off Tfield.offset so cgen needs no change.
Errors on non-struct embed or name collision.
2026-05-11 23:32:44 +09:00
dd188ca460 ww: add Hare-style is/as postfix ops on tagged unions
`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).
2026-05-11 23:21:08 +09:00
c5f30f2fce ww: cgen trap batch (def-str field, chained-ptr write, scalar+str tuple ABI) 2026-05-11 20:49:21 +09:00
579cc39f9b w6c: float-aware unary minus (fix -1.0 emitting +1.0 bit pattern) 2026-05-11 17:52:29 +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
177862fb29 ww: lift introspection files to lib/ww/ (ast, lex, tok, parse, typ, sym) 2026-05-11 16:17:23 +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
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
ecf0a84127 6l: dynamic linking with symbol versioning
Teach the linker to consume ET_DYN shared objects and emit a
dynamically-linked ELF executable. Snake et al. can now link
against libncurses + libc through the system dynamic loader.

Pipeline additions:

- dyn.c: read ET_DYN, parse .dynsym + DT_SONAME, walk
  .gnu.version_d / .gnu.version to learn each export's default
  version (skip hidden entries).
- pass.c: when an undefined sym is provided by some Lso,
  promote it to dynamic, assign a PLT slot, record the
  matched version on the Lsym.
- dynout.c: emit PT_INTERP + PT_DYNAMIC, .dynsym/.dynstr/.hash,
  .plt + .got.plt + .rela.plt, .gnu.version + .gnu.version_r,
  and the full DT_* set with DT_BIND_NOW. Patch PC32/PLT32
  references against dyn syms to point at their PLT stubs.
- main.c: -L<dir> and -l<name> flag parsing; resolve <name>
  via .so / .so.<N> / .a in libdir order, skipping GNU ld
  linker scripts (libc.so on most distros).
- ww driver: collect -l/-L (joined and split forms) and pass
  through to 6l.

Design choices:

- DT_BIND_NOW so the loader resolves all PLT slots at startup;
  no PLT0 lazy resolver stub.
- SysV .hash, not .gnu.hash. One bucket; loader scans the
  chain. Slow at scale, fine for snake-class binaries.
- Non-PIE at fixed 0x400000.
- No section headers — loader uses program headers, but
  readelf -V/-S won't display anything.

Symbol versioning is the only correctness item beyond the
basic PLT/GOT machinery: glibc symbols default to versions
later than GLIBC_2.2.5 (e.g. clock_gettime → GLIBC_2.17 for
the vDSO impl), and the loader rejects unversioned references
to those without a matching Vernaux entry.

test/wwc/810_dyn covers four cases: bare libc dyn call,
multi-PLT, clock_gettime versioning, and fn-pointer to FFI
binding (which exercises the codegen fixes from the parent
commit alongside the new linker path).
2026-05-11 09:42:27 +09:00
635818eb13 6c: float compare, fn-address @symbol, indirect call
Three N_BIN/N_IDENT/N_CALL sites missed cases that bit ncurses
demos and dyn-linker exercises:

- Float compare emitted CMPQ + signed Jcc on f64 bits. Now goes
  through UCOMISD/UCOMISS + the unsigned Jcc family, mirroring
  Plan 9 6c (txt.c around AUCOMISD).
- LEAQ-of-fn-ident (taking the address of a function as a value)
  used n->str without ffi_resolve, so binding a *fn from an
  @symbol("name") fn declaration produced a reference to the
  ww ident rather than the C symbol.
- N_CALL on a bare ident with no localfind hit was a direct
  CALL ident(SB). Now checks localfind first and indirects
  through AX when the callee names a local fn-pointer slot.

Hare/QBE handles both shapes the same way (sort/search.ha:15
calls cmp(...) where cmp is a *cmpfunc parameter).
2026-05-11 09:42:02 +09:00
1657bdeda3 ww: import toolchain — C bootstrap + ww-side self-host (phases 0-10)
C bootstrap (phases 0-9):
  cmd/wwc, cmd/6c, cmd/6a, cmd/6l, cmd/ww, rt, lib/*.

ww-side self-host (phase 10):
  selfhost/cmd/wwc — ww-cgen frontend; bootstrap fixed point.
  selfhost/cmd/6a  — assembler; byte-identical to C 6a (test 991).
  selfhost/cmd/6l  — linker w/ archive (.a) support; byte-identical
                     to C 6l (test 992).
  selfhost/cmd/ww  — driver (build/run/version); byte-identical to
                     C ww (test 993).

make test: 15/15. make bootstrap: ww2.s == ww3.s, ww2.o == ww3.o,
ww2 == ww3 byte-identical, with the full ww-tooled chain.
2026-05-11 02:17:47 +09:00