25 renames (git mv, content untouched). _test.ww is what the package
coordinator's test detection and the sep loader's canonical exclusion
key on; the old *test.ww spellings survived only through the
line-leading-@test compatibility scan. Consumers updated in place:
LIBRARY_TESTS, the libbyteid roster, the 901/974/975/976 carriers that
copy or invoke these files, and the check.c/check.ww + path/ftos
comments that cite them. Closes the open-driver-work migration bullet.
installparams normalizes a decl's `T...` param lhs to []T in place
(cstage instead sets the resolved tp->type and never mutates AST), so
typeeqast compared a decl-synthesized fn type's []i64 against a fn
TYPE expr's surface i64 and confidently rejected
`let f: fn(args: i64...) void = sum;` — cstage accepts and runs. The
normalization wrapper now carries an op marker and typeeqast peels
exactly it, so both sides compare at the declared element type without
admitting genuinely different element depths. Fixture fnptrlet_variadic;
corpus pin 1486/2972.
The last frontend-gap pin: wwstage had no Hare struct embedding
(struct { hash.hash, ... }), rejecting lib/crypto/sha256 at parse.
- parse.ww: the three member forms (named / anonymous struct / bare
dotted-ident embed), consume-then-branch since this parser has no
peek; embeds carry f.str == "" and the type in f.lhs.
- check.ww N_TSTRUCT flatten: promote the inner struct's flattened
fields at base+src.offset (check.c:961-990); the embed is one
nested-struct unit in the slot ladder; the resolved inner AST is
planted on the TFIELD rhs for cgen.
- check.ww walkers: astoffset / exprtype N_DOT / #251 struct-lit
field lookups descend embeds through shared helpers; the collision
and non-struct-embed rejects live in validatestructfields (the
once-per-decl diagnostic site).
- cgenutil.ww registerstruct: regfieldrun walks the AST against the
flattened tfield cursor, descending embeds via the planted inner
AST so promoted fieldinfo entries keep the inner field's own name
and type node.
- wwi printers unchanged (both stages already emit nameless fields).
sha256_test compiles byte-identically end to end and its 6 tests
pass; 989_lib_byteid is now 44 id / 0 divergent / 0 wwreject.
Fixtures r5913_* (promoted rw, offset shift, anonymous embed,
two-level embed + promoted fn-ptr callee, three rejects); corpus pin
1485/2970.
PROJECT_PLAN.md was a historical disclaimer wrapping five still-open
T1 driver items — those move to docs/test-system-v2.md (Open driver
work); the rest is dated 2026-08-05 design history, archived in git.
Also retired-mechanism cleanup: both drivers drop the *.combined.ww
enumeration skip (the amalgamator is gone; leftover debris now fails
loudly instead of being silently accommodated), the consumer-less
m4_combined_epoch.md5 pin is deleted, and the wwi.ww/ww.h/check.ww
comments that still described combined.ww as the live path are
re-cited to the sep reality.
A TYPE name used as a VALUE (an error-singleton `return too_long;`)
stamped the flattened BODY type, so structurally identical !void
singletons in one union were indistinguishable and flatvariantidxt
loud-rejected the ambiguity — the real cause of the lib/path wwstage
reject (the pinned #120/#29 global-slice-const blame was stale; that
family had drained). The N_IDENT arm resolves through a synthesized
TNAME (the #66 N_STRUCTLIT precedent); the module-qualified N_DOT twin
reads the sym's cached NAMED. Graduates the path M_WWREJECT pin (#142).
An enum OR-fold under `as` ((m.A|m.B) as u32) was never typed:
scruttype resolves IDENT/DOT only, and the post-order restamp then
re-derived the folded member literals as untyped_int. The unstamped
operand missed cgtypeassert's #27b enum-reinterpret gate and lowered
as a phantom tagged assert — unconditional exit 1 at runtime on the
wwstage leg. checkisas now types an N_BIN lhs and the N_BIN restamp
preserves an existing enum stamp. Graduates the #59.9 stattest pin.
The N_TFN arm stamped only size and return, so every fn tinfo carried
a nil param chain and typeeq judged any two same-return fn types
equal: tagged-union dedup collapsed (*fn(A) T | *fn(B) T) to a bare
8-byte pointer and match read the pointer word as a tag, falling
through every arm for a real second-variant value. Build the tparam
chain like cstage's N_TFN resolve (bare ... sets the FFI variadic
flag; a Hare T... param wraps to []T with a per-param variadic bit
that typeeq now compares, mirroring cstage type_eq). Graduates the
four r76_typeeq_fn pins; the DATABYTEID_DIVERGED ledger is empty.
cstage types the binding ty_u8; the wwstage single-binder arm peeled
the element only for slice/array iterables, so a str scrutinee fell to
the N_FORRANGE fallback decl and exprtype stamped the binding str.
The stamp-keyed call-arg marshal then pushed the 3-word str ABI for a
1-word scalar, and an uncast compare on the binding was falsely
rejected. Route str through the same synthetic-N_LET binder with a u8
element. Graduates r940_str_forrange_arg.
peel-ok/sizelint-ok/primsize-ok annotations lose their tools; sites
keep the WHY in plain words. Citations of retired carriers move to
their fixture or @test successors (949_errtype_compare -> r949_*,
900_stdlib -> library owners).
wwstage exprtype's N_CALL fn-value arm peeled ALL pointer levels before the
TY_FN gate, so a deref-less `pf(21)` where pf:**fn type-checked and lowered --
then segfaulted at runtime (a silent miscompile). cstage peels exactly one
level and loud-rejects (the C6a discipline, check.c:1947).
Align wwstage DOWN: peel one level (loop -> if); a remaining non-TY_FN callee
hits a loud "calling non-function" reject mirroring cstage's message. Both
stages now reject the exotic deref-less `**fn`/`***fn` shape; the legitimate
`(*pf)(21)` and one-level deref-less `f(21)` (f:*fn) still compile + run.
Multi-level autoderef is a separate deferred FEATURE, not a miscompile to lower
(rule 7/10 -- align the richer stage down to the leaner, no value ships).
ww-only change (cstage is the correct oracle); a reject emits no asm, so the
byte-id baselines and LANGBYTEID floor are unchanged.
Pins: cfail test/wcc/data/fnptr_pp_derefless_reject (both stages reject,
reddens-on-revert -- the silent miscompile resurfaces if the fix is reverted) +
test/lang/fnptr_derefless_call_test (positive guard (*pf)(21)==42 and one-level
f(21), value-asserted + byte-id, so the fix does not over-reject the legitimate
one-level autoderef).
A stack array literal returned into a tagged-union slice success variant (fn mk() ([]i32|e) = { return [10,20,30]; }) slipped past reject_arrlit_borrow — it bailed when the dst was not TY_SLICE, but a union dst is TY_TAGGED — so cgen built an all-zeros slice header: a silent both-stage miscompile (and the .ptr would dangle anyway, no outliving backing). Extend the reject to chase a TY_TAGGED dst to its slice success variant, then apply the existing reject; this is the #25/#31 treatment seen through the union, and matches Hare (rule-9). Reached by all acceptance sites (return/assign/call-arg) so the class closes by construction. Both stages converge on an identical accept/reject decision (an array literal assignable to a union is assignable to a slice or array variant; neither stage can accept it). Full support — promoting the literal to an outliving backing — is the separate #33 arc. Compile-error fixture + a positive over-reach guard (a real slice into the union still compiles). No asm emitted by a reject, so byte-id is unchanged (no floor ratchet).
An untyped float literal defaults to f64, so in an f32 context it was
materialized as f64 then bit-truncated by a raw MOVSS (low-32 reinterpret)
rather than narrowed -- e.g. `let x: f32 = 2.0f32; x * 3.0` multiplied by
0.0f. Twelve byte-id-gate-blind both-wrong miscompiles, all this one cause
(compare, binop, call-arg, struct-field, array-elem against an untyped
literal).
Broaden coerce_floatlit to stamp the untyped fconst type_=f32 across the
f32-context sites (assign rhs, call-arg, struct-field, array-elem) and to
descend the implicit-cast shapes (peel unary +/-/cast, recurse binop
operands AND the binop node, recurse arrlit elems), mirroring harec's
lower_implicit_cast. The existing CVTSD2SS gate then fires; cgen is
unchanged. f64 contexts are untouched -- the stamp is gated on TY_F32.
Surfaced by the float codegen sub-hunt (= the deferred #120). Pinned by
test/lang/f32_untyped_narrow_test.ww (22 value-asserting rows incl. f64
controls; reddens on revert).
wwstage rejected a module-level `const/let []T = [arrlit]` global with "let: not assignable"; cstage accepts (textbook Hare, ref/hare/path/stack.ha:30). The arrlit->slice admission in checkletassign was gated local-only; lift it to module scope too, aligning wwstage UP to cstage's arrlit_init_fits (check.c:3406-3409, slice arm 519-520). cstage unchanged.
Two guards the un-gating requires: the n.rhs.lhs=arr stash stays local-only (a module decl keeps its raw N_ARRLIT for DATA emit, so stashing would leave an untyped count node for the pass-3 asserttyped walker); and tuple-element slice globals are excluded at module scope, because the synthesis delegates element checks to isassignable which lacks a strict tuple arm (#38) -- a [](str,*fn) table would over-accept a sig-mismatched &fn that cstage's strict type_assignable rejects (#124) -- so they stay on the existing typeeqast path.
Closes two divergences 944_alias_emit_b7 pinned: Group A (cstage-runs/ww-rejects) migrates to test/lang/slice_global_arg_test.ww (promoted from _runonly, now cs==ww byte-id); Group B converges to a shared emit_slice_data reject with the identical diagnostic.
The selfhost checker's resolvewalk had no loop-nesting guard and no
N_BREAK/N_CONTINUE arm, so `break`/`continue` outside any loop fell through
the generic child recursion and was silently accepted -- while cstage
(cmd/wcc/check.c) correctly rejects them. A cs!=ww checker divergence
(rule 10); cstage is correct (break/continue outside a loop is an error in
Hare/C/Go), so align wwstage DOWN, not cstage up.
Mirror cstage's mechanism exactly (check.c:598/2494/2529/2611): a `loops`
counter incremented around for and for-range bodies -- the for-`else` and
the init/cond/post walked OUTSIDE the count, since a break there targets an
enclosing loop -- rejecting break/continue when loops==0 with a
byte-identical `file:line:col: error: <kw> outside loop` diagnostic.
match/switch are not loop targets, matching cstage.
The divergence survived because 300_check.c only exercised the in-process C
checker, never w6c_ww; the fix adds 4 rows to the both-stage
989_catA_f2_reject carrier (break/continue outside loop, the for-else
els-outside-count edge, and an in-loop control). make clean && make test:
all 402 passed, byte-id self-compile gates 990-996 green.
A bodied fn with a bare C-style `...` was silently accepted by cstage
and SEGFAULTED wwstage (resolvefnbody walked a typeless `...` param).
Gate it: bare C-`...` is allowed only on bodiless decls (extern /
@symbol prototypes), the real FFI path; Hare-style `T...` is unaffected.
ww restricts C-`...` to bodiless decls pending vastart/vaarg/vaend
builtins (#16); harec permits bodied C-variadic fns (check.c:3656) -- a
documented divergence, reopened when #16 lands.
Test 852 runs both stages; its reject rows require the gate's diagnostic
(not merely a nonzero exit), so a crash can't pass them vacuously.
Mirror cstage's C-variadic call handling in the ww self-host: parse a
bare `...` param (decl.ww), skip param-keyed desugar for it to avoid a
nil-deref (check.ww), and emit AL = XMM-reg count plus CVTSS2SD
promotion of f32 args in the variadic tail (cgenutil.ww, cgenexpr.ww).
Closes the cat-A wwstage silent miscompile (AL=0, unpromoted f32 tail).
Parse/check/cgen are one atomic align-up (parse alone miscompiles, so
not bisect-splittable). 989_ffivariadic now runs dual-stage (cstage ww
+ wwstage ww_ww), 12/12; w6c==w6c_ww byte-identical. Byte-id alone is
blind here (the bootstrap calls no float-bearing C variadic), so the
ww_ww runtime rows are the real net.
Mirrors cstage check.c:1491-1495 (type_isint via the syntax.typeisint tinfo chaser, which chases TY_NAMED.under/TY_ENUM.sub — not the AST-keyed isinttypeast that would falsely reject an alias-int index). Record-and-continue, before the base-bail. Reject path emits no asm so cstage==wwstage byte-id holds (453 green). Pre-existing index double-emit deferred (#6).
The wwstage checker silently accepted enums with a non-integer storage type, duplicate members, or a non-constant member value; cstage already rejects all three (cmd/wcc/check.c:1000-1042). Add validateenummembers, a pure read-only diagnostic dispatched once per enum decl from resolvewalk's N_TENUM arm (check.ww:791, beside stampenumvals -- not the per-query recompute arms), mirroring the catB-7/14 validatestructfields pattern. Storage gate uses typeisint on the resolved tinfo (the exact type_isint mirror: chases TY_NAMED.under and TY_ENUM.sub, so an int-alias storage is accepted; raw-AST isinttypeast would not). Duplicate members: O(n^2) name walk. Unfoldable values reuse enumvalfold with until=member (forward-only). Emits via cerr + c.errs, no mutation, so valid-program codegen is unchanged and cstage==wwstage byte-id holds.
wwstage's value-fold message is intentionally generic where cstage's eval_enum_value gives per-reason text (enumvalfold returns a bool, not a reason); both reject. Documented at the site, filed as follow-up task #10.
Test: new table-driven both-stage reject test 850_enum_reject (non-int storage / duplicate member / forward-ref rows + a distinct-member control whose backward-ref value builds and runs). Full make test: 452 green incl. 990-997 byte-id.
The wwstage checker silently accepted a struct with repeated field names; cstage already rejects it (cmd/wcc/check.c:925-947). Add validatestructfields, dispatched once per struct decl from resolvewalk's eager type-decl arm (check.ww:792, sibling to the N_TENUM stampenumvals fire): a pure read-only O(n^2) named-field dup walk that emits "duplicate field 'X'" via cerr + c.errs, with no mutation -- valid-program codegen is unchanged so cstage==wwstage byte-id holds. Named fields only; ww has no struct embedding, so cstage's embed-collision arm is intentionally not ported (separate parser gap, catB-89).
Test: new table-driven both-stage reject test 849_dupfield_reject (adjacent / non-adjacent / different-type dup rows + a distinct-field control that builds and runs). Full make test: 451 green incl. 990-997 byte-id.
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.
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.
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.
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).
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).
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.
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.
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.
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).
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.
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.
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.
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).
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].
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.
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).
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.
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.
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.
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).
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).
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.
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.
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.
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)