Commit Graph

183 Commits

Author SHA1 Message Date
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
37c253c367 wcc,ww: @test under separate compilation (M4 E1, #79)
Make `ww test --sep` work the Hare +test way: the -T synth test-main emits a
qualified test.run, and the test package is injected as an ordinary
separately-compiled dependency instead of splicing lib/test source into a
flat unit. Additive — combined stays the default and 910/997 are untouched
(their migration is M4 E2).

- compiler synth (both stages): the -T main emits N_DOT test.run plus a
  synthetic N_USE "test"; cmd/wcc/check.c + selfhost/cmd/wcc/check.ww.
- driver (both stages): build_one_sep gains is_test, injects the test package
  as a root dep, and passes -T to the root; do_test --sep routes a single-file
  test through the sep producer; cmd/ww/main.c + selfhost/cmd/ww/main.ww.
- 989_septest_run gate: ww test --sep on both stages, run-exit + cs==ww
  byte-id of the sep .s, non-vacuous.

The synth's test.run is left ty_err by the checker in both regimes (lib/test's
run is scope-keyed under "" not "test"; cgen emits the correct CALL via run's
//ww:module test directive) — wwstage tolerates it like cstage (rule-10). The
genuine fix, module-keying run under sep so the call type-resolves, is #80.

w6c_ww/wwdump_ww/ww_ww move (their embedded source changed); w6a_ww/w6l_ww and
the combined codegen output are unchanged.
2026-06-17 07:45:46 +09:00
75a03a7d69 wcc: qualify all references to the syntax package (#75)
After the frontend consolidated into one syntax package (#74), wcc still referenced syntax symbols unqualified — residue of the old flat combined namespace, where bare refs resolved by accident. Under separate compilation Hare and Go both require the package qualifier, so those bare refs would not sep-resolve.

Qualify every wcc reference to a syntax type, function, or enum member as syntax.X across the seven syntax-importing files. Resolution-only: the resolved symbol and emitted code are unchanged, so the two combined.ww regenerate textually but all five _ww binaries hold byte-for-byte. The struct-literal sites resolve via #76. This makes w6c fully separate-compilable.
2026-06-16 22:45:17 +09:00
7a8acfb952 lib/ww,wcc: consolidate frontend into one syntax package (Go-compiler model, #74)
The ww compiler frontend was split across packages lex (lex+tok), ww
(ast+sym+typ), and parse — mirroring Hare's ref/hare/hare/{ast,lex,parse}.
That split's only payoff is third-party reuse, which ww has zero of: the
frontend is consumed by exactly one client, the wcc backend. The split's
cost is a wide cross-package export surface — every fn over a sibling
package's type must export it, and under separate compilation that
re-triggers check_exported_type, plus a phantom `import tok;` (tok lives
in package lex). Consolidate into ONE package lib/ww/syntax/, modelled on
Go's cmd/compile/internal/syntax. The 9 files move in (package syntax);
the intra-frontend mutual references become same-package; wcc and the
tool mains import syntax. No cstage C change (the C frontend mangles from
the source package clause). Internal data shapes (AST kinds, token model,
lexer/parser state) still mirror ref/hare/hare per rule 6/12 — only the
module decomposition collapses; the stdlib is untouched.

USER-approved (#74); spec .ai/rob-frontend-reorg.md (drew2 fidelity-
confirmed). Rule-6 carve-out documented in CLAUDE.md. Dissolves the tok
phantom import; collapses the intra-frontend export sprawl. Byte-id
rebaseline (lex.X/parse.X/ww.X -> syntax.X); cs==ww held. The residual
syntax->wcc export surface (10 types) + the unqualified-ref question are
separate follow-ups (#72/#75).
2026-06-16 19:56:34 +09:00
f308818b4b wcc/ww: mangle imported symbols on dotted import path (#22 M1, #32)
Switch symbol mangling from the import leaf clause to the full dotted import path for directory packages; single-file imports keep package-clause mangling (isdir-gate: imported<=>directory-import). The root build unit's fn main stays bare, every other top-level decl mangles, closing #31's duplicate-main hazard by construction (#32). Both stages, byte-identical.

Single commit, not split: the bare rename (f244af3) is red on its own because it unmasks cross-module resolution gaps that do not reproduce pre-M1, so the fixes are intrinsic to making the rename correct. Included: wwstage fnret/fnparamslookupmod map import alias->path (#199b cross-module union-variant scrutinee resolved the wrong fn's union); cstage use_path prefers the referencing module's import for an ambiguous leaf alias (sha256 crypto.math vs strconv math). Tests table-driven: 989_m1mangle_run/_sym, 989_m1union_run (gate-visible per-arm exit codes + cs==ww byte-id).
2026-06-15 17:37:18 +09:00
5ae3787cb9 wcc/ww: reject reassignment of a const binding (catB-22)
The sym carried an is_const flag (lib/ww/sym.ww) but wwstage never
set it at the let-install nor consumed it at assignment, so mutating
a `const` slipped through silently. Mirror cstage's two sites: set
is_const when n.op == TK_CONST at the local let-install (cmd/wcc/
check.c:2408) and reject an N_ASSIGN whose lhs ident resolves to an
is_const sym (cmd/wcc/check.c:1889-1896). cstage already rejected;
this aligns wwstage's w6c_ww UP. A bare `_` discard lvalue (empty
str) is skipped.

Regen w6c/wwdump combined.ww (checker embeds in both). Valid-program
codegen unchanged → cs==ww byte-id gate stays green.
2026-06-15 03:55:46 +09:00
d0adfe5aab wcc/ww: reject value-less return in a non-void fn (catB-24)
wwstage's checkretassign short-circuited on a value-less `return;`
("skip flagging for now"), so `fn f() i32 = { return; }` built and
RET'd a garbage register. Mirror cstage cmd/wcc/check.c:2428-2439:
the no-value return has type void, then run isassignable(c.fnret,
void) — void→void and void→(T|void) accept, void→i32 is a confident
reject. cstage already rejected; this aligns wwstage's w6c_ww UP.

Regen w6c/wwdump combined.ww (checker embeds in both). Valid-program
codegen unchanged → cs==ww byte-id gate stays green.
2026-06-15 03:52:16 +09:00
9fcb3be541 wcc/ww: reject mismatched integer binop operands (#26)
cstage rejects a binop whose two integer operands have different
types (e.g. int vs i32 from len()); wwstage accepted it, miscompiling
under no-implicit-promotion. Align wwstage UP: unifyarith now chases
aliases and loud-rejects an integer-type mismatch, routing the
ordered-comparison ops through the same path with the error message
threaded on `e`. Per the user's no-implicit-promotion decision.

Scope carve-outs: EQ/NEQ stay out of the reject (#34, the comparison
operators keep their own widening rule) and a rune literal is exempt
(#35, N_RUNELIT is still untyped at this point). Adds the 29-case
test/wcc/949_intbinop_mismatch.c and its Makefile wiring.
2026-06-15 01:48:17 +09:00
391ef61d42 wcc/ww: typeeqast identity fast-path for shared type nodes (#36)
wwstage's typeeqast lacked the identity short-circuit cstage type_eq
opens with (cmd/wcc/type.c:250 `if (a == b) return 1`). Enum/struct/
array type nodes are shared from their decl, so two references to the
same type resolve to one node; without the fast-path the catch-all
returns false. Exposed by #26's integer-mismatch reject, which fired
on a same-enum binop like w6l's `os.flag.WRONLY|CREATE|TRUNC` that
cstage accepts via this check. Corpus output unchanged (the w6c_ww/
wwdump_ww binaries move because check.ww regenerates combined.ww).
2026-06-15 01:46:16 +09:00
7d4feac959 wcc/ww: size/align/offset return untyped_int, not i32 (catB-9)
wwstage's size/align/offset builtins returned i32 while their node
stamp was already untyped_int -- and cstage returns ty_untyped_int
(check.c:1570/1602). The diverging return false-rejected the canonical
Hare idiom `let x: size = size(T)` in wwstage (`let: not assignable
(i32 -> size)`) where cstage accepts; sha256.ww:189 was the live
casualty, quarantined as M_WWREJECT (#59.13) in the byte-id gate.

Align wwstage up: return untyped_int at the three sites (check.ww
size/align/offset). The len / slice .len / .cap returns stay i32 --
those match cstage (check.c:1534) and are correct. cstage is unchanged.

Regenerates the w6c and wwdump combined.ww. Full 990-997 byte-id holds
(a size()-mixing comparison emits CMPQ byte-identically on both stages,
so the untyped-int widening does not perturb the asm). Table-driven 844
test: the `let x: size = size(T)` family now compiles on both stages.
2026-06-14 18:46:10 +09:00
34c1051a63 wcc/ww: reject a duplicate top-level main (F-D)
A second top-level decl named `main` (fn/let/def/type) collides with
the entry main on the single bare `main` symbol: today both lower to a
bare `main`, w6l silently accepts the duplicate, and the program links
rc=0 then segfaults (or runs wrong), in both stages. The existing
duplicate-decl rejects key on (name, module), so a cross-module
`foo.main` vs the bare entry `main` read as distinct and slip through.

Add a program-global, name-only, cross-module uniqueness check on
`main` in the checker (both stages), colocated with the duplicate-decl
rejects and counting user decls before the -T synthesized test main.
Corpus-safe: a lone `fn main` in any package stays legal (ww has no
package-main convention -- cmatrix/lisp/mandelbrot are non-main-package
entries and keep building). This converts the silent segfault to a
loud compile error and subsumes the w6l silent-dup-main case (#31);
correct package-aware mangling of a non-entry main is deferred to the
root-unit entry-detection work (#22/#32).

Regenerates the w6c and wwdump combined.ww. Table-driven 842 test:
reject rows for let/fn/def/type main (genuine cross-module import form)
plus a negative single-main corpus-safe row that must still build+run.
2026-06-14 17:05:46 +09:00
1f2bc7fc26 wcc/ww: exprtypeoftry prefers the current module's symbol
The try-operand resolution used bare lookups (ident + bare-leaf call);
a cross-module same-leaf collision mistyped the operand. Prefer the
current module. The historic 995 byte-id break attributed to this swap
was contamination from the guard-bug-carrying bundle — re-probed clean
in isolation and at the full stack. The N_DOT module-keyed leaf stays
task #51. Report item [11], lookup half.
2026-06-13 03:04:52 +09:00
51e3b8f134 wcc/ww: &fn synthesis prefers the current module's fn
The address-of-fn synthesis used a bare lookup — import order could
bind a same-leaf fn from another module, silently LEAQ-ing the wrong
function into a fn-ptr slot. Prefer the current module (mirror
check.c:410/1305). Report item #4 (loud and silent faces pinned).
2026-06-13 03:01:31 +09:00
63e837e820 wcc/ww: scopelookuptype prefers the current module's symbol
A type lookup in a bundled build resolved to the newest-installed
same-leaf symbol from ANY module; prefer the current module first
(mirror cstage sym.c:131; the prior attempt's failure was its own
u64-vs-i32 guard bug, not a deeper layer — probe-proven). Also adds
the rule-7 #58 notes at the latent varianterr/scruttype pair and
rewrites the stale deferral block to closing cites. Report item [5].
2026-06-13 02:58:09 +09:00
87367ae332 wcc/ww: match-as-expression unifies arm yields (coarse-family gate)
exprtype N_MATCH took the first arm's yield type without walking the
rest — int-vs-str arms silently produced garbage downstream. Walk all
arms: typeeq-equal accepts; a definite coarse-family mismatch
(num/str/bool via the new yieldclass classifier, unknown classes stay
lenient) rejects. Same-family non-assignable pairs remain lenient —
the documented precision residual is task #52 (needs tinfo-level
type_assignable). The wave's table-driven reject test lands here:
989_catA_f2_reject, 19 rows x 2 stages, each member pre-fix-red-proven.
2026-06-12 11:31:53 +09:00
780e680c1b wcc/ww: checktryprop flattens spread variants in the success count
The F8 multi-success gate counted a `...inner` spread as one variant,
bypassing the multi-success reject. New trycountvariants recursively
flattens spreads (the #209 recursion; cstage check.c:2160). Also
carries the rule-7 deferral cites for the adjacent task-#50/#51 holes
(exprtypeoftry lookups, desugarcallargs fn-ptr bail, checkisas) — the
attempted scopelookupprefer hardening is byte-id-blocked by the #50
curmod layer (evidence in the task).
2026-06-12 11:28:31 +09:00
6f2ed0cc57 wcc/ww: reject non-tuple multi-assign rhs (resolvewalk N_MASSIGN)
ww accepted a non-tuple rhs in a destructuring assignment that cstage
rejects (check.c:2562); the destructure then read garbage words.
2026-06-12 11:24:58 +09:00
6c380a53ea wcc/ww: reject tuple-literal arity mismatch (checktuplearrfits)
A tuple literal with the wrong arity was silently accepted and the
extra/missing elements mis-stored. Mirror cstage type.c:416 +
check.c:2386.
2026-06-12 11:21:39 +09:00
bd86fa5f46 wcc/ww: size/align intercept unconditionally; unknown type is loud
The size()/align() intercept hid behind a shadow gate with no cstage
twin — a shadowed name silently folded to zero. Intercept
unconditionally and make an unresolvable type a loud error, mirroring
cstage check.c:93/1538; the dead shadow gate is dropped.
2026-06-12 11:18:19 +09:00
353b50489f wcc/ww: binoptype/unoptype operand-kind gates (arith/bitwise/ordered/logical)
wwstage ran no operand-kind check on binary/unary operators: str+str
compiled to integer ADD on the 24B header (silent garbage). Gate each
operator class on the operand kind, mirroring cstage check.c:1186-1238
wording; the pre-existing ptr-arith arm aligns to intkindast so
ptr+untyped_int keeps compiling (byte-id-neutral, the over-reject the
self-compile gate caught). New intkindast/numkindast/boolkindast
predicates.
2026-06-12 11:14:48 +09:00
6f11763462 wcc/ww: reject non-constant array dimension (tinfofornode N_TARRAY)
arrayelen silently folded a non-const dimension to 0 — the 0-sized
slot aliased its neighbor (review item: cs loud / ww rc=0 clobber).
Mirror cstage check.c:715. Test rows land with the wave's final commit.
2026-06-12 11:11:33 +09:00
e0df2adf47 wcc/ww: tagged-union normalization at tinfofornode (never-drop, dedup, collapse, nullable fold)
wwstage computed tagged sizes/tags off the raw variant list — size()
folded wrong constants (size((*u8|void)) 16 vs 8, (i32|never) 16 vs 4)
and duplicate variants got divergent tag numbering vs cstage, while
ww's own cgen layout folded nullable but its size() didn't. Make
tinfofornode's N_TTAGGED arm the normalization SSoT mirroring cstage
resolve_type (check.c:801-882): never-drop, duplicate dedup via
structural typeeq, single-variant collapse, nullable fold on the
normalized pair; astsize/astalign delegate, and voidvariantindex reads
the normalized ti.params (cgen.c:900-911) so construct/match/void tag
readers agree. Corpus-neutral (zero-move on all combineds);
989_tagnorm_run pins the folds dual-stage, red-proven. Review items
#1/#3; residual #45 filed (AST-keyed nullable gate at global emit).
2026-06-12 07:03:45 +09:00
556a65ee86 wcc: general call-arg typecheck via assignability union, both stages
wwstage's desugarcallargs ran no general per-arg typecheck (only the
narrow #258 array-to-slice arm): any mistyped scalar call-arg silently
miscompiled (int read as a 24B slice header; the -T face was a user
const __wwtests building a garbage test binary). Route every call-arg
through the predicate union isassignable()||assignableaddrfn(),
mirroring cstage type_assignable||assignable_addrfn and the check.c:1869
diagnostic. Confident scalar/aggregate and aggregate/aggregate
kind-mismatch rejects live in shared isassignable; the concrete-to-
tagged arm is shape-matched-lenient via tagshape() (AST mirror of cgen
taggedvariantindext) so genuine variant members keep flowing while
shape-mismatched aggregates reject. Reserve __wwtests under -T in both
stages (mirror the main reservation, check.c:2996). New table-driven
989_callarg_typecheck, 31 fixtures, reject rows proven red on pre-fix
binaries.

Deferred, filed, site-commented: the assign seam rides #178->#36
(typeeqast cannot compare variadic/module-qualified fn sigs); the
same-coarse-shape same-leaf nominal collision over-accept rides #37
(#10/#66 — the distinguishing module is absent from the AST surface
isassignable operates on).
2026-06-11 20:45:02 +09:00
8d2d157a58 wcc/ww: flexible rune-const promotion at scalar seams (coercerunelit)
wwstage stamped N_RUNELIT as concrete rune (cstage: untyped_rune,
check.c:1296), so an uncast rune literal into a fitting integer slot
over-rejected where harec accepts (promote_flexible). Add coercerunelit
(mirror of coercefloatlit) at the scalar seams — let-init, return,
call-arg, index-store; coarse accept, no range check (mirror cstage
type.c:379); array-literal elements keep the range gate
(arrlit_init_fits, types.c:923). Teeth: the fnmatch.ww '\\': u8
workaround cast and getopttest's 'X': u8 index-store casts are removed
and compile green. Checker-only (no e.type_ restamp): byte-id held.
2026-06-11 20:41:20 +09:00
fa17459558 wcc/ww: exprtype stamps bare fn rvalue with its fn type, not return type
For an SK_FN sym, exprtype's N_IDENT and N_DOT arms returned decl.lhs
(the return type), so a bare fn rvalue compared as its return type —
latent silent mis-accept of mismatched-signature fn assignment (the
706/768 over-reject under any general check was the visible face).
Synthesize the N_TFN instead (mirror of assignableaddrfn's synth), and
add the N_TFN-vs-N_TFN confident-mismatch reject in isassignable.
harec: resolve_function check.c:4279-4288 (obj->type=fntype),
EXP_ACCESS check.c:341-343 (bare ident yields obj->type, no decay),
call check.c:1573, assignability types.c:1001; cstage twin
check.c:1313 + build_fn_type check.c:2611. cgen is name-keyed on fn
rvalues (never reads the stamp): zero asm delta, byte-id held.
2026-06-11 20:37:47 +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
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
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
fddd167ce8 wcc/check: A7 honest-floor tagged-subset reject closes wide→narrow miscompile (wwstage)
wwstage's tagged→tagged subset-assign arm accepted all (lenient escape), silently miscompiling implicit wide→narrow: a (int|bool|str) holding a str, assigned to (int|bool), ran the int arm and read the str pointer as int. cstage rejects loud; this escape was the lone divergence.

Replace the escape with cstage's subset walk (src ⊆ dst): a src variant is covered iff typeeqast matches (structural — []u8/nested/primitives) OR both are N_TNAME with equal leaf names. The leaf bridge covers cross-module forwards where a callee's bare inline-union variant (utf8's `done`) meets the consumer's qualified `utf8.done` — raw typeeqast can't, and the escape was masking it for every forward. Mirrors casecovers (check.ww:4136). cstage untouched (align-up); spread-bearing unions keep the escape (#199b orthogonal).

Honest floor: leaf-only defers true module identity to #10 — a callee's defining module for an inline-union return is unrecoverable at check-time (the call node is gone; #211 fnretlookupmod is cgen-only). Retained divergence = cross-module same-leaf-collision over-accept (absent from bootstrap), documented at the site and filed as #10-A7 / census cat-A.

Test 989_tagged_subset_reject is table-driven with composition-discriminating rows: []u8 subset (typeeqast-only), bare↔qualified xmod forward (leaf-only), xmod named genuine-absence (reject). 352 green; w6c unchanged, w6c_ww 4b4496b8→4b316f01.
2026-06-10 00:22:22 +09:00
758d3ec8c3 wcc/check: #14 nominal typeeqast (aliassym) closes A6 cross-module variant identity (wwstage)
typeeqast's N_TNAME arm compared variant types by SURFACE string
(streq(aa.str, bb.str)), so a cross-module type referenced bare
(oserror) vs qualified (os.oserror) mis-compared unequal -> A6:
concrete->tagged silently mis-identified the variant. Resolve each
name to its canonical type sym before comparing: keep the streq
fast-path, else aliassym(c,aa)==aliassym(c,bb) (ww's existing resolver
maps bare [#53] and qualified [#51] to the same SK_TYPE sym). AST
analog of cstage type.c:278 TY_NAMED a==b / harec types.c:579
ident_equal -- NO interning. `c` threaded into 21 typeeqast sites.
B-full Layer 1: closes A6/#14; Layer 2 (A7 tagged->tagged subset
reject) stays deferred to the #199b flatten arc. Byte-id-neutral;
graduates shlex #59.15 (989 M_WWREJECT->M_ID). test/wcc/839 pins
both-stage symmetry (discriminating teeth = 989 #59.15, per 839 doc).
2026-06-09 21:27:40 +09:00
08ccb734b2 wcc/check: reject untyped-float + nil into a tagged dst with no matching variant (wwstage align to cstage)
isassignable's untyped-float (A4) and nil (A5) arms over-accepted a source
into a tagged dst where no variant accepts it -> silent over-accept then
tag-0 miscompile; cstage louds. Reject (cerr + c.errs+=1). Part of the
isassignable tagged-dst over-accept class (#23 fixed arm3; A6/A7 deferred
to the B-full nominal arc). test/wcc/836.
2026-06-09 19:07:40 +09:00
64606de8af wcc/check: #23 reject untyped-int into nested-union variant (wwstage align to cstage)
isassignable's untyped-int arm fell through *confident=false/return true,
silently accepting an untyped int into a union whose variant is itself a
nested (non-flattened) union; ww emitted tag=0 (wrong arm) where cstage
louds. Reject unless a direct variant is numeric-or-enum (N_TENUM accept
mirrors cstage type_isnum). Faithful flatten+rebox deferred (nominal
identity, post-CSP). test/wcc/835 (new) + Makefile.
2026-06-09 17:31:34 +09:00
d1ac836fb9 wcc/check: #24-sib reject array-payload tagged-union construction, both stages
Constructing an array-typed payload into a tagged-union variant silently
dropped it (cstage MOVQ $0 -> returns 0; wwstage match-loud only). Reject
the construct when the selected variant chases to TY_ARRAY (target
TY_TAGGED, non-tagged source). tagged-struct/slice/scalar/str variants and
the array TYPE-decl stay legal. Faithful array-into-box block-store
deferred (#6). test/wcc/834 (new) + Makefile.
2026-06-09 17:30:34 +09:00
785fe342fa wcc/check: #24 reject composite-element tuple (array/struct/tuple), declared+inferred, both stages
A tuple whose element chases to TY_ARRAY/STRUCT/TUPLE (>8B) silently
miscompiled both stages: t.0[i] read segfaulted and construction dropped
the payload into the 8B slot. Reject the type at resolution (DISP-B);
faithful inline layout deferred to #60. cstage resolve_type N_TTUPLE
(declared) + N_TUPLE expr (inferred literal, was a cstage-only silent
miscompile + cs!=ww asymmetry); wwstage tinfofornode covers both.
test/wcc/832 + 941 migrated.
2026-06-09 17:29:11 +09:00
f767819b41 wcc/check: #25 reject overlong array-lit in a tuple return (wwstage) 2026-06-09 15:01:34 +09:00
89f3e58458 wcc/check: #26 recurse over-fill walk into nested tuple element (wwstage)
extracts the shared checktuplearrfits helper (also used by #25); wwstage-only checker reject-align, cstage already louds.
2026-06-09 15:00:03 +09:00
46e8354056 wcc/check: #6 stamp inferred-type array global so wwstage compiles it (was asserttyped exit 1)
An inferred-type array global -- let xs = [1,2,3]; -- hard-failed wwstage
with 'asserttyped: int' exit 1, while cstage compiled+ran it. The
array-twin of the inferred-global family (#135 inferred-float, #150-B
inferred-Sym-repoint).

exprtype's N_ARRLIT arm synthesizes the array type for an unannotated
literal but left two synthesized child nodes unstamped: the count literal
(asserttyped trips on it -> the loud failure) and the element TNAME (cgen
then drops a non-scalar element's header load -> the silent miscompile
that merely accepting on alone would introduce: an inferred str-array's
xs[1].len read 24 not 3). Both are now stamped at the synthesis site:
cn.type_ (asserttyped facet) and elt.type_ (cgen facet). Inferred int /
u8 / str / struct / multi-dim array globals + locals + args now compile
byte-identically to the explicit-typed form (== cstage).

cstage unchanged (w6c md5 unchanged); selfhost has no inferred array
globals so byte-id 990-997 8/8, no lib pin flips. test/wcc/833.
2026-06-09 02:59:52 +09:00
dfdf99ffd8 wcc/check: #20 reject overlong array-literal in a tuple element (wwstage)
An overlong array literal as a tuple element -- let t: ([2]int, i32) =
([1,2,3], 5) -- was silently accepted by wwstage; cstage loud-rejects it.
The #12+#106 over-fill coverage wired checkarrlitfits for direct-array,
slice and alias lhs positions but not the tuple-element position.

wwstage-only checker, reject-align: checkletassign gains an N_TTUPLE arm
that walks the lhs element types (llhs.list) lockstep with the rhs values
(n.rhs.list), calling the existing alias-aware checkarrlitfits per array
element (no-ops scalars, recurses nested arrays). cstage unchanged (w6c
md5 unchanged); reject-only, 990-997 8/8, no lib pin flips. test/wcc/832.

Two sibling tuple-element positions stay open (filed, not folded -- they
are reject-aligns on invalid programs, no selfhost byte-id impact): #25
tuple-RETURN overlong, #26 nested tuple-in-tuple.
2026-06-09 02:34:33 +09:00
c10fffae16 wcc/check: #12+#106 reject overlong array-literal in return/call-arg/alias positions (wwstage)
An overlong array literal (more initializers than the declared length) is
invalid -- cstage loud-rejects it everywhere -- but wwstage silently
accepted (and truncated) it in several positions; #9 wired only the decl
position. This folds the remaining three (one class: checkarrlitfits
over-fill coverage), all wwstage-only reject-align:

- #12a return   fn f() [2]int = [1,2,3]            -- silently accepted.
- #12b call-arg g([1,2,3])                         -- louded only late via cgen #271.
- #106 alias    type A=[2]int; let g: A = [1,2,3]  -- silently truncated;
  checkarrlitfits bailed on the N_TNAME alias without chasing.

Four inserts in check.ww: an alias-chase (resolvealias) at the top of
checkarrlitfits (makes all callers alias-aware), the over-fill check wired
into checkretassign (hoisted above the isassignable short-circuit) and
desugarcallargs, and the alias-let-global guard made alias-aware. cstage
unchanged (w6c md5 unchanged); reject-only, so no asm moves -- 990-997 8/8,
no lib byte-id pin flips. A 5th position (tuple-element overlong) is a
separate pre-existing hole, filed (#20). test/wcc/828 table-driven.
2026-06-09 00:17:10 +09:00
03fc7c7abe wcc/check: #14 reject def-global scalar str index (silent segfault) (both stages)
def S:str = "hi"; S[0] silently segfaulted: a def is a compile-time
constant, never materialized as DATA (unlike let), so indexing it emitted
an unbacked main.S(SB) reference -> cstage ran into frame garbage,
wwstage link-failed. str[i] itself is valid ww (a deliberate Go-like
str[i]->u8 byte-index that lib/strings compare/dup depend on), so the fix
is narrow: the N_INDEX TY_STR arm now rejects an index whose operand is a
bare SK_DEF scalar-str symbol, both stages -- 'cannot index a def-constant
str; bind it to a let'. INDEX-ONLY: len(S) and &S are already loud, and a
def's .len/.ptr field reads (the load-bearing w6l INTERP) are N_DOT, a
different arm, and stay valid.

A rule-9 WHY-comment records str[i]->u8 as a sanctioned divergence from
Hare's strings.toutf8. The full make-it-work fold (len(S)->2, S[0]->byte)
is deferred (#16). byte-id 990-997 8/8. test/wcc/821 table-driven.
2026-06-08 20:17:05 +09:00
29a2ab2a72 wcc/check: #9 reject explicit [N]=[init] over-fill incl [0] (both stages)
An explicit [N]T = [init] with more initializers than N silently
mis-compiled for N==0: the over-fill length-mismatch check was suppressed
when alen==0, because alen==0 doubles as the [_] infer-sentinel after
resolve_type collapses the two. So def/let [0]int=[1,2] silently resized
(cstage exit 2) or OOB-read/segfaulted (wwstage) instead of the loud
length-mismatch that [N]=[init>N] gets everywhere else.

The AST keeps the distinction the Type loses: [_] leaves the N_TARRAY
length-child NULL, an explicit [N] carries N_INTLIT. cstage adds an
is_infer_arr() helper, drops the alen>0 exemption at the over-fill check,
and gates the 4 infer-resize/no-init sites on is_infer_arr so an explicit
[0] flows to the over-fill -> loud. wwstage flips the one shared count
gate (checkarrlitfits) from declen>0 to arrtn.rhs!=nil, which also
dissolves a wwstage local-resize/module-OOB inconsistency.

[_] inference, [0]=[] empty, and [_]-no-init louding all preserved.
Under-long (count<N) stays out of scope (#10). byte-id 990-997 8/8.
test/wcc/820 table-driven; its one empty-[0] global row carves out
byte-id (pre-existing spurious-DATAW divergence, task #15).
2026-06-08 19:54:29 +09:00
1c87881bda wcc/check: GAP-A .cap-on-array loud-reject; .ptr-on-array ratified valid (#12)
.cap on a fixed-size array is invalid (Hare has no capacity-read; arrays
can't grow) -> both stages now loud-reject at the checker. wwstage was
silently returning frame garbage for a local array's .cap; cstage typed
it then vaguely rejected at use. Unified to one early checker reject with
an identical diagnostic both stages.

.ptr on a fixed-size array is ratified VALID: array.ptr is &A[0], a
sanctioned ww spelling divergence from Hare; see task #13. The toolchain
already relies on it in 14 backing-pointer sites. WHY-doc added at both
checker .ptr-on-array sites. The def-global .ptr cgen base-selection bug
(#11) is a separate following commit.

Valid-program asm unchanged (byte-id 990-997 8/8); w6c/w6c_ww binaries
move (checker code changed). test/wcc/817 table-driven, model 684.
2026-06-08 17:22:09 +09:00
0c5482fad0 wcc/check: #11 def [_]T length-inference — stamp the def decl path, the #7 let-twin (both stages)
def xs:[_]T=arrlit was sized 0 (no DATA emitted, garbage indexed reads) on BOTH stages, byte-id-identical: #7 wired [_] length-inference only on the let decl path, never def. cstage check.c N_DEF pass-2 infers the length from the initialiser and re-points both d->type and the SK_DEF Sym (an indexed read resolves the def through its Sym); wwstage check.ww runs inferarraylen before resolvewalk. Checker-only — cgen lays the DATA correctly once the length is stamped. w6c and wwdump combined.ww regen'd (both embed the wcc checker).

Pin: table-driven test/wcc/814_def_arr_infer_len (index reads int/u8/2d + 1-elem edge + negative build-fail), teeth-proven against a reverted inference. Filed separately, not folded (rule-11): def-global .len GAP-A (#7 cgdot twin), def str-array element DATA GAP-B (#270), [0]T-vs-[_] alen==0 conflation (pre-existing in the #7 let path too).
2026-06-08 14:50:03 +09:00
f1dcd4ecae wcc/check: #141 def-dim array as struct field — fold def in dim, shared arrayelen across 3 ww readers (both stages)
A def-dimensioned array [MAX]u8 used as a struct field was BOTH-WRONG: cstage
loud-rejected ("array length must be an integer literal"); wwstage silently
sized the dim to 0, so the next field overlapped it (frame-smash). The
reference is neither stage — it is Hare: accept + fold the def.

cstage: fold the def into the dim via eval_def_const. The fold needs def NAMES
visible when resolve_typedecl walks struct bodies, so a stub loop binds
def-name stubs (type=NULL, filled in place by the existing def loop) before
resolve_typedecl — this extends check_file's existing names-first USE+TYPEDECL
pass to DEFs; def-TYPE resolution stays in its original order, and the
kind-filtered type lookup (#225) keeps the SK_DEF stub out of type position.

wwstage: one shared arrayelen(c, rhs) (INTLIT -> uval; else evaldefconst;
else 0) routed through astsize / tinfofornode / checkarrlitfits.

Closes #13's def-dim cstage-reject half (the slice-repeat clause stays open).
Pin test/wcc/951 (5 rows incl a cross-module os.PATH_MAX dim + a ~4KB shape;
teeth = cstage loud-reject + ww frame-smash). cgen-first blocker for the
path::buffer arc (type buffer = struct{[MAX]u8, ...}).
2026-06-08 01:00:29 +09:00
d0a1e2a221 wcc/check: #133 const-expr scalar module-global — fold+stamp let-init like def, emit DATA (both stages)
A module-global let with a const-expr init (let s = 7*6) emitted NO DATA word: cstage LINK-FAILed (undefined main.s, loud), wwstage was SILENT (no DATA, MOVSXD on stale AX, exit 152). The DEF pass-2 arm already const-folds + stamps its rhs to N_INTLIT (the #88 eval_def_const/stamp_intlit machinery); the LET pass-2 arm omitted it. Mirror it: after the assignability check, fold the rhs and stamp N_INTLIT when the plain-literal fold missed AND the const-fold succeeded. The existing DATA-emit downstream then fires (DATAW 42 + load). Both stages, byte-identical. Closes the inferred const-expr global and the typed b-ii case (let s:i64=7*6, link-fail both stages) with one stamp.

Gated on genuine int-const success (the eval return value, not the out-param): str/struct/slice/call/runtime-operand rhs short-circuit before the stamp and are left untouched — never zeroed. Non-const rhs stays on its current loud route; div-by-zero stays loud. Latent in selfhost (no const-expr module globals → 990-997 byte-id unchanged).

Pin: 947 rows C1 inferred 7*6, C2 typed b-ii, C3 def-ref K*7, C4 unary-over-binop, C5 div-by-zero loud-guard; cs==ww byte-id.
2026-06-07 22:57:06 +09:00
66d69537a5 wcc/cgen: #124 cross-module &fn in a const — N_DOT reloc + checker accept (both-stage)
A cross-module `&module.fn` in a const emitted no static reloc (the
const was never defined -> w6l undefined-reference, both stages) and
wwstage's checker rejected the const fn-table. #117/#119 wired the
&fn->DATAR const-data reloc for SAME-module &fn only; charclass_map
(fold-6) needs cross-module (12x &ascii.isXXX).

cgen: add the N_DOT arm to the &fn->symbol helper (node_fnptr_sym /
nodefnptr + the two ww emit sites), emitting mafn(leaf, module-ident)
-- exactly the symbol a runtime &mod.fn or a direct cross-module call
already emits. The helper is the SSoT for both the scalar (#119) and
tuple-row (#117) const-data paths, so one arm closes both.

checker: type a cross-module `&mod.fn` as `*fn(...)` in the TK_AMP arm
(the N_DOT twin of #206's N_IDENT fn-ptr synthesis, gated on a resolved
SK_FN/N_FNDECL leaf), so isassignable affirmatively accepts the const
table -- aligning wwstage UP to cstage's actual acceptance reason
rather than by abdication. The SK_FN gate keeps a non-fn `&mod.var`
from synthesizing a fn type (the one pre-existing nonfn-scalar cs!=ww
slip is N_IDENT-base, untouched and reproduces same-module).

One consumer-coupled commit (the checker accept gates wwstage cgen, so
neither half is independently testable). Narrow: slice-row + scalar
only; fixed-array (#118) and struct-field (#129) stay separate. Both
stages emit the correct cross-module symbols at the right tuple-slot
offsets -> byte-identical (990-997 green). Pin 949_xmod_fnptr_const_run
(distinct fns so a wrong reloc is caught + the SK_FN-gate axis). This
was the last fold-6 cgen blocker; charclass_map is now unblocked.
2026-06-06 23:34:46 +09:00
da30f10f70 wcc_ww/check: #47 gap-B tuple-with-tagged case-arm variant-match (align-up)
wwstage's checker rejected a `case let t: ((void|size),(void|size),
size) =>` arm against a (tuple|error) scrutinee ("case: not a variant
of scrutinee"), while cstage accepts and runs it. typeeqast's N_TTUPLE
arm recurses per-element, but a tagged element (void|size) is
N_TTAGGED -> fell to the conservative catch-all `return false`, so the
whole tuple-compare failed. typeeqast is the sole acceptance route
(casevariantpairmatch is N_TNAME-only).

Add an N_TTAGGED arm to typeeqast, sibling of N_TTUPLE, mirroring
cstage type.c:288-300 (type_eq TY_TAGGED): position-by-position
variant compare over the tagged node's .list (direct nodes, not
.lhs-wrapped). cstage's nullable-flag check is deliberately not ported
(resolved-Type property, no ww AST analogue; moot for case-match).

A spread variant (TK_ELLIPSIS) in the .list is loud-rejected rather
than compared: a naive streq would silently accept a `...ab` case that
cstage rejects (a new cs!=ww over-accept the bare arm introduced).
Flattening the spread is deferred (#115); until then it louds, matching
cstage.

ww-only (cstage already accepts); the gap-A cgen store landed in
6a5bb3e. wwstage now accepts the b1c match and runs the full shape
byte-identical to cstage -> #47 (both gaps) closed. The deferred
full-b1c row in 944_tuple_tagged_union_run is promoted to a both-stage
runtime row. Checker change is acceptance-only/additive -> bootstrap
byte-id neutral (990-997 green).
2026-06-06 17:36:10 +09:00