Commit Graph

30 Commits

Author SHA1 Message Date
b817e5d498 test: ww-native behavior harness (runww.ww) + 2 pilot cases
First piece of the Go-model test rebuild: runww.ww is a ww program (the
test/run.go analog) that drives compiler cases through BOTH stages and
asserts behavior/diagnostics. Directives //ww:run / //ww:run-exit N /
//ww:error "<substr>" / //ww:compile; spawn+stderr-capture lift the
driver's procrun (main.ww:160) + dup2(2). The //ww:error check requires
rc!=0 AND the diagnostic substring (the #20 non-vacuity guard -- a crash
can't pass), and a malformed error directive fails loudly.

Bulk corpus migration + ww test wiring are follow-up folds; case spawns
need os.envp() (#28) and per-pid /tmp paths (#29) first.
2026-06-21 15:28:59 +09:00
294f4c93fd w6c: set variadic-call AL to XMM-reg count, not hardcoded 0 (catB-54 C1)
SysV §3.5.7 requires a variadic call to set AL = number of vector (XMM) regs used for the variable float args; the C callee gates its xmm-save-area stores on `test %al,%al`, so the old hardcoded XORQ AX,AX (AL=0) made va_arg(double) read garbage for any float-bearing C variadic call. Emit MOVQ $fi,AX (fi = the in-scope XMM cursor, ≤8); w6a has no MOVL-immediate encoding so MOVQ is the assemblable form and sets AL=fi identically. fi==0 keeps XORQ → byte-identical to pre-fix for no-float variadic calls. Runtime test 989_ffivariadic links a cc-compiled va_arg(double) fixture (zero relocs/undefined, w6l-linkable) and sweeps N=3/5/8 floats (N=2 is vacuous via stale-stack aliasing). C1 of the C-FFI-variadic align-up (USER ruling); C2 wwstage + C3 bodiless gate follow. ref/qbe/amd64/sysv.c:384. 454 green.
2026-06-20 23:21:58 +09:00
7b6f24adea w6c,ww: mangle an imported package's fn main under separate compilation (M4 E3, #99)
The bare-`main` carve-out (which keeps the link entry's main unmangled)
keyed on `leaf == "main" && imported == 0`. Under the combined path a
dependency's body folds in with imported==1, so only the root's main
stayed bare. Under separate compilation each package is its own unit and
a dependency's body carries a path-mangling module-reset but imported==0
(#57) — so an imported `fn main` matched the carve-out, emitted a bare
`TEXT main`, and collided with the root entry (`w6l: duplicate symbol
main`). The combined path was unaffected, so this only surfaced under sep.

Gate the carve-out with sep_isdep = (wwiout != NULL): the producer emits a
.wwi output only for dependency units, never for the root/link-entry unit
(root stripped, #69), symmetric on both stages. Only the root unit's main
now stays bare; an imported package's main mangles on its import path
(e.g. aa.bb.main). Both stages.

Gate: test/wcc/989_depmain_sep.c (table-driven, dotted + single-component
shapes, both stages; asserts the mangled dep main + a single bare root
main + cs==ww byte-id; combined path stays neutral).
2026-06-18 12:47:41 +09:00
200f51ca94 ww: resolve self-named import to the dir-package, not a sibling file (M4 E3, #98)
The driver searchpath is srcd-first (srcd = the entry file's directory).
A co-located black-box test lib/<mod>/<mod>test.ww makes srcd=lib/<mod>,
so resolving `import <mod>` hit the sibling-FILE branch lib/<mod>/<mod>.ww
and folded it inline into the consumer unit under the wrong module tag
("package <mod> does not match import path <importer>") — 7 lib-run tests
fail under separate compilation. The combined amalgamator tolerated the
co-location; only sep surfaced it.

Resolve a package directory-first: walk ALL searchpath entries for a
directory match, and only fall back to a file match if no directory
exists anywhere. A dir-package now beats a same-named sibling file (fixes
the self-named shadow), while a leaf package with no directory (e.g.
lib/encoding/hex) still resolves via its file. This realizes the driver's
"a module is the directory" intent; the originally-specced per-directory
suppression was rejected because it broke leaf packages (rob-pike). Both
stages (cmd/ww/main.c + selfhost twin). The dir-beats-earlier-file
precedence change is latent and loud-failing (#101).

Move-set: ww + ww_ww (driver) only; w6c_ww/wwdump_ww/w6a_ww/w6l_ww HOLD.
Gate: test/wcc/989_coloimport_sep.c (table-driven, both stages).
2026-06-18 12:09:25 +09:00
24ca570a7e w6c,ww: re-emit ... union-spread marker in .wwi producer (M4 E3, #95)
The N_TTAGGED serializer emitted each variant via wwi_type but never
re-emitted the `...` prefix for TK_ELLIPSIS spread variants, so an
exported `(...inner | str)` round-tripped through .wwi as `(inner | str)`.
The consumer's checker then could not flatten inner's members into the
alias and variadic-assignability rejected bare members — under separate
compilation this broke fmt/log/getopt. Re-emit `...` before the variant
type, both stages; the producer stays purely syntactic (flatten/dedup
remain the consumer's type-store job, per ref/hare/hare/unparse/type.ha:290-300).

Gate: test/wcc/989_wwispread_sep.c — table-driven (2-arm + 3-arm spreads)
x both stages, asserts the marker survives the .wwi, the consumer binds
bare members under --sep (exit 0), and cs==ww .wwi byte-identity.
2026-06-18 09:52:19 +09:00
2c62be44a7 test/wcc: 989_declns_sep decl-namespace gate — dup reject / builtin redecl / dir-pkg modfn coexist (M4 E2-C2c, #22)
Final E2-C2 commit. New decl-namespace gate, both stages: dup fn/type/def/let build-FAIL with a right-reason "duplicate <kind> <name>" diag (shared substring; ww has no line:col); builtin_redecl ACCEPT exit 7 (shadowing a pre-seeded builtin is redeclarable); modfn coexist reshaped to a real directory package (696 precedent) via ww build --sep on two layouts (modfn_coexist + _vbu use-before-value) so both run to 6, cs==ww per-pkg .s/.wwi/.unit.ww, cross-order byte-id on .s/.wwi (.unit.ww excluded: verbatim source flips by construction, order-independence is codegen). __root.s carries both CALL aa.helper and CALL main.aa distinctly. Test-only; all 5 pins hold; 443 to 444.
2026-06-18 00:52:33 +09:00
939c984f51 wcc,ww: prepend synth use test; user fn run coexists with runner (M4 E2, #80)
The -T harness synthesized `use test;` after name-binding, so the lib/test runner run keyed the bare scope and collided with a user-defined bare fn run — a spurious "duplicate fn run" reject (the E1 tolerance seam). Prepending the synth use before binding keys the runner as test.run in the test module namespace, distinct from the user bare run; the two coexist. Hare-faithful: the runner is its own test module (ref/hare/test/+test.ha:97). Inverts attest_userrun.ww from the #23-mandated reject to a coexist fixture; gate asserts exactly 1 TEXT run + 1 TEXT test.run on the -T asm (distinct symbols, not a dead-dup). Closes #80.
2026-06-17 23:54:21 +09:00
b9c4562135 ww test: @test name-filter via fnmatch (both stages)
`ww test <file> <pattern>` runs only the @test fns whose names match the
fnmatch glob; no pattern runs all (byte-for-byte the pre-filter path);
zero matches prints "No tests run" and exits 0 (Hare ground truth
ref/hare/test/+test.ha:114-117). A pattern in directory mode is rejected
"ww test: pattern needs a single test file" (rc 2), identical wording in
both twins (cmd/ww/main.c do_test + selfhost/cmd/ww/main.ww dotest).

Mechanism (a): rt/start.s stashes argc/argv into rt_argc/rt_argv getters
(rt_envp twin shape, -T synth untouched so 990-997 byte-id holds);
lib/os.args() rebuilds the []str view, build-once-cached; lib/test/run.ww
imports fnmatch and filters av[1..] (argv[0] is the binary path). The
driver forwards the 2nd positional as argv[1] via fork/execv (cstage) /
procrun (wwstage) so glob metachars aren't shell-expanded.

os.args() is the first `alloc`-caller in the base os module, so os.ww now
imports rt — the `alloc` builtin's malloc lowers to rt_malloc only when
the rt binding is bundled (mirror lib/strings/strings.ww:30); without it a
plain `ww build` of any os-importing program links bare libc `malloc`
(undefined). os is bundled by ~every program, so this is load-bearing.

The lib/test floor rises os-only -> os+fnmatch+ascii+strings in every -T
build; the bundled `ascii` module vs a `@test fn ascii` collision that
exposed is closed by the preceding #30 promote commit. 989_test_filter
pins the full matrix on both twins byte-identically; 949 gains the
dir-mode reject row. (#17)
2026-06-11 04:51:17 +09:00
64f0ddf01b wcc/ww: promote module-name SK_USE to value kind (use_alias), aligned to cstage
A primary-package top-level decl whose leaf also names a bundled module
(`type sym` vs `import sym`; the lib/test->fnmatch->ascii -T floor's
`ascii` module vs a `@test fn ascii`) collided in the flat scope: #23's
installtop dup-check false-fired "duplicate fn/type" where cstage
coexists. cstage keys the module name out of the value namespace by
PROMOTING the same-leaf SK_USE in place to the value kind with
use_alias=1 (cmd/wcc/check.c:2831/2848/2871/2907/2928/2951), so one
correctly-kinded sym serves bare refs (call/structlit/var) while
`name.member` still resolves the module via the `kind==SK_USE ||
use_alias` N_DOT guards (check.c:87/1337).

This REVERSES wwstage's documented two-sym coexistence design
(lib/ww/sym.ww scopelookupuselocal): keeping the SK_USE as a separate
coexisting sym ripples into every bare-reference resolver (~25
scopelookup sites), and a missed site is a byte-id-consistent-but-wrong
cat-A risk the gate cannot prove away — the same failure mode retired
with the name-keyed variant-match cluster. The promote model is correct
by construction: one sym of the right kind, identical to cstage.

sym gains a use_alias field; the three N_DOT/N_CALL module-qualified
guards honor use_alias. cstage installs every SK_USE in a dedicated
first pass, so its value-arm promote is order-INDEPENDENT; wwstage
installs in source order, so BOTH directions of the collision are
promoted to reach cstage's identical single-sym end state:
  - use-before-value (`import aa` then `fn aa`): installtop promotes the
    pre-installed SK_USE to the value kind (use_alias=1).
  - value-before-use (`fn aa` then `import aa`): installdecl's N_USE arm
    promotes the pre-installed value sym in place (set use_alias=1,
    no coexisting SK_USE), mirroring cstage's self-import N_USE arm
    (check.c:2823-2834 `if (prev) prev->use_alias = 1`).
Both orders compile + are cs/ww byte-identical AND byte-identical to
each other. Cannot split: promote without the guards leaves
`name.member` red on the promoted sym; the guards without promote are
inert (no use_alias is ever set) — no bisect-clean intermediate. (#30)

Pins (910/997): modfn_coexist_ok (use-before-value) AND
modfn_coexist_vbu_ok (value-before-use) both accept on cstage + are
cs/ww byte-id + run to exit 6 (bare fn and qualified module both
resolve); the dup_fn row still rejects both stages (regression). fnmatch
byte-id holds. The order-dependence is exactly what regresses silently,
so both orders are pinned.
2026-06-11 04:51:07 +09:00
00b9933ee0 wcc/cgen: enum as int is a value conversion, not a tagged assertion (ww stage)
The value-passthrough arm gated on node shape (isenumexpr); a
constant-folded enum member reaches cgen as an int-literal node,
missed the arm, and fell into the tagged-union assertion path
(CMPQ $0 + JNE -> exit 1) — silent wrong code for every `enum as
int`, fnmatch's flag tests included. Gate on the stamped operand
type (tichase == TY_ENUM) at the cgtypeassert choke point, mirroring
cstage cgen.c N_TYPEASSERT; dead node-shape helpers deleted. Tagged
`as` assertion path byte-id-unmoved (control rows). Six @test pins
in attest_pass.ww, dual-stage. (#27b-team, cat-A)
2026-06-11 02:37:32 +09:00
19e9535ef4 wcc/ww: reject duplicate top-level decls, aligned to cstage
installdecl routes all four kinds (fn/type/def/let) through installtop,
which turns scopedefineinmodule's nil return into cstage's exact
"duplicate <kind> <name>" reject, keyed (name,mod) so cross-package
same-leaf decls coexist. Builtin redecls are dropped, not dup-errored:
cstage never scopes builtins (lookup_builtin first, check.c:69), so a
user redecl is dead there — wwstage mirrors via scopesamekeysym +
no-source-decl test. -T synth __wwtests installs direct, mirroring
check.c:3079. Closes the silent dup-fn hole (user fn run vs lib/test
run built a broken test binary with no diagnostic). Per-kind reject
rows + cross-package/builtin accept byte-id rows + -T collision parity
row in 910/997. (#23-team, category-A addendum closed)
2026-06-11 00:57:43 +09:00
16c83e70d3 ww test: fork-isolated record-and-continue harness (lib/test, both stages)
lib/test/run.ww: fork+wait4 runner; each @test runs in its own child,
abort/SEGV/FPE decoded from wait-status, failures recorded and the run
continues; exit = fail count. Tests are hermetic: module globals do not
persist test-to-test (fresh fork image; sanctioned divergence from
harec's shared-process __test_main, no setjmp/signal layer needed).
-T synth (both stages) emits a module-global (str,*fn() void) table +
return run(table) instead of straight-line calls. Driver twins bundle
lib/test under test mode and gain ww test -c/-o (go test -c) so the
byte-id gates diff the same artifact the real path builds. Gates
989/910/997 rewired onto it; new 911 pins record-and-continue across
all three fault classes; 949 +3 rows. (#17-team commit-2)
2026-06-11 00:08:39 +09:00
08a76cf4c8 wcc: drop @test fns from non-T builds, both stages (harec check.c:3941)
Splice @test N_FNDECLs out of the unit after the body-check passes,
mirroring harec's checked-but-not-emitted: a broken @test body still
errors loudly in non-T; @test-free units are emission-unchanged.
910/997 table rows pin keep/test x non-T/-T, head+consecutive unlink,
undef-body reject, and plain-calls-dropped loud link-fail. w6c+wwdump
combined.ww regen. (#6-team)
2026-06-10 22:20:15 +09:00
9f8df525c2 wcc/check: reject self-import, both stages (#16 ENFORCE-checker)
check-(c): a package importing itself (any spelling) is a hard error,
mirroring Go. Predicate is leaf==owner at the N_USE/installdecl seam —
sound only after the PREP commits (dotted-test renames, package-less
boundary directive). Identical wording both stages; diagnostics-only,
byte-id-neutral. 948 pins the reject in both compilers; 708's
pos_selfimp (which pinned the abolished self-import skip) converts to
neg_selfimp + new pos_crossmod preserving the param-shadow tolerance
the case existed for. Checks (a) unused and (b)/(d) name-membership
stay deferred to the multi-package arc: imports are filename-keyed
pulls, so those need import->file provenance this compiler lacks.
2026-06-10 15:18:58 +09:00
49a5173f3f wcc+ww: //ww:module-reset boundary directive; package-less files keep module "" (#16 PREP-main)
Driver twins emit the line-comment directive only before package-less
files (peekpackage==nil); both lexers tokenize it (TK_MODRESET, appended
=87 so existing token values hold) and both parsers reset curmod — a
package-less file's decls get module "" instead of inheriting the last
bundled package (the sticky-curmod leak, task #11). Withdrawn
alternative: injecting 'package main' flips non-entry symbols
bare->main-prefixed (FFI-visible, broke 764). Codegen-neutral by proof:
bare symbols preserved, both stages emit byte-identical asm for a
directive-bearing combined. Transitional until strict-package rejects
package-less files outright. Includes 737 bad-deep pin for the
PREP-peek >2048 edge + 904/toktest rows for the new token.
2026-06-10 14:07:35 +09:00
b795c320c9 wcc: -T test-mode collects @test fns + synthesizes entry, both stages (#15)
@test was parsed then dropped (no consumer); `ww test` needed a hand-written
main listing each test by hand, so adding a @test and forgetting the call
silently skipped it. -T makes the checker collect @test N_FNDECLs in source
order, loud-reject a user main, and append a synthetic
`export fn main() i32 { t0(); ...; return 0; }` at the install->body-check seam;
the existing cgfn emits it (cgen untouched) -> byte-identical by construction.
Mirrors harec's checker-side is_test placement.

Plan-9-lean reduction (user-sanctioned, reinstatable post-CSP): sequential,
abort/nonzero=fail; no setjmp isolation, no fnmatch filter, no file:line.

910/997 rewired from a regex scanner to driving `w6c -T` directly (thin trusted
drivers; the @test content stays ww), with a cross-stage byte-id assert on the
-T output. attest_userman/attest_badsig pin the user-main and bad-signature
rejects.
2026-06-10 02:01:20 +09:00
4459a49d3a wcc/cgen: #87 plain tagged-union module-global DATA + match SB-resolution (both-stage)
A PLAIN (non-alias) module-level tagged-union global SEGV'd on BOTH
stages: no static DATA was emitted (let_emit_size/letemitsize returned 0
for TY_TAGGED) so the global was never registered, and the match
scrutinee resolved it as a frame-local at offset 0 — reading saved BP as
the tag. Two sub-sites, one route (neither half ships alone — DATA
without SB-resolution still SEGVs; SB-resolution without DATA reads
nothing):

(a) DATA-emitter — a non-nullable TY_TAGGED arm emits the box that
    byte-MIRRORS a runtime LOCAL of the same type: tag word at +0 (the
    const-selected variant index via cg_tag_for_variant / taggedvariant-
    index), payload at +8, zero-padded to the union box size. int and
    str/slice literal variants are wired (str carries a DATAR ptr patch
    at +8); any other variant payload loud-stops (rule 7). emit_tagged_
    data + emittaggeddata are the per-stage twins; let_pre_intern/
    letpreintern gain the matching str-variant intern. Nullable stays 0
    so the (*T|void) one-word fold keeps the 8B scalar arm.

(b) match-scrutinee global resolution — the PLAIN-tagged twin of #78:
    a global tagged ident scrutinee LEAQs name(SB) and copies the box
    into an @match_spill slot the dispatch indexes off BP.

DATA target (mirror of the local box, verified byte-for-byte): for
(i32|str)=42 the 32B box is tag0 | 42@8 | zero-pad; for ="x" it is
tag1 | ptr0@8(DATAR _S_n) | len@16 | cap@24. cs and ww emit byte-
identical asm.

Pins (rob §3, dual-stage 910 cstage + 997 wwstage, attest_pass.ww): the
tagged global-vs-local byte-identity pin (match over the GLOBAL gives the
same arm/value as over a LOCAL — was SEGV both stages) and the str-
variant tag-1 pin, plus the #86 tuple global-vs-local lock-pin guarding
the already-correct emitter path.

929 fail_global_src graduates: a >48B tagged GLOBAL by-value arg now
resolves through the cgplaceaddr MEMORY-class arm (LEAQ g(SB) + blit)
instead of the #38b loud-stop, and runs correctly (uninit zero box ->
first variant); the row becomes a positive run pin. The struct-variant
>48B init still loud-stops via the data emitter.

The first-class-VALUE copy of a tagged ident (`let q = g`) stays a
pre-existing silent #49/#46 sibling (local and global identically),
filed separately — out of this fold's two sub-sites.
2026-06-06 12:05:56 +09:00
90479fed68 w6c+wwstage: reject untyped empty-[] alloc — require context, loud cannot-infer (#3 B', subsumes #5)
An empty `[]` carries no element type; ww gets it only from a let
annotation (the #45 retype). Both stages used to silently default the
element to u8, and in value-form positions (return / call-arg) the
lowering miscompiled — malloc(8) ignoring n, a 16B *u8|nomem where a 24B
slice was expected (#5). Now every empty alloc that isn't a
let-annotated binding fails to infer with a loud error, aligning ww DOWN
to harec (ref/harec/src/check.c:1801-1802).

Mechanism: clet / checkletassign flags the single alloc call node that a
`let x: []T =` rescues (save/restore around the init walk); the alloc
branch errors on any empty alloc that isn't that node. The #45 wide-T
retype path is kept. wwstage needs an extra not-yet-stamped guard because
resolvewalk re-types value nodes context-free after checkletassign.

Tests: negative cstage-driver 729 (table-driven: bare-let, return,
call-arg, assignment) + positive @test in attest_pass.ww exercising the
u8 and the wide-i32 (#45) paths at runtime. Both stages reject
symmetrically; byte-id verified on []u8 and []i32.
2026-06-02 18:54:35 +09:00
bec1e7d6b0 w6c+wwstage: #264 read cached operand tinfo for match-expr yield type
The wwstage checker rejected a match-bound binder used in a `yield` arm
of a match-AS-EXPRESSION (`let v = match (x) { case let p: *T => yield
*p; ... }`) with asserttyped:un/bin/index; cstage compiled it.

resolvewalk stamps the yield operand's type_ during the in-scope N_MCASE
arm walk. exprtype's N_MATCH arm then derived the match's type by
re-running exprtype on the same operand to recover a type NODE — but the
arm binder's scope is already popped, so the re-derive returned nil and
the N_UN/N_BIN/N_INDEX restamp arms overwrote the good in-scope stamp
with nil. cstage never re-runs: match_yield_type reads the operand's
cached ->type (cmd/wcc/check.c:121).

Root fix (align wwstage UP): matchyieldtype now returns a *tinfo and, at
the post-walk call, READS the operand's cached node.type_ instead of
re-running exprtype — so no operand shape can be clobbered by
construction (deref/bin/index all vanish, no per-arm guards). The
exprtype N_MATCH consumer stamps e.type_ from that tinfo directly (no
tinfofornode round-trip). The pre-walk call (checkletassign L302 /
checkretassign L303 run before the in-scope arm walk, so the operand is
nil there) keeps the nil-safe re-derive — benign and load-bearing: it
types the void-arm literal so let/return-assign has a usable node. The
re-derived node (or btype for the bare-binder idiom) is carried back via
an out-param for the assignability check and for the N_MLET/N_MASSIGN
tuple-destructure consumers (`let (a,b) = match { case let t => yield t
}`, test 945). cstage is single-pass so its else is dead; eliminating
the pre-walk call is #279.

Supersedes the narrow N_UN non-clobber guard (removed — its match
consumer is gone). @test check_match_ptr_deref extended to pin the whole
operand class (deref / bin / slice-index / deref-then-field), dual-stage
(910 + 997) with correct runtime + cs==ww byte-id. The *[N]T ptr-to-
array index variant is blocked separately by #278. Both compiler-
imported combined.ww regenerated. smoke + test-unit (242) + 994 w6c_ww
byte-id (18 corpus incl. selfhost combined.ww) green.
2026-06-02 16:22:22 +09:00
9cf1560392 wwstage: load-combine-store local-field compound assign (#227)
A compound assign (`-=`/`+=`) on a local field silently dropped the
operator in wwstage, storing the bare rhs. Two same-class sites in
cgenexpr.ww lacked the `n.op != TK_ASSIGN` load-combine-store guard that
the pointer-to-struct path already had: the local str/slice pseudo-field
fall-through (`view.len -= 1` stored 1) and the direct struct-local
scalar field (`p.x -= 4` stored 4). Both now load the field, push, eval
rhs, pop, combine (ADDQ/SUBQ), and store — mirroring cstage
cmd/w6c/cgen.c:3235-3264 and :3477-3502. cstage was already correct;
this aligns wwstage up. PLUSEQ/MINUSEQ only, matching cstage's switch.

This is the missing-SUBQ half of fmt's cs/ww divergence (fmt's
view.len-=1). The remaining match-label-counter offset is separate, so
777/780/781 stay STAGE_CS until that lands.

test/wcc/data/attest_pass.ww: @test check_local_field_compound covers
both sites (str pseudo-field + struct scalar), run by 910_at_test
(cstage) and 997_at_test_ww (wwstage); pre-fix the dropped op aborts via
the 1/0 idiom.
2026-06-01 03:52:06 +09:00
9e0816e199 cmd+selfhost+lib+test: directory-as-module enumeration in driver (#22)
Replace the cmd/ww + selfhost driver's file-walk import resolver
with true directory enumeration. `import encoding.utf8;` now finds
the lib/encoding/utf8/ directory and concatenates every *.ww file
in it (excluding *test.ww and the driver's *.combined.ww artifacts)
in byte-wise sorted order, instead of just finding the single
lib/encoding/utf8/utf8.ww file. Mirrors Hare's
hare/module/srcs.ha:183 _findsrcs minus tag handling.

Lookup order in both stages: (1) <dir>/<dot-as-slash>/ as directory
→ enumerate. (2) <dir>/<dot-as-slash>.ww as file. The legacy
<dir>/<name>/<name>.ww shape from #18's retained divergence is
dropped per rule-9 Hare-fidelity — Hare has no foo/foo.ha fallback;
a module IS the directory.

Symmetric across cstage (cmd/ww/main.c via opendir+qsort+stat) and
wwstage (selfhost/cmd/ww/main.ww via existing lib/os.getdents64 +
os.stat — no new lib/os surface needed; the rundirtests() walker
in main.ww from #18 was the model). Bootstrap ww2.s==ww3.s==ww4.s
byte-identical post-change.

Bundling justification (rule 11): strict-same-package validation is
bundled because the failure mode is dir-enum's own (a non-dir-enum
compilation unit cannot trigger mismatch across enumerated files).
The natural enforcement site is the driver — the parser can't
distinguish dir-enum concat from file-walk concat. Both stages
peek each file's first `package <name>;` line in expand_dir /
expanddir and exit(1) on mismatch with a precise error pointing
at the offending file. Hare's hare/module/srcs.ha:131 has the
same constraint via its README gate. Other half of #23 (strict
missing-package error tightening — 63 inline-source test wrappers
blocker) stays deferred per its filing.

Parser side (cmd/wcc/parse.c parseuse + lib/ww/parse/decl.ww
parseuse): n->str now carries only the LEAF identifier from a
dotted import. With the driver translating the full dotted path
to a directory walk, the checker only needs the package bareword
(last component) for the N_USE → decl disambiguation walk in
check.c's src_imports / decl_mod. Mirrors Hare's
`use encoding::utf8;` → `utf8::name` semantics
(ref/hare/hare/ast/import.ha:7).

Migration: lib/ww/sym.ww drops `import typ; import ast;`;
lib/ww/parse/parse.ww drops `import expr; import stmt; import
decl;`; lib/ww/lex/lex.ww drops `import tok;` — all sibling
imports auto-resolve via the new dir-enum when callers import the
package directory. lib/strings/, lib/encoding/utf8/utf8test.ww
migrate `import utf8;` → `import encoding.utf8;`. Makefile drops
-I lib/encoding/utf8 stopgap from wwdump_ww + w6c_ww. Seven test
wrappers (700_e2e, 966_strings_run, 970_fmt_run, 971_log_run,
972_fnmatch_run, 982_getopt_run, 990_selfhost) and 995_self_rebuild
drop the -I lib/encoding/utf8 runtime stopgap.

Tests: new 737_direnum C wrapper + test/wcc/data/direnum/ fixtures
pin (a) cross-pkg multi-file dir-enum build at runtime (both stages
must succeed) and (b) strict-same-package mismatch error (both
stages must surface "differs from" + exit non-zero). 738_module_decl
gains row 6 pinning the n_use->str leaf-only storage post-parser
change.

Retained workaround at selfhost/cmd/ww/main.ww expanddir loop:
`names[i][k]` nested-deref-then-index split into
`let nm: *u8 = names[i]; nm[k]` because wwstage cgen miscompiles
the chained form (treats inner u8 element as 8B sizeof *u8 instead
of 1B sizeof u8: extra MOVQ $8 + IMULQ on the inner index, MOVQ
instead of MOVZBQ load). Inline rule-8 WHY comment cites task #24
(wwstage cgen chained-index inner element size on **T). Two-step
form routes through the bare-pointer index path which both stages
handle byte-identically.

Class A wwstage cgen UNDER (chained-index inner element size on
**T) surfaced first time the codebase exercises the **T[i][k]
shape via enumeratedir() — corpus-coverage-blind landmine pattern,
same family as the trio (#27/#28/#31) from STATUS-5.

112/112 ok. ww2 == ww3 == ww4 byte-id holds.
2026-05-18 19:22:27 +09:00
79d9528a00 toolchain+lib+test: Go-style package/import keywords (#18)
User-mandated language redesign: source files declare their own
namespace via the new `package <name>;` keyword and pull dependencies
via `import <path>;`. Both keywords use Plan-9 `.` separator (user
override on Hare's `::` — `import encoding.utf8;`). Internal token-
kind enum values TK_MODULE=86 and TK_USE=17 kept stable for 990
wwdump byte-diff symmetry; only kwtab strings + tokname spellings
rotated. Executables (selfhost/cmd/{ww,w6c,w6a,w6l,wwdump}/main.ww)
declare `package main;` per Go convention; lib/ + selfhost/cmd/wcc/
files declare their parent-dir basename.

One-commit bundle per the brief's all-at-once directive: a per-stage
split breaks bootstrap byte-id mid-rewrite (cstage with new keyword
can't parse old `module`/`use` files and vice-versa). Body documents
the bundle per rule 11.

Two retained divergences from the user's stated ask, both filed per
rule 7 / rule 8 with inline task pointers at the deferred sites:

  Task #22 — Directory-as-module enumeration in the driver. User
  asked: "module is combination of files in directory" (golang/hare
  shape). After this commit lib/ww/{ast,sym,typ}.ww all declare
  `package ww;` but are still pulled into the compilation unit via
  explicit sibling `import` chains (sym.ww does `import ast;` etc.),
  not via dir enumeration. The cstage scaffold for true dir
  enumeration was drafted and reverted because the symmetric wwstage
  port requires a ww-side opendir/readdir wrapper around getdents64
  (~150-200 lines new ww). Inline citation at locate_import_in /
  locatein in both stages points to task #22.

  Task #23 — Parser strict missing-`package` error. The original
  brief mandated: parser errors when a .ww source omits `package
  <name>;` as its first non-comment item. Softened here to silent-
  default because 63 test wrappers (200_parse, 100_lex, 300_check,
  400_w6c, ..., the inline-source-fragment family) build ad-hoc ww
  source strings that lack `package` and the strict error cascaded
  into 60+ test failures. Migration is mechanical-sed but deferred
  so this commit ships green. Inline citation at parsefile in both
  stages points to task #23.

Node.module renamed to Node.nmod and modent.module to modent.nmod
in wwstage source — the field name `module` would collide with the
freshly-reserved TK_MODULE token. The rename is left in place as
clean separator between AST-field-name and reserved-keyword
namespaces. Cstage's n->module retained — C has no `package` or
`module` keyword.

rt/ensure.ww deliberately ships WITHOUT a package declaration so
its `export fn rt_ensure` keeps the bare linker symbol; adding
`package rt;` would mangle to `rt.rt_ensure` and break libwwrt.a
linkage. Documented at the file head.

111/111 ok (110 + new 738_module_decl sentinel). 995_self_rebuild
byte-id holds (ww2 == ww3 == ww4). All 5 frozen
selfhost/cmd/*/main.combined.ww regenerated under the new driver.
CLAUDE.md rule 5 amended with the language-layer divergence note.
2026-05-18 18:25:36 +09:00
ed94467a0c test: extend 708 with mlet/forrange/mcase shadow rows (#28)
708 pinned param + clet + self-import-skip after #19 landed; the
other 4 sites where check_module_shadow is wired (N_MLET, N_FORRANGE
single, N_FORRANGE tuple, N_MCASE) had no dedicated negative-compile
row. Worker-19's note that all 6 sites use identical call shape is
true today but unpinned by tests; a future asymmetric edit could
silently disable enforcement on one site.

Adds neg_mlet (let-tuple binder shadows), neg_forrange_single
(`for shadowmod of ...`), neg_forrange_tuple (`for (shadowmod, x)
of pairs`), neg_mcase (`case let shadowmod:`). Each errors at the
binder decl line with the canonical
"<kind> '<name>' shadows imported module '<name>'" message.

708's main now runs 8/8 (param, let, mlet, forrange_single,
forrange_tuple, mcase, pos_rename, pos_selfimp).
2026-05-16 10:47:50 +09:00
c9bbfcb6a6 cstage+selfhost+test: refuse let/param shadow of imported module (#19)
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.
2026-05-16 08:39:35 +09:00
10de5a5e0e selfhost+test: cgdot LEAQ-of-fn for module-qualified N_DOT (#12)
wwstage cgdot lacked a TY_FN branch for module-qualified N_DOT
rvalues. `let p = mod1.ping` fell through to the MOVQ/LEAQ-narrow
fallback, loading 8 prologue bytes from the fn's first instruction
instead of taking its address. Cstage cgdot already handled this
case (wired during #9, f1440bf).

Mirror cstage: gate on fnretlookup(c, fld) before the localloadop
fallback; emit `LEAQ <module>.<name>(SB), AX` via emitfnname with
the hint from lhs.str.

Extend 706_fnlabel_mangle: pos.ww now stores mod1.ping/mod2.ping
into local fn-pointer slots and dispatches through them in addition
to the existing direct calls. Expected exit 56 → 112. Regression
shape: without the new branch, MOVQ leaf(SB) loads the prologue
bytes; indirect call jumps into garbage → SIGSEGV.

ww2 == ww3 == ww4 byte-identical at the new emit.
2026-05-16 00:07:00 +09:00
f1440bf9e8 cstage+selfhost+test: mangle fn labels by module (#9)
Both stages emitted fn TEXT labels by leaf only; lib/os and lib/io
exporting the same leaves (read, write, close) collided at link.
lib/fmt + lib/log worked around with @symbol("rt_syscall") stubs.

Drop d->export from the fn skip rule in mod_collect (both stages) so
exported fns mangle as <module>.<name>. Let/def/type keep current
behavior. Skip retained for {@symbol, main, empty-module}.

Add cur_mod thread through cgfn + mod_lookup_for_fn(name, hint) at
all 4 label-emit sites (TEXT def, LEAQ N_IDENT, CALL N_IDENT, CALL
N_DOT). Wwstage mirror: emitfnname + modlookupforfn + curmod.
Invariant comment pinned in both stages.

ww2 == ww3 == ww4 byte-identical at the new label format.
706_fnlabel_mangle covers same-leaf cross-module CALL + private-leaf
cur_mod disambiguation through a fn-pointer rvalue.

Wwstage LEAQ-of-fn N_DOT (`let p = mod.fn` rvalue) is a pre-existing
gap; deferred to a follow-up. fmt/log rt_syscall stubs untouched
here; cleanup follows.
2026-05-15 23:41:01 +09:00
7f60ebbe44 cstage+test: SK_USE→SK_X promotion sets use_alias
In cmd/wcc/check.c the pass-1.5 SK_USE→SK_DEF/SK_FN/SK_VAR promotion
sites forgot to set prev->use_alias = 1 when the imported module's
top-level decl shadowed the SK_USE leaf in flat scope. Downstream
dot-prefixed lookups (resolve_typename L77, N_DOT L709) gate the
module-head walk on (SK_USE || use_alias), so `mod.flag` resolution
fell through to "unknown type". The SK_TYPE precedent at L1660 had
the line; the three sister sites at L1709/L1722/L1736 now do too,
in the same one-line shape and field-set order.

The wwstage selfhost/cmd/wcc/check.ww uses coexistence rather than
in-place promotion: SK_USE and same-leaf SK_TYPE/FN/DEF/VAR live as
separate entries differentiated by sym.mod, and scopelookupinmodule's
mod-filter already disambiguates dotted lookups — no use_alias flag
needed, so the cstage bug is structurally non-reachable there. An
architectural note at installdecl documents this divergence-by-design
and warns against porting the flag (adding a field to `sym` changes
its size and risks the wwstage cgen amalloc-undersize trap).

Audit covered every SK_USE→SK_X promotion path in check.c (4 sites:
SK_TYPE already-correct as precedent, SK_DEF/SK_FN/SK_VAR fixed). The
surfacing case was lib/fnmatch: `fn fnmatch(...)` shadows the SK_USE
leaf, so `fnmatch.flag` failed in worker-fnmatch's WIP — that test
(972_fnmatch_run) now flips PASS as live integration proof.
test/wcc/699_use_promote_alias.c pins all four rows with a single
table-driven driver (type/fn/def/var → use mod; let m: mod.flag =
mod.flag.A; return m: i32, expecting exit 42 per row). 995_self_rebuild
byte-identity holds.
2026-05-15 15:06:37 +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
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