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.
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 oserror = i64` carries -errno (Hare's errors::errno-shaped
named-i64). The three convenience wrappers move off the i64 = -1
sentinel and onto the tagged-union surface.
Callers updated across the selfhost (wwdump, w6c, w6a, w6l, ww
driver). The slurp paths in w6c/w6a/w6l/wwdump now match on the
filesize and readall results; the ELF-emitting writeall sites in
w6a/obj.ww are wrapped through two small local helpers (`wrn` for
"wrote N bytes ok?", `wrdrop` for fire-and-forget) so the existing
11-callsite write loop stays readable.
selfhost/test/smoke.ww kept using raw os.read instead of
os.readall: the 990 cgen-match probe compiles smoke.ww standalone
(no `use` expansion), and cross-module type references like
`os.oserror` can't be resolved in that mode.
Two selfhost-side gaps surfaced and got plugged:
- lib/ww/parse/parse.ww parsetype now collapses dotted type names
(`pkg.Type` → single N_TNAME with the joined string), mirroring C
parsetype's dotted-path loop. Local `joindotted` helper because
there's no arena-based string-concat in the selfhost lib yet.
- selfhost/cmd/wcc/check.ww name-resolver applies the dotted-prefix
rule from cmd/wcc/check.c's resolve_typename: split at the last
dot, look up the head as a `use` import, then the leaf as a type.
read → (i32 | eof | closed), write → (i32 | closed),
close → (void | closed). `type eof = void` and `type closed = void`
are exported as named-void variants — distinct nominal tags despite
identical (zero-byte) payloads.
No existing callers exercised the eof/closed sentinels (the smoke
test mimics the stream pattern with its own local types), so the
graduation is purely API shape — no callsite churn.
Both used the -1 sentinel return; both had no external callers, so
the graduation is purely the API-shape change. utf8.runesz uses void
for "rune outside legal range"; bufio.readbyte uses void for EOF
(empty buffer). The full Hare shapes ((size | invalid) and
(u8 | EOF | io::error)) are still richer than this — those richer
returns arrive when utf8 grows an explicit invalid type and bufio
wires through io::stream's error path.
ascii.digitval returns (i32 | void) instead of an i32 -1 sentinel.
Two callers updated to match-on the result (lib/ww/lex/lex.ww escape
parse, selfhost/test/smoke.ww probe 6).
`!` would have been more idiomatic at both call sites — both have
verified isxdigit beforehand — but the selfhost parser doesn't yet
recognize postfix `!`/`?`, so using them in bootstrap-bound code
breaks the 993/995 byte-identity gates. Match is fine for now.
Selfhost cgen follow-on for the 8-byte-rounded tagged-union ABI
(landed in 1e2f55a for the C side):
- cgenutil.slotsize: tagged size = 8 (tag) + max(payload), padded to
8-byte multiple. Was hardcoded 24.
- cgendecl prologue: spill size/8 arg registers, not always 3.
- cgenstmt cglet tagged-call path: spill the CX value-word only when
the slot is >16 bytes.
All three were emitting 3-register patterns appropriate to (T | str)
sized unions and overflowing the new 16-byte (i32 | void) slots.
`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.
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.
`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.
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).
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.
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.