Commit Graph

122 Commits

Author SHA1 Message Date
e59881cb30 compiler: support package test runtime aliases 2026-08-12 03:46:28 +09:00
9b970eb16a compiler: make package exports self-contained 2026-08-12 01:12:42 +09:00
db96422f74 compiler: load imports and enforce package exports 2026-08-11 22:25:59 +09:00
80fc13c828 wcc: delete parser backtracking remnants 2026-08-09 04:25:36 +09:00
c9425a3b46 wcc: delete unused warning counters and ww err twin 2026-08-09 04:19:22 +09:00
53a9e94736 wcc: reject final-band integer overflow; restore rewind columns 2026-08-09 03:25:17 +09:00
999b110001 wcc: forrange bindings get a per-loop scope; dup binders reject
The TODO(#11) silent-accept of `for (let (a, a) .. xs)` rested on a
stale premise -- resolvewalk has per-block scopes since #53 and IS
the live w6c_ww checker pass. Bindings now install in a per-LOOP
scope (the N_MCASE pattern), so a duplicate name within one pattern
errs "redeclared in same scope" (cstage parity via stamptuplebinds'
scopedefine-nil check) while sequential same-name loops stay legal.
kwtab restores its stated alphabetical order (`import` before `is`,
both stages, kinds swapped in lockstep with names).
2026-08-09 01:59:33 +09:00
411515a83b wcc: checker-seam drain — spreads, tuple aliases, yield scoping
Four seams from one review cluster, all landing on the same checker
files; gates ran on the union (rule 11 body).

?-subset: the wwstage walk compared `...spread` ALIAS nodes on both
sides of the error-subset check, falsely rejecting a spread-carried
error return (`(i64 | ...errs)`, cs-accept/ww-reject) — both sides
now flatten like trycountvariants (cstage Tparams are pre-flattened
at type level).

Multi-let/multi-assign: the tuple gates keyed on the RAW rhs kind,
rejecting a NAMED tuple alias (`type pair = (i64,i64)`; cs clean-
reject, ww asserttyped stop) — both stages chase per #99 alias
transparency, wwstage rettupleof peels the alias so cgmassign keeps
the str element's len/cap stores, and the catA massign-alias error
pin re-rules to a run fixture. The N_MLET diagnostic also printed
its operands swapped (elem/declared reversed vs its wording).

Yield: match_yield_type didn't descend into N_SWITCH, so a yield
inside a switch arm typed the match void and dropped the value
(both stages); and a stray yield outside any match arm reached cgen
unchecked — both stages now gate on a match-arm counter (the
c->loops discipline).
2026-08-09 01:51:39 +09:00
cf9d83b209 wcc: enums compare nominally; folded members keep their enum stamp
Two composing defects made a two-enum union mis-tag in wwstage
(live cs!=ww: `let e: (color|shape) = shape.BALL` stored tag 0 —
the color arm — while cstage stored 1). type_eq/typeeq had no
TY_ENUM arm, so ANY two enums fell into the primitive default and
compared equal; enums are nominal (harec: an enum IS its alias
type) and now compare by node identity only. Underneath, the
wwstage post-order revisit re-stamped the constant-folded enum
member (an N_INTLIT) as untyped_int, clobbering the enum stamp the
N_DOT fold applied, so the widen matcher fell to its first-variant
fallback -- the #59.9 N_BIN guard now twins on N_INTLIT (cstage
cexpr is single-pass and never clobbered).
2026-08-09 01:26:31 +09:00
f191e6e0e2 wcc: modulo is integer-only; compound ops carry their operand class
Hare's rule (harec check.c binarithm): % and the bitwise/shift five
are integer-only; + - * / need numeric operands. ww grouped % with
the numeric ops, and compound assigns never op-checked at all, so
`a % b` on floats compiled half-lowered (live cs!=ww divergence),
`a %= 2.0` plain-stored the rhs (op silently dropped, both stages),
and `s += "cd"` garbled str headers. Gate both at the checker, both
stages; the cgen float-compound fallbacks and the three unknown-
compound legacy defaults (deref/global/local) demote to rule-7 hard
stops. 34 compound-on-tagged/str/slice fixtures re-pin from the old
cgen "not wired" stops to the earlier checker diagnostics; 3 new
reject fixtures pin the closed shapes.
2026-08-09 00:32:41 +09:00
62b9d20383 toolchain: banner purge + WHY-only comment sweep (rule 8)
selfhost/, cmd/, internal/ join the tree-wide sweep: every section
banner dies (91 selfhost + the cmd C-style dividers -> 0); narration
and stale contracts deleted (pre-#22 bundler notes, retired
single-PT_LOAD and no-archive claims, superseded ABI tables); every
ref/harec/qbe cite, task cite, encoding/ELF contract, and rule-10
twin pointer kept; lost lifetime/rationale lines restored where the
sweep over-cut (elf_globals ownership, kwtab linear-scan). Comment-
only proven: all five wwstage tool binaries byte-identical across
the sweep; test-commit, test-byteid (161+1399, 0 pinned-divergent),
and test-bootstrap (fixed point + 991-995 byte-id) all exit 0.
The read-through banked 66 latent-bug leads (checkpoint).
2026-08-08 23:14:03 +09:00
b52f30a894 wcc: reject module-scope alloc/call let initializers at check time
A module-scope let whose rhs runs code (alloc, call — peeled through
cast/?/! wrappers) emitted no DATAW slot: emit_lets' fold-fail
silently skipped the definition and every reference died at LINK
time with 'undefined reference', the one unacceptable failure mode
(rule 7). Hare's model rejects at check time (ref/harec/src/
check.c:4360 'Unable to evaluate initializer at compile time') and
routes runtime init through @init, which ww does not have — so both
frontends now reject at the declaration with identical wording.

alias_infptr_global flips compile->error as the alloc pin (its
letvartnode N_TPTR-over-N_TSTRUCT coverage lives on in the local
alias_infptr_{nest,slicecap} siblings); callinit_global_reject pins
the call shape. Corpus pin 1741/345/21/209/1166/3482.
2026-08-08 19:15:10 +09:00
5ba410ff60 wcc: reject exported/bodyless/duplicate @test shapes
Both frontends, byte-stable fragments; -T-mode checks, so the rows
live in test/package (the fixture compile cell cannot reach -T).
2026-08-08 14:19:50 +09:00
fa4b9a134b lib: migrate every test file to the canonical *_test.ww name
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.
2026-08-08 04:29:31 +09:00
f11f1629d9 docs: dissolve PROJECT_PLAN.md; drop retired-mechanism citations
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.
2026-08-08 01:15:03 +09:00
90dc6369c9 comments: drop retired lint markers, re-cite migrated carriers
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).
2026-08-07 23:21:04 +09:00
7b9488706b parse: enforce strict-package — reject package-less files (#24a)
Flip the soft-default to a hard "missing package clause" error symmetrically in
both stages (cmd/wcc/parse.c + lib/ww/syntax/parse.ww): the first real decl of a
primary section with empty pathmod/resetmod and no seen clause is now rejected.
Closes the documented soft-default divergence (the 63-wrapper carve-out).

The gate flip can't be split from the migration it breaks, so this is one atomic
commit: ~80 test/wcc wrappers gain `package main;` via a shared wwtestpkg.h
helper, 6 data fixtures plus 17 asm-grep assertions update for the bare->main.<leaf>
root-helper mangle shift, and rt/ declares `package rt;` with @symbol pinning the
bare rt_ensure/rt_malloc linker names.

Root mangling narrows: the executable entry `main` stays bare (existing
carve-out), but root helper symbols become main.X. The #84 cluster is rewritten
to assert main.run distinct from aa.run/test.run; its cgen fix and bare machinery
are retained — still load-bearing for package-less module-reset deps. New
table-driven test 782_strict_package.c (6 rows, both stages).

Retiring //ww:module-reset is deferred to #24b: it is load-bearing (clears the
.wwi pathmod so the body's package clause asserts), not a vestige; fusing its
removal here would be a silent mismatch.

All byte-id gates green; full make test reports "all 335 tests passed".
2026-06-29 03:55:26 +09:00
5fb18876b9 check: lower an array-variable borrow into a union slice success variant (#17) 2026-06-28 10:43:34 +09:00
6b36b050d7 check: reject an array-literal borrow into a union slice success variant (#13)
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).
2026-06-28 02:44:23 +09:00
e515a08fb7 check: autodereference a single-level *fn callee before the call gate (C6a)
cstage rejected a deref-less function-pointer call `f(21)` (f: *fn...) with
"calling non-function" while wwstage accepted it and ran correctly -- a
cs!=ww divergence. Hare auto-dereferences a pointer callee to its fn type
before the call. Peel one pointer level after the named-type chase so
cstage accepts a `*fn` callee, matching wwstage and Hare.

One level only, deliberately: the #181-cgen indirect-call path lowers the
callee VALUE as the target (CALL AX), which is the fn address for a single
`*fn` but only the address-of the fn-ptr for `**fn`. A multi-level peel
would accept `**fn` past what cgen can lower -- a silent both-stage
miscompile (returns garbage). cstage stays loud on `**fn` (rule 7);
wwstage's loop-accept of all levels (check.ww:3763) is the over-permissive
side, filed for align-down plus the deeper cgen multi-level autoderef.

Surfaced by the codegen miscompile hunt (finding C6a). Pinned by
test/lang/fnptr_derefless_call_test.ww (deref-less + canonical (*f)(21),
multi-arg, alias-chain, tuple-return; reverting the peel reds the
deref-less rows at compile).
2026-06-27 14:49:36 +09:00
e7effe5f57 check: narrow untyped float literals to f32 in f32 context (#120)
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).
2026-06-27 12:34:58 +09:00
6525e137ae wwi: derive decl-less module's .wwi package leaf from parse-stamped path (#11)
wwi_emit took the .wwi `package` leaf from the first primary decl's module tag;
a fully empty primary module body (zero decls) had none, so the leaf stayed the
literal default "main" and the importer rejected it ("package main does not
match import path <leaf>"). The module identity is only available at parse time
(curmod is overwritten by imported //ww:module sections before emit), so stamp
the primary path onto the N_FILE node (TK_MODULE and TK_MODRESET rp!=NULL sites,
only-if-empty so a bare-reset `package main` root stays "main") and, when the
decl-scan finds no leaf, fall back to that stamped path. Symmetric cstage+
selfhost; both detect scan-miss via the same found-flag so the emitted .wwi
stays byte-identical.

Regression: test/wcc/989_wwileaf_run.c, table-driven over {empty body,
comment-only, nested a.b.c} decl-less shapes, non-vacuity proven.
2026-06-22 21:17:30 +09:00
c0383274d1 lex: clear pending modpath on module-reset, fix empty-module stale-modpath miscompile (#9)
BUG-A #9: when an empty/export-less inlined module body produces `//ww:module <p>` immediately followed by `//ww:module-reset` in one skipws run, lexnext drained TK_MODRESET first then emitted the STALE TK_MODPATH past the reset boundary, re-binding pathmod=<p> so the root `package main` was validated against <p> -> wrong reject. Fix clears the pending modpath at the reset-RECOGNITION site in skipws (NOT lexnext drain): cstage l->modpath=NULL after both l->modreset=1 (cmd/wcc/lex.c); selfhost l.modpathset=0 after all three l.modreset=1 (lib/ww/syntax/lex.ww). Symmetric (rule-10): identical token streams, bootstrap byte-id holds. The normal RESET-then-PATH boundary is unaffected (clear is a no-op when no path is pending). Pin: test/wcc/989_modresetadj_run.c feeds the raw composed adjacency to w6c+w6c_ww (accept + byte-id), red-path-proven (reverting a clear reddens it). A separate wwi-producer defect surfaced (decl-less .wwi defaults to 'package main') filed as #11; not touched here.
2026-06-22 19:49:57 +09:00
1f1efb273a check: gate C-style ... to bodiless decls, both stages (#11)
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.
2026-06-21 12:14:31 +09:00
027f90c572 parse,check,type,wwi: @packed struct attribute, both stages (#51)
Hare/harec @packed struct layout: no inter-field or trailing padding, align =
max field align (NOT forced to 1) — matches harec type_store.c + types.c:621
(packed{u8,u64}=size 9/align 8). Parser consumes inline @packed (loud-rejects
unknown struct attrs, both stages); layout gates padding on !packed; cstage
type_eq enforces packed type-distinctness; the .wwi producer round-trips
"struct @packed {". wwstage sets slotsize=size for packed so its composite-ABI
copy matches cstage byte-for-byte. cstage identity is faithful; wwstage identity
rides the deferred #224 nominal-resolvealias arc (#108). Both stages byte-id;
447 tests pass.
2026-06-19 00:17:49 +09:00
0197dfb9e6 lex,wwi: \u/\U unicode escapes + wide-rune .wwi round-trip, both stages (#50)
Hare-faithful \u (4 hex) / \U (8 hex) escapes; \x/\u/\U share one codepoint
path (ref/hare/hare/lex/lex.ha lex_unicode); string literals UTF-8-encode
multi-byte codepoints (cstage inline utf8enc, wwstage utf8.encoderune). The
.wwi producer rune serializer now emits \u/\U so exported wide-rune defs
round-trip (was a fatal >0xFF). Reject >0x10FFFF and surrogates with Hare-
verbatim error strings. Closes the int-cast spelling divergence (#48 RUNE_MAX).
Both stages byte-identical; 446 tests pass.
2026-06-18 22:47:53 +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
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
01b657a7ff wcc,lib/ww/syntax: resolve qualified struct-literal pkg.Type{...} (#76)
The parser folded a qualified type pkg.Type into two different node shapes by position: declaration position collapsed it into one N_TNAME (resolved via the strrchr-leaf path), but literal position left an N_DOT chain that the struct-literal typeref handoff had no resolver arm for, so pkg.Type{...} rejected with "expected type expression".

Normalize the literal-position N_DOT chain into the same source-order N_TNAME the declaration path emits, reusing the existing resolver; no new checker arm. cstage flattens at parseprimary struct-lit handoff; wwstage (no token peek) folds dots in parsepostfix and normalizes there, guarding numeric tuple components and staying in the postfix loop so trailing ops still chain. Both stages emit identical N_STRUCTLIT(N_TNAME). Prereq for qualifying wcc syntax refs (#75).
2026-06-16 22:20:42 +09:00
10d005ef58 wcc/ww: serialize aggregate exported defs as value-less .wwi prototypes (BUG-2, #70)
The .wwi (separate-compile interface) producer could not serialize an
exported def whose initializer is a struct/array literal (N_STRUCTLIT/
N_ARRLIT) — `export def f64info: floatinfo = floatinfo{...}` aborted with
"unhandled const-expr node kind 15". Such a def is a DATA-global per the
#52 model, so its value lives once in the defining package's .o; the
interface needs only the type+symbol. Emit a value-less prototype
`export def X: T;` for aggregate-initializer defs; scalar fold-eligible
defs keep their value (the importer const-folds those). The parser gains
an optional-init arm so the importer can parse the prototype — value-less
`def X: T;` is now legal in any source, symmetric with the existing
bodyless-fn prototype `fn f();` (USER ruling: unconditional; a value-less
def with no defining .o is a loud undefined-symbol error at link, never
silent). Both stages; producer + parser fold into one commit (the
producer's output is unparseable without the parser arm).

M3-tail commit-6 prerequisite #2 (surfaced by the c6 scout). The
aggregate-def-field const-fold boundary is documented inline (#71). Gate
989_sepstructdef_run proves struct+array exported defs sep-build, link,
and run via external DATA refs, cs==ww, with a value-less .wwi.
2026-06-16 18:04:54 +09:00
747475174a wcc/ww: tag sep-built dotted-path packages by full path not leaf (#57)
A separately-compiled package's primary body was emitted under a bare
`//ww:module-reset`, so its own `package <leaf>;` clause set curmod to
the leaf (e.g. utf8) while the importer spliced the .wwi under the full
`//ww:module encoding.utf8` — definer mangled `utf8.X`, importer wanted
`encoding.utf8.X`, unresolved. Thread the dotted path through the
directive: `//ww:module-reset <path>` sets curmod to the dotted path
(imported stays 0, so the root `fn main` stays bare per #32), and the
body's package clause is demoted to a leaf==last-component assertion
instead of overwriting curmod. Aligns sep-build to the M1 path-mangle
model; only the SEP emitter changes (the combined build_one arm is
untouched, so all combined byte-id gates hold). Both stages mirrored.

Commit-6 broad-soak prerequisite. Gate 989_sepdotpath_run sep-builds a
2-level dotted package and proves definer==importer qualification +
single-component non-vacuity, cs==ww.
2026-06-16 16:36:03 +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
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
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
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
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
83025b03a6 wcc: #99 alias-of-tuple — chase TY_NAMED in tuple coercion (cstage) + param spill (wwstage)
type pair = (int, int); let x: pair = (3, 4) -- an alias of a tuple
initialized from an untyped literal, and passing such a value to a fn --
was a both-stage bug, mirror-twins of the same TY_NAMED-not-chased root:

cstage CHECKER over-rejected the init (not assignable to declared pair):
type.c's tuple-assignable arm gated on the un-chased dst kind, so a
TY_NAMED alias skipped the per-element untyped->int coercion the direct
tuple path applies. Fix: chase TY_NAMED both sides (mirrors the #258
slice-borrow arm). Direct and typed-alias tuples already worked; only
alias+untyped was rejected.

wwstage CGEN dropped the second word of an alias-tuple fn-arg: the
tuple-param spill at cgendecl.ww gated on the syntactic N_TTUPLE, so an
alias param (N_TNAME) fell to the scalar path and spilled one slot ->
t.1 read frame garbage. Fix: chase the alias via aliaslookup to the
resolved N_TTUPLE and spill all its slots. cstage cgen was already
correct -- the bug was checker-only there. Converges cs==ww byte-id.

One commit: same construct, the two halves must ship together (either
alone leaves cs!=ww). test/wcc/826 (init/fn-arg/return, 2-field byte-id);
test/wcc/944 4 rows graduated err->run-correct. byte-id 990-997 8/8.
2026-06-08 23:12:06 +09:00
ebd5b8014c wcc/check: #150 inferred-type module-global Sym repoint (cstage, #18)
Every inferred-type module-global -- let n = 5; ... return n, or
let g = pt{...}; g.a -- yielded <nil> downstream in cstage: the module
N_LET pass-2 stamped d->type from the initializer but never repointed the
Sym, so later references resolved the still-unstamped Sym. The wwstage
checker already repointed correctly, so this aligns cstage UP (cstage-only;
no combined.ww / check.ww change, w6c_ww unchanged).

Mirrors the #11 [_]-array repoint. The fix is shape-agnostic (keyed on the
unstamped Sym, not the use site) -- verified across scalar/field/arg/index/
str/match/nested inferred-global shapes. Commit B of #150 (Commit A 5c37648
fixed the by-value struct-arg cgen). A pure inferred ARRAY global is now
correct in cstage but trips wwstage asserttyped -- opposite-stage, filed
#125-class. byte-id 990-997 8/8. test/wcc/823 table-driven.
2026-06-08 21:07:47 +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
c9cfa52624 wcc/check: #103/#108 inferred untyped-int defaults to int (8B), both stages
cstage type_default(TY_UNTYPED_INT) returned ty_i32 (4B): an unannotated
`let x = <v>` / `let a = [<v>,..]` silently TRUNCATED any value > 2^31
(5000000000 -> 705032704) and strode inferred arrays at 4. wwstage kept
the element raw untyped_int (size 0), which sized INCONSISTENTLY across
cgen — the array STORE strode the 8 sentinel but letslotsize under-
allocated the frame (SEGV) and cgindex strode the READ at 1. The two
stages were each wrong differently; #263 polarity: cstage was the
truncating side. int = machine word = 8B (Go-style, MEMORY
project_int_machine_word_derived_limits); Hare lowers a flexible iconst
to `int`, never a fixed i32 (ref/harec/src/types.c:835).

Fix, one root, both stages (FUSE — the cs default + the ww concrete
element must land together, else the inferred array is transient cs!=ww):
- cmd/wcc/type.c type_default(TY_UNTYPED_INT) ty_i32 -> ty_int. The
  root; stops scalar AND array truncation at source.
- cmd/wcc/check.c N_ARRLIT empty-elt fallback ty_i32 -> ty_int. Symmetric
  pair; count-0 array emits no stores, so byte-id-neutral.
- selfhost/cmd/wcc/check.ww exprtype N_ARRLIT: default the inferred
  element's untyped flavor to concrete (untyped_int->int, _float->f64,
  _str->str, _rune->rune, _bool->bool, mirror cstage type_default),
  empty-elt "i32"->"int", and stamp the synthesized N_TARRAY's .type_ so
  slotsize / elemsizeofc / letslotsize read its real [N]int size via the
  type table (rule-13) — no letslotsize special-case (SSoT).
combined.ww regen (check.ww embed): w6c + wwdump.

ken v2 corpus re-census (160 files): EXACTLY 5 rows move, ALL CONVERGE
(byte-id YES + run exit 0, none both-wrong, zero regression):
  m2_while   #108 scalar via alias-bool loop
  m8_range1  #104 for-range elem over alias [4]int
  m8_range2  #104 over 2-level alias
  m8_slice1  #103 inferred array + alias-slice init
  m8_slice2  #103 + 2-level-alias slice + re-slice
Bootstrap byte-id neutral (5 combined units w6c==w6c_ww; 0 bare inferred
arrays in selfhost). Annotated controls untouched ([4]i32 stride-4,
[4]int stride-8, byte-id). Pinned in test/wcc/813_arrlit_infer_elem_run
(the 2 direct repros incl the >2^31 truncation teeth + all 5 movers +
controls; test-unit 296).

Closes #103 (inferred-array SEGV + truncation), #108 (cstage scalar
untyped-int truncation), #104 (for-range elem alias i32-stamp), and the
m8_slice []int-init acceptance divergence.
2026-06-06 09:23:24 +09:00