When `use fmt;` is in scope and a local/param named `fmt` shadows it,
`fmt.X` in the body silently resolved to the str-typed value sym and
emitted `CALL AX` through str.ptr → runtime crash. Surfaced during
#15 (lib/log's printfln family); worked around by renaming the param
`fmt`→`format`.
Per rob + user, option (C): "value names and module names are
disjoint." Refuse the shadow at the decl site. Single rule, no
non-local reasoning, no silent footgun if a future lib/X exports a
new leaf.
cstage: src_imports walks file->list for N_USE entries (skipping
self-imports where u->module == u->str — same-module fixtures like
lib/fmt/fmttest.ww carry these); check_module_shadow runs before
each SK_PARAM / SK_VAR scope_define (param, clet, mlet, forrange
single + tuple, mcase). Wwstage mirror in check.ww; wwdump-only
diagnostic today, full enforcement waits on #11 checkfile pass.
Bootstrap byte-id holds — no codegen change. One source patch in
selfhost/cmd/w6a/main.ww renames an outer `let asm: asm_;` to `s` to
sidestep task #27 (cstage localoff scope-blind dedup); unrelated to
#19 but the new rule's first run flagged it as a self-shadow.
Test 708 (param_shadow_mod): 4 rows — neg_param (param shadow errs
at fn decl line), neg_let (let shadow errs at let decl), pos_rename
(rename compiles + runs), pos_selfimp (in-module use is skipped).
4 wired sites without dedicated rows deferred to task #28.
Follow-up: lib/log can revert format→fmt now that the silent
crash is impossible.
Hare-shaped filestat introspection. New types: filestat (80B,
mirrors fs::filestat ref/hare/fs/types.ha:141), mode (31-member
enum mirroring fs::mode ref/hare/fs/types.ha:63), stat_mask (7 bits
mirroring fs::stat_mask ref/hare/fs/types.ha:129), timespec (i64+i64,
layout-compatible with future lib/time::instant).
APIs: stat / lstat / fstat (*filestat, *u8|i32) (void|oserror) over
SYS_newfstatat (nr=262). The out-param shape sidesteps the cgreturn
24B ABI cap; commented inline. exists(*u8) bool goes through the
syscall directly rather than wrapping stat()? — dodges task #22's
80B-scrutinee match-slot disagreement until that lands.
Three latent cgen workarounds in tree, all pointer'd to filed tasks:
#22: os.exists sidesteps the (void|oserror) match shape
#24: `at` enum bundles AT_FDCWD/SYMLINK_NOFOLLOW/EMPTY_PATH instead
of three top-level `def`s (negative-literal def DATA omit)
#25: kstat.mode typed as `mode` (enum) rather than u32 to skip the
redundant u32→enum cast emit
Tests: 976_stat_run, 9 rows — stat/lstat/fstat × regfile/dir/symlink
plus exists × {regfile,dir,noent}. Row 1 also pins perm-bit and
atime/mtime/ctime!=0 to catch silent kstat→filestat offset miscompiles
(kstat fields at 72/88/104).
Graduation to lib/fs when it ships is noted inline; signatures stay
rename-compatible.
Add os.alloc(n: u64) *void and os.free(p: *void, n: u64) void as
`export fn` via @symbol("rt_alloc") / @symbol("rt_free"). Signatures
mirror lib/memio's existing internal bindings byte-for-byte — only
the name and `export` keyword change. lib/memio + lib/shlex + lib/
getopt drop their own copies in a follow-up commit.
Doc comment spells out the actual failure ABI: rt_alloc wraps the
raw mmap syscall (no libc), so OOM yields a negative-errno cast to
`*void` (e.g. (void*)-12 for ENOMEM). Neither `== nil` nor the libc
MAP_FAILED `(void*)-1` value catches it; deref faults. A typed
fallible variant is future work (alongside #16 fmt.asprintf).
Test (ostest test_alloc_free_roundtrip, signalled=5): alloc 4096B,
write 0x5a at head + 0xa5 at tail, read-back asserts both, free.
The head+tail write/read prevents DCE (failure path calls os.exit)
and proves a real page is backing the returned pointer.
Auto-regen of derived files; lib/os.mkdirs landed in 19aa66a but the
selfhost combined.ww snapshots that fold lib/os in were not regened
in that commit. Catching them up now so the next make doesn't fight
the tree.
No source change; make test 61/61.
temp mirrors Hare's temp: file, named, dir. file() routes through
named() and discards the path (no O_TMPFILE yet). Path randomizer
uses inline SplitMix64 seeded from getpid + O_EXCL retry (Hare uses
crypto::random which we don't ship). named() takes out-pointers for
fd + path — return shape gated on tasks #5 and #11. Caller closes
and removes; no defer in ww.
os gains mkdir, rmdir, flag.EXCL — straight ports of ref/hare/os.
selfhost combined files cascade; 995_self_rebuild byte-identity
holds.
Seven fixes across the toolchain, plus three new lib/hash modules
(adler32, crc16, crc32) that surfaced them.
1. `~x` on u8/u16/u32 left the upper bits set: NOTQ inverts the
whole 64-bit register and nothing trimmed it back to type
width, so a returned `u16` would compare 64-bit against a
typed literal and disagree. Both stages now mask after NOTQ
for narrow unsigned: AND $0xFF/0xFFFF for u8/u16, MOVL r,r for
u32 (ANDQ $0xFFFFFFFF sign-extends imm32 and is a no-op).
Signed narrows stay sign-extended and need no fix-up. See
cmd/w6c/cgen.c N_UN TK_TILDE and selfhost cgenexpr.ww cgun
TK_TILDE with new nodeprimwidth helper.
2. w6a had no D_CONST immediate path for ANDQ / ORQ. cgen would
emit `ANDQ $65535, AX` and the rr encoder silently wrote
`21 /r` with garbage reg fields — the mask never happened.
Added `81 /4` (AND) and `81 /1` (OR) imm32 paths in both
cstage and selfhost w6a. The ~width fix above depends on this.
3. `s: []u8` cast as a direct fn argument produced a 0-length
slice. cgexpr for N_CAST left (AX=ptr, BX=len) from the str
source but never set CX (cap), and the arg-push fallback only
pushed AX. cgcast now synthesises CX=BX when target is slice
and source is str; node_isslice / arg-push recognise
cast-to-slice and emit the full (cap, len, ptr) triple. Both
stages.
4. `*[N]T` element-store used 8-byte stride + MOVQ regardless of
T's width. Indexing `buf: *[4]u16` would step 8 bytes and
write 8 bytes per element. Added idx_eff (drills *[N]T → T)
in cstage and the matching pointer-array drill in selfhost
elemsizeof. Also added MOVW / MOVZWQ / MOVSWQ to w6c, w6a,
and selfhost mirrors so 2-byte element stores/loads use the
right opcode (was falling through to MOVQ and trailing 6 bytes
into the next slot).
5. Slicing a top-level fixed array (`g[0:n]` where `g: [N]T` is
a global) computed the base from BP instead of the symbol —
localfind returned 0 and the cgen treated it as a local at
offset 0. Both N_SLICE-as-expression (cgslice) and N_SLICE-
as-call-arg paths now check let_islet / letvartnode and emit
LEAQ name(SB) when the base is a global array (or MOVQ
name(SB) for a global slice/pointer base). Both stages.
6. Top-level `let arr: [N]T = [v0, v1, ...]` link-failed on
cstage — emit_lets bailed when it saw N_ARRLIT init on an
array type, and the sz==8 scalar path then misemitted any
8-byte-sized array (e.g. [4]u16, [8]u8) as a single quad.
emit_lets now walks N_ARRLIT, evaluates each element as an
int/rune/bool/nil literal, packs per-element bytes
little-endian, and honours the trailing `...` repeat marker.
Selfhost already handled the literal-init path; fixed the
parallel sz==8 duplicate-DATAW emit on its side (the array
and the scalar paths both fired, last write winning at link
but the duplicate broke cross-stage byte-identicality on user
code with this shape).
7. w6a's per-line input buffer was a 1KB stack `char buf[1024]`.
A `DATAW` for a [256]u16 emits ~2080 bytes on one line, which
truncated mid-escape; the assembler then re-parsed the
remaining tail as garbage opcodes ("unknown opcode"). Bumped
cstage w6a to a 32K static buffer (selfhost w6a already
allocated per-line via amalloc).
lib: lib/hash/adler32, lib/hash/crc16, lib/hash/crc32 — pure
buffer-subset shape (matching lib/hash/fnv), with per-module
*_test.ww runnable via `ww test lib/hash/<name>`. Adler-32 plus
CRC-16 (CCITT/CMDA2000/DECT/ANSI) and CRC-32 (IEEE/Castagnoli/
Koopman) cover Hare's reference vectors bit-for-bit. Wired into
test/wcc/900_stdlib.c. .gitignore: lib/**/*.s,*.o so `ww test`
droppings stay untracked.
`make test` (26/26), `make bootstrap` (ww2≡ww3≡ww4), and per-module
`ww test` all pass. cgen output is byte-identical across cstage and
selfhost for every repro that previously diverged.
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.
`type nr = enum i64 { READ, WRITE, OPEN, ... }`. syscall0..4 take
`num: nr` so the wrong-arg-order trap is now a compile error
(`syscall1(0i64, ...)` no longer typechecks — it has to be
`syscall1(nr.READ, ...)`).
Internal-only (callers outside os.ww never touched the constants),
so no external API change. ABI is unchanged: nr's storage is i64
and rt_syscall's RDI is unchanged.
The selfhost combined.ww files regenerate as a side effect of
`make wwstage`.
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.
Selfhost parser (lib/ww/parse/expr.ww) recognises postfix `?` and
`!` at the same level as `as`/`is`/`:`. Selfhost cgen
(selfhost/cmd/wcc/cgenexpr.ww) emits matching code: cmp AX against
the success tag (0 in legacy mode), branch over the propagate /
abort path, then unwrap (DX → AX, CX → BX for str). Mirrors the C
cgen but without the tag-remap loop — none of the selfhost code
that uses `?` today needs cross-shape remapping.
lib/ww/lex/lex.ww \\x escape handling switched from 5-line match
blocks to one-liners: ascii.digitval(c: rune)!. Both digits are
already validated by isxdigit above; the void variant is
unreachable, so `!` collapses correctly. 995 fixed-point gate
verifies the selfhost cgen produces the same `!` codegen as C cgen.
`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.
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.