Commit Graph

89 Commits

Author SHA1 Message Date
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
4b118fa8f8 cgen: B7 emitter elem chases + tools/peellint gate — #5 alias-arc cs side closed by construction
The last four raw `->under` reads outside the whitelist were the
static-DATA emitters' ELEMENT-type single peels (the outer type already
chased): emit_array_lit_bytes:14356, emit_strarray_data:14574,
emit_slice_data:14788, let_pre_intern:15088 -> type_chase_named.
:15088 is the :14574 row's label-order leg and must flip in the same
commit or _S_ labels intern in emit order, not decl order (the in-tree
comment at the site); the strarr row's byte-id is the coupling proof.

Behavior moves (ken B7 first-position oracle + impl pre-state, all
pre-observed at 05f7af7):
- [N]alias-struct + [N]alias-str globals graduate cs link-ERR
  ("undefined reference") -> 0/0 BYTE-ID (cs emits ww's DATAW).
- zero-consumer latent silence closed: a never-referenced
  2-level-elem-alias global silently lacked DATA (no reference, no
  link error); now emits, pinned by the byte-id cell.
- []alias-str diagnostic routing: the alias escaped the 3-way
  slice-of-{str,slice,tagged} fatal onto the downstream "not a
  foldable constant" text — now the intended 3-way text (== control).
- []alias-tagged DESIGNED NARROWING: the alias dodged the 3-way fatal
  ENTIRELY — cs silently accepted + RAN WRONG for reachable consumer
  shapes (review-verified at base: a len+payload-read probe exits 1;
  the len-only row was luck-correct). Now loud with the 3-way text;
  widen what the gate SEES, never what it ACCEPTS (B6-c2 precedent).
- kb7_slc/slc0 scalar legs byte-NEUTRAL (the synthesized-array
  choke-point already handled them); full kb corpus sweep: movers are
  exactly the two graduation shapes, nothing else.

tools/peellint (sizelint clone, dep of test/test-unit): character-scan
strips comments and string/char literals, then matches the under-token
accessor-spelling-wide — `->under`/`.under` in C (deref-dot is the
same peel), `.under` in ww, optional whitespace after the operator,
and the line-split continuation (operator at EOL, `under` next line).
Scope cmd/wcc + cmd/w6c + selfhost/cmd/wcc + lib/ww (lib/ww/typ.ww
ruled IN — it is type.c's ww mirror, the accessor layer itself);
`peel-ok`/`peellint-ok` annotations exempt a 10-line window. Green at
this tip = zero unwhitelisted raw peels survive; the gate lands in the
commit that deletes the last raw read (the-funnel-completing-commit-
carries-the-gate; sizelint precedent). Whitelist, 27 entries:
  cmd/wcc/type.c    :78 :141 construction, :162 chase body,
                    :180 :193 :214 recursive chase
  cmd/wcc/check.c   :102 :2572 resolve-state probes, :2586 construction
  cmd/w6c/cgen.c    :731 probe-cleared scan peel (B5-c1),
                    :813/:814 :834/:835 peel-ok #218 variant-match
  lib/ww/typ.ww     :316 construction, :374 :385 :410 :437 :447 :463
                    :475 :488 :514 recursive chase
  selfhost/cmd/wcc/cgenutil.ww :1302 chase body (tichase),
                    :2759 probe-cleared peel
  selfhost/cmd/wcc/check.ww    :1815 construction (peellint-ok)

Negative validation wired into 944_peellint_gate (B4 precedent):
re-introduced raw peel (C and ww spellings) REDS the lint; corrupted
annotation (peel-okk-…, token-bounded matcher) REDS the lint; the
check.ww:3683 "io.underread" prose, a code read of a longer field, and
comment-quoted tokens are pinned green regression rows; real tree must
lint clean. 944_alias_emit_b7_run pins all four emit paths
table-driven (14 rows / 36 checks) incl. ken's ww observation cells
(ww checker rejects slice-literal globals, "let: not assignable" —
unmoved; plain []str louds at ww's own emitslicedata 3-way, pinned by
the shared needle).

REVIEW AMENDMENT (reviewer-B7, fix-what-you-find): the frozen tip's
regex matcher passed five compiling evasion spellings green — `t ->
under` spacing, `t->`/EOL + `under` next-line (both stages; ww parses
`t.`/EOL too), C deref-dot `(*t).under`, ww `t. under`, and a string
literal containing a block-comment opener that blinded the regex
comment-strip for the rest of the file. The matcher is now a
character scan (comments + string/char literals stripped before
matching) with the widened token rule above; all six spellings are
pinned RED rows in 944_peellint_gate (checks 10 -> 16). The 10-line
annotation window stays as designed (a peel within an annotation's
window is exempt by construction — the window IS the exemption
mechanism). Lint + test bytes only; zero compiler-source bytes moved
in review.

What this does NOT close, said out loud (f2-ruling): a consumer that
never spells `under` at all — a switch on t->kind that simply never
peels — has no token for the lint to see. The accessor+lint closes the
WRONG-PEEL class (single-peel where chase was needed) by construction;
the NO-PEEL class is closed only at sites where classification routes
through the internalized chasing helpers, and contained elsewhere by
the acceptance-commit-carries-tripwires doctrine, which stays standing
for every future acceptance widening. The gate does not make alias
bugs impossible; it makes the four-times-burned shape unwritable.

Rule-11 note: forced fuse — the four conversions ARE the last raw-read
deletions; peellint cannot be green one commit earlier (consumer-graph
-forces-the-fuse precedent, #61).

Invariants: cs asm byte-NEUTRAL on the whole bootstrap corpus (five
mains + smoke, base-input pre==post); five mains cs==ww byte-id at
tip; _ww binary quartet bit-identical to the W2 baseline (ww changes
are comment-only annotation bytes — codegen-inert, proven by the md5
hold); w6c_ww+wwdump main.combined.ww regen'd via make, idempotent;
989 lib ratchet zero flips (31 byte-id / 9 pinned-divergent / 3
pinned-wwreject across 43 units); sizelint 0; peellint 0;
make test-unit "all 294 tests passed" (292 + the two new suites).
2026-06-06 06:05:46 +09:00
9bd0d8bc81 wcc: #5 F1 promote type_chase_named + transitive-peel acceptance align-cs-up
Promote type_chase_named from cmd/w6c/cgen.c (static) to cmd/wcc/type.c
(exported via ww.h) and re-route every checker single-NAMED-peel through
it: check.c's ~28 inline ternaries + 3 ad-hoc loops, type.c's
assignability/untyped/borrow/opaque peels. type_eq's nominal identity
(check.c:114) and the resolve machinery guards stay untouched.

The re-route IS the acceptance align-up — cstage loud-rejected alias
shapes wwstage accepts AND runs Hare-right (F0 census, harec dealiases
at every consumer):
- #54 binop alias-vs-base: unify_arith gains the harec type_promote arm
  (ref/harec/src/check.c:1083-1105) — one-sided alias + dealias-equal
  promotes to the ALIAS side; alias-vs-alias stays rejected.
- alias-cond family: if/for/&&/||/! chase-then-bool (harec
  check.c:2141/2515/3229/3572). assert stays loud (F0 2a symmetric).
- #70 field access through 2-level alias chains (ken c3_chain3).
- assignability through the full chain (harec types.c:989-996
  dealias-both): return/init/assign legs, F0 8b idx/slice walls.
- alias-of-ptr deref (harec types.c:19-22 type_dereference).

The widening reaches cgen arms whose own single peels then misbehaved —
both classes are closed IN THIS COMMIT so no intermediate state ships a
loud->silent flip (bisect no-silent invariant):
- index family: the 8b acceptance hit ptr-load base + esz=1 (SEGV /
  prefix-luck) — idx_eff + the N_INDEX read / index-write / &base[i] /
  N_SLICE (expr + call-arg) / N_FORRANGE / aggarg_srcaddr-index /
  castsrcprim-dot / match-field base classifies chase.
- kind classifiers (ken #61-root-verify v3 find): a 2-level f64 alias
  param reached cg_isfloat's single peel and classified INT — silent
  wrong-register-class. cg_isfloat / type_isf32 / fld_isfloat /
  type_isstr / type_isslice chase. ken's v3 row is pinned with credit.

Bootstrap asm is byte-identical before/after (w6c on every
main.combined.ww cmp-equal vs a pristine 738d7f4 scratch; 989
lib_byteid pins unchanged): 2-level chains were checker-walled pre-F1,
so no previously-accepted program changes shape.

test: 944_alias_accept_run (20 rows): acceptance graduations pinned
runtime + byte-id both stages; idx/slice/range/slice-param rows cs-only
until the wwstage #60 esz family lands (F2 batch 1); cs-only
harec-parity loud pin for alias-vs-alias binop; assert stays-loud row;
ken-v3 + f64/str/slice kind rows. Mutation-checked at 738d7f4.

reviewer-F1 fold — the same invariant, outside the F0 census: this
commit ADMITS 2+-level alias slice/str/aggregate types in STRUCT FIELD
position, therefore this commit must keep them correct-or-loud. The
cgen FIELD-TYPE gates single-peeled, so the slice/str 3-word arms fell
to word0-only scalar tails — accept-and-corrupt, ww correct, every
shape loud at the pristine base. Chased (probe-proven, byte-id
graduations): single-dot field store + via-ptr twin, struct-lit fill,
chained store-walk LEAF (the #71 walk chases hops, not leaves),
chained-ptr-field store, single-dot / via-ptr / chained-walk field
reads (clobber-probed — word0 reads luck-passed on stale BX/CX). The
six unprobed sibling gates (indexed-elem store/read, ptr-chain read,
heap fill, tuple-elem read, static emit) hard-error via
fld_alias_tripwire on a 2+-level alias over an aggregate base, citing
task #73 (the family's scheduled chase); <=1-level and scalar bases
never fire — zero behavior change for any pre-#5-legal program (five
selfhost mains cmp-identical vs the pristine 738d7f4 scratch).
test: 944 +11 rows (9 K_RUN byte-id, wholeread K_RUN_CS [#60 ww half +
pre-existing 1-level read-spine divergence], #73 tripwire
K_BUILDERR_CS pin); 1-level controls per gate in /tmp/revF1.
2026-06-05 18:33:47 +09:00
738d7f481c wcc/check: #62 typedecl layout is decl-order-INDEPENDENT — demand-resolve forward refs + loud cycle guard (#69)
check_file resolved typedecl bodies in file order with an eager
under->size copy, so any body referencing a typedecl declared LATER
read its size-0 placeholder and baked it in: alias size 0, tagged-
union maxsz 0 (the F0 m5_match $48-frame under-allocated box), struct
field offsets collapsed, array element stride 0 — a whole cstage-only
family (7 size()-probe rows, all cs-fail/ww-pass pre-fix). wwstage's
demand-driven tinfofornode was order-independent on every row, so this
aligns cstage UP to the measured runtime-correct side (the #263-era
ruling; rule 10's align-down governs acceptance surface, not layout
correctness). Oracle: ken /tmp/ken_62_oracle.md — union size is 8B tag
+ roundup8(max CHASED member size), a fixed point over the module,
never a function of decl order.

resolve_typename now resolves a referenced-but-unresolved typedecl on
demand via resolve_typedecl (cycle-guarded by Type.resolving); the
pass-1.5 loop funnels through the same helper. No consumer can see an
unresolved placeholder by construction.

CYCLE GUARD — #69 ABSORBED into this rider (rob's rider condition):
true typedecl cycles now LOUD-reject on BOTH stages — "circular type
dependency" — mirroring harec's in_progress check (ref/harec/src/
check.c:4767 "Circular dependency for '%s'"). Pre-guard: cs silently
sized cycles 0; wwstage HUNG on an alias cycle (`type a = b; type
b = a` — ken's hang probe /tmp/ken62/c1_cycle.ww, killed at the 20s
timeout) and stack-overflowed on a struct value cycle. The check sits
at the VALUE-position size consumers only (alias root, struct field,
array elem, tuple member, union member), so the legal pointer
self-ref (`type node = struct { next: *node }`, the io.stream shape)
stays accepted, byte-id. wwstage gets the twin tinfo.resolving flag
(lib/ww/typ.ww) + circularnamed in check.ww; its arm loud-STOPS
(os.exit) rather than accumulating — wwstage's AST-level alias
walkers (resolvealias, aliaslookup chains) follow TNAME->TNAME by
name, blind to the tinfo table, and spin on a cyclic alias graph even
after the table edge is cut to tyerr (measured); cstage accumulates,
its single-peel ternaries cannot loop.

TWO-LAYER SPLIT — this is ONE bug number (#62) deliberately split
across THREE commits (this rider + F1 + F2), per ken's sizes-correct ≠
payload-correct proof: in NORMAL decl order both stages size the box
correctly (16/24, frames $64) yet both still run exit 2 — the box
STORE is word0-only, a chase-blind copy-WIDTH lookup in cgen, NOT the
type table. EXPECTED-FAIL after this commit: m5b_match1/m5_match stay
exit-2 both stages (now byte-id BOTH orders; pre-fix the fwd order was
$48-frame divergent). The Layer-2 sites and destinations:
  - F1 (cstage): cg_widen_tagged_store single NAMED peel,
    cmd/w6c/cgen.c ~2464 — the type_chase_named census family.
  - F2 (wwstage): rhsstructpayload bare name-keyed structlookup, no
    alias chase, selfhost/cmd/wcc/cgenutil.ww:3062 (structlookupchain
    :1691 already exists).
Banked runtime payload-readback rows for F1/F2: /tmp/impl62r_layer2_rows.md.

Test 944_alias_decl_order_size_run: every size class pinned in BOTH
decl orders (sizes, named union, struct field offsets, array elem,
2-level chain — norm + fwd twins, prefix-luck-breaking last-word
readbacks), 3 cycle BUILDERR rows + the legal ptr-self-ref row,
(void|base) no-regress control; dual-stage + per-row byte-id (arrelem
rows byte-id exempt: pre-existing #60 index-over-alias divergence,
order-independent, cited at the rows). lib/ww/typ.ww is an embedded
source: both main.combined.ww regen'd + committed (freshness gate).
2026-06-05 10:46:17 +09:00
24e02b259c wcc/check: inferred-let tuple literal carries its slot-layout size — 0-size local smashed saved BP/RIP (#44)
The N_TUPLE expr arm built its TY_TUPLE with size 0 (only the
annotated N_TTUPLE resolve_type route computed the layout), and
type_default passes TY_TUPLE through, so an inferred
`let t = (4: size, 2: size)` planted a 0-size local at offset 0 —
the element stores landed on the saved BP/RIP and main segfaulted
on RET (cstage; the arg shape instead fell to the global-symbol
path and link-failed). wwstage (exprtype N_TUPLE -> tinfofornode)
was runtime-correct throughout — cstage aligns UP to it; all
fixed shapes are now byte-id. Slot rule mirrors the N_TTUPLE twin
and cgen tuple_eslot, with untyped elements sized at their
type_default (element types stay untyped for the consumer-side
assignability contract).

7 table rows in 941 pin the class (cast/bare/mixed/float elems,
destructure-from-local, call-arg, nested); each fails at master
e8977a4 cstage (segfault or link-fail + byte-id NO).
2026-06-05 09:08:01 +09:00
66991585d6 wcc+w6c+w6c_ww: tagged tuple elements take their full slot — tuple_eslot accessor (#22a)
slot = roundup8(size(elem)) — 8B is a FLOOR, not a ceiling (user-
ratified 2026-06-04; the #237 fieldslotsize precedent; (str,str)=48B
predates this — tagged was the one truncated >8B kind). Pre-fix the
checkers truncated a tagged element to one 8B slot and every cgen
transport walk strode wide=(STR||SLICE)-else-8: cstage read the
NEIGHBOR slot, wwstage read ZEROS — both-wrong-differently, so the
byte-id gates were blind (prober-9 PG1, /tmp/p9).

One stride accessor per stage — tuple_eslot (cgen.c) / tupeslot
(cgenstmt.ww) — now feeds every tuple walk; the per-site predicates
are deleted as absorbed. Sizer: check.c N_TTUPLE + check.ww
tupleelemslot gain the TY_TAGGED arm (astsize already rides the type
table since commit 0). Routes flipped to the accessor, both stages
symmetric: cursor producers (lit/slot-to-cursor; tagged ident
elements load their box from the slot — cgexpr's tagged ident load
is word0-only), let-receive (tuple_store/tupstore generalized to
eslot/8 words), N_RETURN send, by-value param receive, arg
restage/drain (tagged stays loud per C-t2), destructure
(MLET/MASSIGN, ident + sret + in-cap), t.N read + len(t.N) +
global-g.N offset walks (t.N gains the tagged box load: AX=tag,
DX/CX/R8=payload — the is/as spill cursor), sret classifier, DATA
emit. wwstage cgtagvariantidx gains the #67 stamped-carrier arm
(flatvariantidxt on .type_) — its AST-only key silently clamped
't.0 is size' to tag 0; fused here because the tuple-element read
this commit wires is its only exercisable consumer.

Exit invariant: zero silent tagged-tuple paths — in-cap shapes
(<=4 GP eightbytes) are correct end-to-end; everything else is LOUD:
over-cap sret return (#22b, task #28), call-arg (C-t2 #32),
non-local literal element sources (#22b/#23), tuple-in-union payload
(#242/#22b), global static-init, element write (pre-existing).
Closure proof-grep at HEAD: 'tuple_ebytes|tupebytes' -> 0 hits;
'wide.*=.*(TY_SLICE|TY_STR)' tuple-walk survivors are all behind
tagged loud-guards (cgen.c:2535/2568/12013 widen-store + over-cap
send; cgenutil.ww:3527 twin).

Latent cross-checks closed by the accessor: wwstage MLET-sret strode
esz (4 for i32) vs cstage 8; wwstage param-receive strode slotsize
(composite slotsize) vs cstage 8; both now the accessor's floor-8.
Tagged inits in pins use the CAST form (5: size) — the bare
untyped-int widen-store mis-tag is pre-existing at master and filed
(task #33).

941 gains 13 rows: t22_* size/align folds (+ void-elem 0-slot pin),
the full runtime round-trip (read, is/as, both element orders, void
variant, destructure, literal-let), two-tagged-elem in-cap and
float+tagged in BOTH orders (ken k1/k2 regression-pinned as rows),
t.N-as-call-arg, 5 loud pins (arg, over-cap return, call-source
element, global init, element write), and the sret_narrow_mix_*
triple below. Runtime rows exit-checked under both drivers +
byte-id. 129/129; unit tests green; sizelint clean.

ken R1 (re-validation fix): an OVER-CAP tuple init whose rhs is not a
CALL fell past every cstage N_LET store arm to NOTHING — silent
uninitialized-frame reads — while wwstage loud-rejected the same
shape. Pre-existing for (str,str) literals; the #22a tagged slots
routed >16B-box tagged shapes into it (k5b/k5c/k5d, base-correct
because base truncated them in-cap). cstage now routes the rhs
through cgexpr (the cursor materialisers carry the exact wwstage
loud texts) with a trailing fatal as the net; 941 pins both the
tagged and the (str,str) spellings of the hole.

reviewer-22 (review fixes, folded): the MLET-receive slot flip above
landed ALONE on the wwstage over-cap sret family — the ww RETURN-send
and MASSIGN-receive still strode packed esz (4 for u32), so
`let (a,b,c) = f()` over (str,u32,str) read c at slot offset 32 while
ww's send wrote it packed at 28: a ww runtime REGRESSION vs base
(base was packed-consistent on both sides — ran right, byte-diff).
Both walks now stride the slot (send mirrors cstage's
`wide ? esz : 8`; MASSIGN strides tupeslotn) — closing, in the same
stroke, the pre-existing base skews: ww `let t = f()` over-cap
narrow-mix read (reader slot-laid vs send packed, runtime-wrong at
base) and the cs≠ww asm on all three routings. Second find: t.N
tagged element as a CALL ARG — cstage rides its generic
node_istaggedarg cursor push, but wwstage's kind-gated aistagged
missed N_DOT and mis-routed the box into the widening branch
(taggedvariantindex -1 clamped to 0: callee read variant 0, silent,
ww-only). cgenutil.ww gains the N_DOT arm (aistagged + pushargsrev),
the #67 stamped-carrier twin of the N_INDEX (#12) arm. The N_DOT arm
was a LIVE wwstage stdlib miscompile: hextest's import graph carried
two t.N tagged-arg sites (base .s diff = exactly the clamped-tag
PUSHQ pair) — the 989 lib-byteid ratchet caught the convergence and
#59.4 graduates to M_ID (runtime-correct per 979_hex_run). Probes:
/tmp/r22. Residual filed: tagged arg from deref/cast/unwrap sources
is a word0-only read on BOTH stages (pre-existing, base-confirmed) —
generalizing wwstage's kind gate to cstage's type-keyed check rides
that task.
2026-06-05 01:37:35 +09:00
1bcf2726cf wcc+w6c+w6c_ww: delete() range form delete(xs[lo:hi]) (fold-5a P2)
Hare's delete also takes a slicing place (harec check.c:1981-2027
EXPR_SLICE; Hare spells it delete(xs[i..j])): remove [lo, hi) — shift
[hi..len) down count = hi-lo strides, len -= count, cap unchanged; lo
defaults 0, hi defaults len, so delete(xs[:]) clears the slice with
storage retained. Checker accepts N_SLICE next to N_INDEX (object must
chase to a slice, harec :2024); the old range-unimplemented reject and
its #35 cite drop.

Lowering (both stages, converged byte-identical by construction) is the
single-element arm's same-slice whole-stride word-copy loop with a
DYNAMIC src offset (count*esz via a src register) instead of the
constant one-stride. Base shapes: local slice ident, deref-of-local,
plus NEW indexed local-slice base xs[g][lo:hi] — the fold-5a consumer
shape (regex.ha:333 delete(jump_idxs[group_level][..]); outer stride
off the type table). Bounds stay implicit, inheriting the documented
single-element posture (no index checks anywhere in cgen). Operands
evaluate left-to-right, exactly once, before the shift (harec order);
only the header ADDRESS is taken before operand eval, so a bound
expression's writes through the slice land before the copy.

test/809: 64 fixtures — full/explicit/re-clear/head/mid/tail/empty
a:a/end-boundary len:len/explicit 0:0 on a never-appended (nil-ptr)
slice, single-vs-range equivalence, cap preservation, esz 1/2/4/8/16/24
copy tails against the dynamic src, operand order-of-eval (lo/hi CALLs
fire once each, in order) + aliasing-visibility pins, the EXACT
[][]size regex consumer shape, deref base, 2 reject rows w/ diagnostic
text; every accept row cs==ww asm byte-id. test/804: reject_range row
retired (form now accepted), reject_nonindex text follows the widened
message.
2026-06-04 23:37:08 +09:00
74767c70cc wcc/check+wcc_ww/check: reject overlong array literal — frame-smash class (#71)
An array literal with more elements than the declared [N] passed the
per-element accept-if-fits checks in both stages and cgen then stored
every element at its natural offset, writing past the slot: local
frames smashed silently (the repeat form [1,2,3...] into [2]int wrote
at the saved BP), module DATA corrupted neighbours. All four
declaration contexts (local let, module let, def, struct-field
literal) funnel through one choke point per stage — arrlit_init_fits
(check.c) / checkarrlitfits (check.ww) — which now pre-counts the
literal (skipping the ... marker) and rejects count > N naming both
counts.

cstage clet's blanket has_arr_repeat bypass is narrowed to non-array
declared targets: repeat literals into arrays now run the same
overlong + #130 range checks wwstage's checkletassign always ran
(the bypass let [2]u8 = [999...] dodge the range check cstage-only).

checkarrlitfits also recurses into NESTED array-literal elements
(declared elem node N_TARRAY): cstage catches the nested shape
through its typed-literal assignability net, which wwstage's untyped
elements have no analog of — [2][2]int = [[1,2,3],[4,5]] at module
scope silently emitted corrupted DATA (1,2,4,5) and the struct-field
twin likewise. Recursion through the one choke point closes any
depth; a named-alias element type still bypasses — task #16.

alen==0/nil-length stays exempt ([0]/[_] sentinel conflation and
un-inferred [_] in def/struct-field — task #11); a non-INTLIT length
child (def-named [N]) is exempt in wwstage — task #13; under-long
literals keep their current accept (Hare rejects — task #10);
wwstage's overlong accept at assign/call-arg/return position (cstage
already rejects) is task #12; exact-fit bare-int nested cs-reject/
ww-accept divergence is pre-existing — task #17.
2026-06-04 22:48:53 +09:00
eea3e197c2 w6c+w6c_ww: *[N]T indexing strides by element, not whole array (#61 A+B)
Indexing through a pointer-to-array auto-derefs, so esz and the element
classification must come from the pointee array's ELEMENT (cstage
idx_eff semantics, cgen.c:1163). Two halves of one root class:

A (wwstage-only, cs!=ww, cstage runtime-correct): elemsizeofc's #270-2
nested-array block treated an N_TPTR pointee-array like a [N][M]T outer
index and returned the whole-array size — every p[i] read/write/
compound scaled by N*size(T), and the same wrong element belief reached
the store-width chooser (var-idx write emitted an N*8B aggregate copy
sourced at the 8B rhs slot: caller-frame smash, the siphash round()
corruption). Fixed via two wwstage choke-points mirroring idx_eff:
idxeffti (tinfo: NAMED peel + TY_PTR->TY_ARRAY drill; feeds elemsizeofc
and elemissignedc/elemisfloatc/elemisf32c) and idxelemtn (node: element
tnode with the same drill; feeds every cgindex/cgassign/nodeisstr/
match-scrutinee elemtn resolution).

B (BOTH stages identically wrong, byte-id-BLIND): the TK_AMP &base[i]
arm read bu->sub->size without the ptr peel (&p[3]-&a[0] = 96, not 24).
cstage now routes esz through idx_eff.

A and B are FUSED by the pre-existing routing topology, not by choice
(rule 11): wwstage's TK_AMP arm already reads its esz via elemsizeofc
(selfhost/cmd/wcc/cgenexpr.ww:4095, the #11 addr-of twin of the #10
cgindex fix), so fixing A's choke-point flips wwstage's half of B in
the same stroke. A standalone A leaves &p[i] transiently cs!=ww;
B-first is the mirror transient; carving the TK_AMP caller out of the
fixed choke-point to preserve the wrong stride for one commit would be
a deliberate known-wrong intermediate (rule-7, vetoed by rob). One
choke-point, two enrolled routes — un-fusable without a red
intermediate.

Close-by-construction proof-grep (both stages): every remaining raw
sub->size index-stride read is TY_ARRAY-gated, a slice-only builtin
(delete/insert), a checker-stamped element tinfo (indexresult already
decays *[N]T, check.ww:2277-2284), or a non-index context (tuple
slots, let-init elements). Two true residuals filed with site+symptom
instead of silently absorbed: N_SLICE through *[N]T does not decay
(LOUD type error, Hare divergence; team task #18) and non-ident
cast-expression index bases keep wwstage's 8B-default esz (pre-existing
#74-style cluster; team task #19). cstage's N_INDEX read-side
str/slice header gates also move from u->sub to esub (identical for
every non-ptr-to-array base; honest for *[N]str — pre-fix BOTH stages
were runtime-wrong there, differently).

949_ptrarr_index_run pins the class at runtime + byte-id: {1,2,4,8}B
elems, const+var idx, param/local/cast bases, read/write/compound,
neighbor guards, &p[i] pointer-difference, siphash-round mix shape.
989_lib_byteid: siphash_test graduates #59.7 DIVERGE -> ID (ratchet
tripped loud pre-update; no other #59.x pin flipped in the same run).
(*p)[i] (sub-bug C) follows separately.
2026-06-04 22:34:09 +09:00
fdfc2ce318 wcc+w6c+w6c_ww: tuple slot layout SSoT — checker size = cgen slot stride (C-t0)
The checker computed TY_TUPLE size as the packed element-size sum
((u32,u32) = 8B) while every cgen cursor-transport site strode 8B
slots (16B). 16B tuples were blind to the split (slot == packed);
packed tuples hit it everywhere: cstage let-receive keyed on sz 16/32
missed sz 8 and dropped word 1, the cgfn param receive spilled
8B/element into a packed-sized local (saved-BP clobber, SIGSEGV), and
mixed (u32,f64)/(u32,str) shapes missed the receive arms entirely.

Slot layout is now the SSoT (user-ratified): the flip lives in the two
checkers' N_TTUPLE size computation only (check.c, check.ww
tupleelemslot + stamp); cgen's packed-keyed walks (t.N read, #235 len
arm, over-cap sret send/receive pair) align onto the slot stride, and
the wwstage t.N read gains the natural-width load (tnodeloadop) to
byte-id with cstage's fldloadop. ttupleelem.offset re-stamped
slot-cumulative (no consumers yet). The #242/#243 eightbyte-share
loud-stop dissolves by construction (no two narrows ever share an
eightbyte) — 940's eightbyte_share row graduates to a runtime
round-trip. Hare-layout divergence documented at both checker sites;
re-alignment is task #60. #32 send skew and #33 wwstage literal-let
receive are separate commits on this base.

941_tuple_slot_layout_run pins the matrix: 4 packed rows fail at the
parent (8/21 checks), 3 neutral anchors prove 16B/32B emission
untouched.
2026-06-04 19:06:54 +09:00
9861f73bbb wcc+w6c+w6c_ww: insert() builtin — single-element slice insertion (part of #35)
Hare's insert(xs[idx], v) (ref/harec/src/check.c:745
check_expr_append_insert — append/insert share the checker arm,
"insert" at :786): checker accepts an INDEX place over a slice plus
one value, stamps void; idx == len is a legal end-insert (the
ref/hare os/exec/platform_cmd.ha:86 idiom). Loud-rejects with exact
texts: spread form insert(xs[i], vs...) (filed, #35 — also covers
harec's with-length form via the arity check), range place (not
Hare; harec only parses ACCESS_INDEX, :784), non-index operands,
array bases, wrong arity. delete()-parity throughout.

Lowering (both stages, converged byte-identical by construction) is
a DESUGAR: append(xs, v) — reusing append's grow (rt_ensure) and the
entire #34 value-store dispatch (scalar / str-slice header / tagged
widen / struct fill) verbatim, one boxing choke-point — lands v at
slot len-1; then a rotate-right of [idx, len) moves it home through
a fresh per-site esz frame scratch (@insscr). The rotate is delete's
shift loop in reverse (descending j, the safe memmove-up direction)
and is a same-slice whole-stride raw byte move — no boxing exists
for any element kind. idx evaluates BEFORE the grow (Hare's
left-to-right operand order — pinned by the pregrow_len_idx row,
insert(xs[len(xs)-1], v): pre-grow [7,13,11] vs post-grow [7,11,13];
an idx==len(xs) end-insert cannot discriminate, the rotate
degenerates either way). Base shapes: local slice ident (LEAQ) and
deref-of-local ptr-to-slice (MOVQ); others rule-7 loud-stop, like
delete.

test/807: 57 fixtures — front/middle/end + idx==len via len(xs) +
the pre-grow eval-order pin, esz 1/2/4/8/16/24/56 (MOVB/MOVW/MOVL
tails, struct body, str header, 7-qword tagged from a typed local
[the regex fold-3 ha:347 newinst shape] and from a cast rvalue
[ha:419/441]), empty-slice grow, (*p)[i] deref base, front-insert
loop, 6 checker reject rows with diagnostic-text checks; every
accept row cs==ww asm byte-id.
2026-06-04 17:24:57 +09:00
48df04a8ca wcc+w6c_ww: loud-gate try-propagation over multi-success unions (F8/F9 interim)
? and ! assume ONE success member end-to-end: the checker collapses
the result to the first non-error variant (check.c tagged_success_type
/ check.ww exprtype) and cgen emits a single tag compare, so any other
success member is silently mistaken for an error — ? propagates it to
the caller (p11h: []capture read back as nomem, exit 21), ! aborts on
it. Until the honest subset-union result typing lands (task #14, harec
check.c:2759-2835), both stages loud-reject |success| > 1 at the
checker choke-points (one per stage), identical diagnostic, both ops
per rob's one-class ruling (#133 precedent). (T|err1|err2) — one
success, many errors — stays legal (925 canary + new accept rows).

F9 rides along (task #12): wwstage scruttype only resolves IDENT/DOT,
so the direct forms f()? is T / match(f()?) / f()! is T slipped its
lenient-miss contract and were silently ACCEPTED where cstage rejects
(cs!=ww, gate-blind). checkisas/checkmatchexhaust now resolve the
try-result via exprtype, keyed on the RESOLVED success type — a named
tagged success ((ab|nomem)? is i32) keeps being accepted, matching
cstage's verdict empirically.

test/wcc/806: 11 rows x dual driver + byte-id accepts (26 fixtures);
reject rows pin exact per-stage diagnostic text; p11h + q_card2_unw
graduated to rejects; call-arg-position reject + void-success accept
pin position-independence and the dominant lib/ (void|err)? shape.
Tasks #5 + #12; #14 lifts both gates together.
2026-06-04 10:20:18 +09:00
37febab9d5 wcc+w6c+w6c_ww: delete() builtin — single-element slice removal (part of #35)
Hare's delete(xs[i]) (ref/harec/src/check.c:1981-2027): checker accepts
an N_INDEX over a slice-typed base, stamps void; loud-rejects the range
form delete(xs[i..j]) (stays filed on #35 — regex fold-2b's consumers
are all single-element), non-index operands, array bases, wrong arity.

Lowering (both stages, converged byte-identical by construction):
ascending word-copy loop shifts [i+1..len) down one esz stride, then
hdr.len -= 1; cap unchanged. The move is a same-type whole-stride byte
copy — src and dst are elements of the SAME slice, so no boxing exists
for any element kind; one loop serves scalar/narrow/str/struct/tagged.
esz off the STAMPED base type (#34/#48 discipline). Base shapes: local
slice ident (LEAQ) and deref-of-local ptr-to-slice (MOVQ — the fold-2b
delete_thread shape); others rule-7 loud-stop.

test/804: 38 fixtures — first/middle/last/to-empty, esz 1/4/8/24/56
(MOVB/MOVL tails + 7-qword tagged), cap-unchanged, (*threads)[i], 4
checker reject rows; every accept row cs==ww asm byte-id.
2026-06-04 06:41:12 +09:00
bf1037d8c4 wcc/check+w6c+w6c_ww: materialize array-literal slice-borrow base into per-fn scratch (fix #25 + #31)
A one-step `let xs: []T = [e0,e1,..]` had two faults. #31 (silent, cs!=ww):
the #258 array→slice borrow wrapped the un-addressable N_ARRLIT directly as
the N_SLICE base and cgen never spilled it to a stack slot, so .ptr dangled
(`let xs:[]i32=[10,20,30]; xs[1]` returned the un-stored header 1; []u8/[]str
segfaulted). #25 (over-strict): a slice target fell through to the exact-
element type_eq borrow gate, rejecting bare-int-width ([]u8=[1,2,3]) and str
elements the array-init path coerces.

Fix (re-stamp + per-borrow scratch; both stages byte-identical asm):
 - Checker re-stamps the slice arrlit as [count]T, reusing the array-init
   per-element coercion + range-check (#25): in-range accepts, out-of-range
   loud-rejects. cstage arrlit_init_fits gains a TY_SLICE arm; wwstage
   checkletassign mirrors it and stashes the synthesized [count]T tnode on
   arrlit.lhs (free for N_ARRLIT) so cgen can size the backing NODE-wise
   (elemsizeofc) and count from the tnode's .rhs intlit — the arrlit's own
   value tinfo carries the literal's untyped element (unsized), so node-first
   sizing is required (a cstage/wwstage representation divergence; cstage's
   Type IS sized and reads base->type).
 - cgen materialises the N_ARRLIT borrow base into a FRESH per-borrow
   @slicescr stack slot (distinct slot per borrow: a borrow's backing must
   outlive the lowering, so it can't share a cached @aggargscr/@tagscr-style
   slot — two live borrows would alias one backing; localalloc/local_alloc
   is always-fresh), filled by REUSING the array-init element fill extracted
   from the N_LET path (cstage cg_arrlit_fill_bp, wwstage cgarrlitfillbp —
   same store sequence the byte-id-green `let a:[N]T=[..]` uses, the
   frame-order + store-op guarantee), then LEAQ'd as the base.

Supported ONLY at a `let` init. In call-arg / return / assign position
there is no addressable backing, so both stages LOUD-REJECT ("bind it to a
`let` first") — aligning cstage DOWN to wwstage (which already refused the
untyped arrlit element) per rule-10; this closes #31's silent call-arg
segfault as a compile error. Full non-let support is deferred (#33).

Escape (rule-8 WHY): a `let xs:[]T=[..]; return xs;` returns a slice into a
freed frame slot = dangling, IDENTICAL to the pre-existing named-array
borrow and Hare-consistent (no escape analysis / GC / heap promotion).

Test 953_arrlit_slice_run: 8 accept rows (cstage runtime readback +
cs==ww byte-id, frame-size canary incl.) covering the #31 i32 pin, bare-int→u8
coercion, str readback, the multi-live soundness pin (xs[0]+ys[0]=5, not 8 —
proves fresh-per-borrow), and a mutate-through-borrow proof; 4 reject rows
(out-of-range element + the three non-let contexts, loud in both stages).
Tuple-element slices stay blocked by the pre-existing #30 array-init FATAL.
2026-06-04 01:44:39 +09:00
7ca32432b1 w6c+wcc/check: infer [_]T array length from initializer element count (fix #7)
`[_]T = [...]` (canonical Hare array-length inference) silently
miscompiled to a zero-length array: the parser already left the array
type's length child nil as the infer sentinel — distinct from an
explicit [N] — but neither checker stamped the real count, so `len(x)`
returned 0 with no diagnostic (rule-7 silent miscompile). Module-level
was worse on wwstage, where `x.len` on ANY global array (even an
explicit [N]) fell to the SB fallback and mis-emitted `MOVQ len(SB), AX`
(linker: undefined reference to len).

The length lives in the stamped TYPE and cgen already keys stride /
length / data-emission off it, so stamping the inferred count at the one
checker inference point closes it permanently (rob's #7 ruling):

  - check.c clet + module-level N_LET pass-2: count the initializer's
    elements and patch the array type's length (the Sym too, so a later
    x.len reads the inferred alen). No-init / non-array init can't infer
    -> loud error, never a silent zero-length array.
  - check.ww inferarraylen: the wwstage twin — stamp a synthesized
    N_INTLIT length child before resolvewalk caches the array tinfo;
    same loud-error rule. Idempotent for the module-level double-call.
  - cgenexpr.ww cgdot: the missing wwstage arm for a top-level [N]T
    global's .len / .ptr (cstage cgen.c:8011 already had it).
  - cgenutil.ww letslotsize: drop the now-redundant [_] slot-size
    intercept — a workaround for this very bug; the stamped length flows
    through the general slotsize path (rule 7).

Both stages converge byte-identical; new table-driven test 684 covers
[_]int/[_]str/[_]u8 local + module-level, len + element read-back,
dual-stage runtime + asm byte-id, plus three negative no-infer rows.
2026-06-03 18:44:39 +09:00
90479fed68 w6c+wwstage: reject untyped empty-[] alloc — require context, loud cannot-infer (#3 B', subsumes #5)
An empty `[]` carries no element type; ww gets it only from a let
annotation (the #45 retype). Both stages used to silently default the
element to u8, and in value-form positions (return / call-arg) the
lowering miscompiled — malloc(8) ignoring n, a 16B *u8|nomem where a 24B
slice was expected (#5). Now every empty alloc that isn't a
let-annotated binding fails to infer with a loud error, aligning ww DOWN
to harec (ref/harec/src/check.c:1801-1802).

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

Tests: negative cstage-driver 729 (table-driven: bare-let, return,
call-arg, assignment) + positive @test in attest_pass.ww exercising the
u8 and the wide-i32 (#45) paths at runtime. Both stages reject
symmetrically; byte-id verified on []u8 and []i32.
2026-06-02 18:54:35 +09:00
e92708ecda w6c+wwstage: implicit [N]T->[]T array-to-slice coercion via desugar (#258)
Hare admits an array with a defined length wherever its element slice is
expected (assign / return / call-arg / init) as a borrow; ww rejected it
everywhere (the #108(c) exclusion), so base64 worked around the gap with
explicit a[0:n] slices.

type_assignable / isassignable now admit array->slice on an exact element
match (mirror ref/harec/src/types.c:1080-1097, the SLICE-dst arm). The four
acceptance sites route through one shared helper (desugar_arrayslice /
desugararrayslice) that rewrites the array expr to the explicit full slice
arr[0:len(arr)] — an N_SLICE over the array base. cgen is untouched: the
existing slice lowering (#252/#257/#135 made array bases, incl struct-field
arrays, correct) materialises the borrow header {.ptr=&arr[0], .len=N,
.cap=N}, byte-identically in both stages.

wwstage runs no general call-arg / N_ASSIGN typecheck, so checkassign +
desugarcallargs are added solely to route those two contexts through the
shared desugar (rule-10). desugarcallargs additionally loud-rejects an
element-MISMATCH array into a []T param, scoped to that shape so wwstage's
broader call-arg leniency is untouched.

953_arraytoslice_run covers the four contexts + a borrow-alias proof + the
i32/u8 element axis (dual-stage run + cs==ww byte-id), plus mismatch-reject
rows asserting both stages refuse [4]i32 -> []u8. Regen'd w6c + wwdump
combined.ww (#110).
2026-06-02 05:31:24 +09:00
d56b7ca946 w6c+wwstage: narrow int/rune array-literal elements to the declared type (#251)
`let a:[4]u8=[65,66,67,68]`, `def D:[4]u8=['A',..]`, and `enc{m=[65,..]}`
rejected with "init [4]i32 not assignable to declared [4]u8": an array
literal's element type came from the elements via type_default (int-lit
-> i32, rune-lit -> rune) with no declared-element-type propagation. The
scalar path already narrows (`let c:u8='A'`); only array aggregation at
the let/def/struct-field sites #130 (test 920) left unwired did not.

Fix = the int/rune analogue of coerce_floatlit, realised as the EXISTING
#130 accept-if-fits range-check — NOT a node-type restamp. cgen drives
the array element WIDTH from the declared type at every site (cgen.c
local-let lu->sub, emit_array_data d->type), so a restamp would be dead
code (the array literal keeps its [N]i32/[N]rune node type; the cs==ww
byte-id gate confirms the bytes emit u8-wide regardless). Per element:
foldable int/rune literal -> defcastfits range-check vs declared T
(in-range accept, out-of-range REJECT loud, rule-7); non-foldable ->
type_assignable / isassignable.

cstage (check.c): wire arrlit_init_fits into clet (local let),
struct-field-init, and def-init — the three sites the #130 module-let
path already covered.

wwstage (check.ww): factor checkletassign's inline #130 block into
checkarrlitfits and call it from the let path, the def path, and a
TARGETED array-field walk in the N_STRUCTLIT arm. This also closes a
pre-existing rule-7 wwstage over-accept: the def path ran NO init
assignability check and the N_STRUCTLIT head-stamp parks field
assignability (#23), so out-of-range / str array elements silently
over-accepted (a truncating miscompile) at those two sites. The
struct-field walk is the array-field accept-if-fits ONLY — it reuses the
stable N_TSTRUCT field-list walk (astoffset precedent), isolated from
the broader parked #23 field-assignability walk.

Regenerated w6c + wwdump combined.ww (embed check.ww). New test 951
covers let/def/struct-field x int/rune accept (run + cs==ww byte-id) and
out-of-range/str reject (both stages). test-unit 237 + smoke green.
2026-06-02 01:53:57 +09:00
39292b3c47 wcc: kind-filter type-position name resolution so a value can't shadow a same-named type (#225)
resolve_typename used the kind-blind scope_lookup_prefer, so a same-named value binding (param/let) in a closer scope hid the type it shadowed, wrongly rejecting valid Hare like 'fn f(off: off)'. wwstage already separates type/value namespaces; this aligns the cstage frontend up. New scope_lookup_type skips non-SK_TYPE syms and keeps scanning, preserving same-module preference. Byte-id-neutral: the new branch fires only on the old 'unknown type' error path.
2026-05-31 17:00:42 +09:00
f6ac7fb2f8 wcc: accept bare &fn into a fn-pointer-alias slot via a caller-site gate (#206)
A bare `&fn_name` was not assignable into a `*reader` / `(*reader | void)`
vtable field without an explicit cast: cstage type_eq on TY_NAMED is
pointer-identity, so a structural `*fn(...)` referent never matched the named
`*reader` variant; wwstage accepted it only via an accidental catch-all
leniency. harec accepts bare &fn through hint-directed alias adoption at the
address-of site (check.c:3594-3626) while keeping pointer assignability
strictly nominal (types.c:1039-1066), so a materialized `*fn` value never
launders across alias names.

Mirror that decision without threading a type hint through the bottom-up
cexpr: keep type_assignable / isassignable fully nominal, and add a
caller-site helper (assignable_addrfn) at the assignment boundaries
(let-init, struct-literal field-init, assign, return, call-arg, array
element) that accepts iff the rhs is a DIRECT &-of-fn-ident and the
destination (or exactly one tagged variant) is a pointer-to-fn-alias whose
underlying fn signature structurally matches. A materialized `*fn` value, a
distinct same-signature alias, and an ambiguous multi-variant target all stay
rejected. Both stages share the rule; wwstage's lenient pointer-fn punt
becomes a confident reject. ww has no methods, so a `value.leaf` slot is only
ever a fn-pointer field and this never over-admits.

The tightening surfaced a wwstage typeeqast gap: a TY_FN result that is a
tuple (`*fn(...)(i32,i32)`) compared false where cstage type_eq handled it,
newly rejecting a legitimate structural assign. Add the N_TTUPLE structural
case (rule-10), restoring test 766.

cgen-neutral (the cast was a no-op reinterpret); pre/post bootstrap .s
zero-delta. Test 783 covers the positive paths (incl. a byte-id-clean
three-field-vtable dispatcher) and the negatives. Tagged-slot negatives
(ambiguous / tagged-laundering) are rejected on cstage but wwstage's separate
`(X|void)` void-variant leniency (#214) still admits them; 783 pins them
cstage-only, to graduate when #214 closes (required before wwstage becomes
the authoritative selfhost checker).

Note: `make clean && make test` is RED at HEAD on 4 alloc fixtures
(700/748/758/915) via a pre-existing clean-build defect (#215, malloc vs
rt_malloc); identical with or without this change, so bisect-clean for #206.
2026-05-29 18:05:53 +09:00
487cf91f12 wcc: accept NAMED-variant nominal match at tagged→tagged subset (#205)
The tagged→tagged subset arm walked src's leaves against dst's flat
variant list, so `let r: (size | eof | wrapper) = e` with e: wrapper
REJECTED at cstage's checker — wrapper's leaves (unsupported, underread,
nomem) aren't direct variants of dst. Wwstage's permissive tail
accepted silently but cgen then miscompiled the tag (#199b layout-
extension family, deferred).

Mirror the concrete→tagged fix from #199 (α) at type.c:316: when src is
a NAMED-tagged wrapper and dst has a direct NAMED-tagged variant equal
to src, accept by nominal identity BEFORE the subset loop. Wwstage's
isassignable mirrors the structural insertion before the existing
`*confident = false; return true;` tail (deferred-tightening per #202).
SSoT with `is`/`as` non-recursive variant lookup (#198 family).

Cgen's tag-remap for the wrapper-as-whole case still maps src variants
to dst tag 0 — the wrapped-slot layout for `dst.tag = variant_idx,
dst.payload = src` is #199b future-work. Probe verifies checker-accept
+ runtime exit-clean only; does NOT inspect the resulting variant tag.

Probe 774_tagged_widen_named_variant.c covers 5 rows: bug-repro,
nested-wrapper, pure-leaf subset (regression), concrete-unrelated
rejection (gate), branched callee. Two sibling cgen/checker bugs
surfaced (wwstage cgwidentaggedstorebp ssz<slot_sz pad gap; wwstage
isassignable !void-alias collapse) and documented inline at the
probe-row comment, kept in #202 family.
2026-05-29 06:18:27 +09:00
4d44242363 wcc: reject transitive nested-tagged widen at type_assignable (#199 α)
cgen has no wrapped-slot layout — the tagged-union slot is universally
[tag:8B][payload:up_to_24B], single level. The recursive walk admitted
let r: (size|io.eof|io.error) = u for u: io.underread (transitively
in io.error.params); cg_tag_for_variant + taggedvariantindext don't
recurse, returned -1, defaulted to tag=0, and the slot read back as
variant 0 = size at runtime.

Restores SSoT inside the checker pair: is / as / match variant
lookup is already non-recursive (#198 sibling), and the LET-init /
return / assign arms now agree. Aligns DOWN to the leaner side
(rule-10 stage symmetry). ww-stricter than Hare; harec keeps the
drill at ref/harec/src/types.c:702-739 (#199b is the deferred
wrapped-slot layout port).

Pre-flight audit (drew mandate): zero transitive-widen sites in
lib/ + selfhost/ + cmd/ + examples/. No wrapper-tagged variant
(io.error, strconv.error, fmt.field) is used as a variant of a
wider union anywhere in bootstrap. Mechanical fix.

Escape hatch for callers: spread (...wrapper) inlines the wrapper's
flat variants into the parent set at parse time. Wwstage's gate
additionally preserves the recursive drill on op == TK_ELLIPSIS
because wwstage stays AST-keyed (cstage flattens at resolve_type).

771_widen_transitive: 5 rows (reject_transitive_widen,
spread_alt_widen, direct_flat_variant, branched_callee_widen,
wrapper_typed_widen). Row 2 is CS-only — wwstage's is / match on
spread-expanded variants is open-bug #190/#198.
2026-05-29 03:04:18 +09:00
0546bda6c4 wcc: array-init accept-if-fits coercion (#130, merges #146)
Align bare-int array-init assignability to Hare's literal-fits rule
(ref/harec/src/types.c promote_flexible): accept untyped-int array
elements that FIT the element type, reject out-of-range loud. cstage
(rejected all bare-int arrays, over-strict) and wwstage (accepted +
silently truncated out-of-range, over-loose) converge to the same
accept-if-fits rule. Per-element: foldable int literal range-checked
against element type [min,max] via def_cast_fits (rule-13 type-table
widths); non-foldable element falls back to type_assignable.

cstage: new arrlit_init_fits, N_LET decl-check fallback after whole-
array type_assignable fails. wwstage: checkletassign array branch +
route top-level lets through checkletassign (were unchecked — only
function-body lets ran assignability; closes #146 wwstage str->u8
over-accept).

Scalar-init range-check (let X:u8=300 truncates, both stages,
pre-existing) deferred to #148 — language-wide, needs bootstrap audit
+ explicit-cast conversion. def-array accept-if-fits deferred to #151
(def constfold machinery, different risk). Both bootstrap-NEUTRAL.

Test 920 (14 rows — accept: in-range u8/u32/u64/i32 + u8/i8 boundary +
typed regression + non-foldable-body; reject: over-range + over-256 +
i8-over + neg-for-unsigned + str->u8 + non-foldable-wider). Non-
foldable else-branch cs==ww verified (matching-type accept + byte-id;
wider-runtime-int reject both stages). Make test: 183/183 incl 990-997
byte-id + combined_ww_fresh.
2026-05-27 09:18:03 +09:00
a1dff13ec1 wcc: stamp un-suffixed f32-context float literals (#104 fold-2)
fold-1 narrows a float literal at materialisation only when its node
already carries an f32 type — the `f32` suffix. The common un-suffixed
case `let x: f32 = 1.0` stays ty_untyped_float through the checker, so
the node is never f32-typed: the literal materialises as a 64-bit double
and the f32 consumer reads the low 4 bytes (0.0f for clean values).

Stamp such a literal f32 when an f32 target type is in context, the way
harec's lower_implicit_cast does (ref/harec/src/check.c:148): a float
literal's bit pattern is target-dependent, unlike a width-agnostic int
immediate, so the value-producing node must carry the type. Scoped to
untyped_float -> f32 only (f64 already works via cgen's double default).

coerce_floatlit (cstage clet + cstmt N_RETURN) / coercefloatlit (wwstage
resolvewalk's post-order N_LET / N_RETURN handler) are logically
identical. The wwstage stamp is placed AFTER the child re-walk: the
post-order exprtype dispatch re-stamps a bare N_FLOATLIT back to
untyped_float, so coercing earlier (checkletassign) would be undone.

Scope is let-init and return ONLY, aligned down to the leaner wwstage
(rule 10). The wwstage cgen's exprfloatkind hardcodes a float literal to
f64 and cgbin / the unary negate pick f32 off the operands, not the node
stamp — so a stamped literal in an arith-binop / behind a unary minus
narrows in cstage (ADDSS) but not wwstage (ADDSD), a byte-id break. The
wwstage checker also has no assign / param-typed call-arg / per-field
struct-lit site. binop, unary-minus, assign, call-arg, struct-field wait
on #120 (wwstage cgen + checker build-out).

965_f32stamp_run: cstage run + cs==ww byte-id over un-suffixed let-init
and return literals, the hole 964 left open. Regen w6c/wwdump
combined.ww embeds.
2026-05-26 12:06:47 +09:00
3a0c7442d4 wcc: opaque assignability sink + reinterpret-cast verify (#108)
#108 sub-fold (c): opaque as a type-erasure sink. Two implicit
assignability rules + the reinterpret casts sort's impl relies on.

  rule 1  `*T -> *opaque`  IMPLICIT — any pointer is the universal
          void-pointer. harec type_is_assignable pointer arm
          (ref/harec/src/types.c:1053: `case STORAGE_OPAQUE: break;`
          — the referent need not match).
  rule 2  `[]T -> []opaque` IMPLICIT — any slice is the erased slice;
          {ptr,len,cap} header is normal, byte stride supplied at
          runtime. harec slice arm (types.c:1094).

Both fire only when the destination element is opaque, so they are
inert on the opaque-free selfhost corpus.

Rule-10 (per-rule, empirical): rules 1 & 2 are CSTAGE-ONLY. cstage
type_assignable gains the sink; the wwstage check.ww isassignable is a
resolve-only AST approximation that returns "can't tell, stay quiet"
(confident=false) for a ptr/slice whose element it cannot match, so it
already ACCEPTS every form (let-init AND call-arg). Verified: w6c_ww
compiles each probe source exit 0, byte-identically to w6c. cstage
rejected these before this change; no ww twin is needed (same
align-down precedent as 960/961's cstage-only arms).

Casts: N_CAST is validation-free in BOTH stages (the checker never
checks cast legality), so `[]opaque -> *u8` / `*opaque -> *u8`/`*i32`
are already legal. The reinterpret CGEN needed NO change: cgexpr
leaves the pointer in AX for both a slice (so slice->ptr naturally
takes .ptr) and a pointer (ptr->ptr is a no-op). drew described the
Hare idiom as `*[*]u8`; ww has no unbounded-array `[*]`, so the
ww-faithful reinterpret target is `*u8` + uintptr stride arithmetic.
cs==ww byte-id proven on every probe row.

Array->[]opaque (harec array->slice decay, types.c:1080-1099) is
deliberately EXCLUDED: ww has no implicit array->slice for any element
type (`let s: []i32 = a` is rejected too — a slice is built only via
an explicit `a[0:n]`), so there is no array->slice-header cgen.
Accepting array->[]opaque alone would assign a fat array local into a
24-byte slot with no decay: a silent miscompile (rule 7). sort's
caller passes a slice, so slice->[]opaque suffices.

opaque is unused by the bootstrap → INERT → 990-997 stay
byte-identical; combined.ww unchanged (no embedded source touched).
New probe 962_opaque_assign_cast_run carries both dimensions per row
(cstage build+run asserting type-erasure round-trips, AND a w6c-vs-
w6c_ww .s byte-id gate — the 990-997 gates never exercise opaque, so
the test pins rule-10 symmetry itself): rule1_implicit_ptr,
rule2_implicit_slice, and sort_pattern (byte-swap via uintptr stride
through []opaque, read back through the *opaque path and the original
[]i32 view). Probe binds call results before comparing to dodge a
pre-existing inline-call-result-in-comparison cgen bug (#116 family,
reproduces with zero opaque) — same dodge 960 uses.
2026-05-26 10:29:04 +09:00
f4970d886c wcc: opaque use-guards — reject every unsized use (incl tuple/tagged, recursive) (#108)
#108 sub-fold (b): close the footgun #108(a) opened. opaque is abstract
and UNSIZED (size = align = SIZE_UNDEFINED = (u64)-1), legal only behind
indirection. Without guards a bare use would fabricate a (u64)-1-byte
slot — a silent miscompile (rule 7). opaque is illegal by-value in FOUR
aggregate positions (array element, struct field, tuple member, tagged-
union variant) + as a bare value, under size/align, and as a []opaque
element-index. LOUD guards, mirroring harec's scattered `size ==
SIZE_UNDEFINED` checks:

  1. bare value/local/param/return-by-value  (check.c clet, build_fn_type,
     top-level let; harec check.c:1524, :3931)
  2. opaque struct field                      (resolve_type N_TSTRUCT)
  3. [N]opaque array element                  (resolve_type N_TARRAY)
  3t. opaque tuple member                     (resolve_type N_TTUPLE;
      harec type_store.c:1147)
  3u. opaque tagged-union variant             (resolve_type N_TTAGGED;
      harec type_store.c:449)
  4. size(opaque) / align(opaque)             (size/align fold;
     harec check.c:2720)
  5. indexing []opaque                        (N_INDEX; harec check.c:384)

Detection is via the SIZE_UNDEFINED sentinel the guard consults, so the
sized forms `*opaque` (8B) and `[]opaque` (24B header) pass untouched.

Rule-10 per-guard stage placement:
  - Guards 1/2/3/3t/3u/5 are CSTAGE-ONLY. The wwstage check.ww is an
    AST-level approximation with no binding-size computation (g1) and no
    type-decl field/element/member validation walk (g2/g3/3t/3u); its
    N_INDEX indexresult returns the element type without consulting its
    size and defers invalid-index rejection to the cstage (g5). Same
    cstage-only neg-case precedent as 712_redecl / 708_param_shadow_mod.
  - Guard 4 is BOTH-STAGES. The wwstage HAS the size()/align() fold
    (astsize/astalign would otherwise fold opaque to a bogus 0 — a silent
    miscompile); twinned via astunsized + deffolderr. Because the wwstage
    has NO per-construction guards, its fold alone must catch every
    opaque-containing type: astunsized is RECURSIVE — a type is unsized
    iff it is opaque OR an aggregate (array/struct/tuple/tagged) with a
    recursively-unsized member. This both reaches the tuple/tagged folds
    AND closes the leaf-only size([4]opaque)/size(struct{x:opaque})→0
    leak. The cstage size/align guard stays leaf — the cstage rejects
    unsized aggregates at construction, so its fold only ever sees a leaf.

opaque is unused by the bootstrap, so every guard is inert on the
selfhost corpus — 990-997 stay byte-identical. Regenerates the w6c/wwdump
combined.ww (check.ww embed). New compile-fail probe 961_opaque_guards
(14 build-fails rows incl tuple/tagged/nested + 2 *opaque/[]opaque
positive controls); 960 positive probe unchanged.
2026-05-26 09:52:13 +09:00
3a18d2cfe6 wcc: add the opaque abstract type (kind + UNDEFINED sentinel + name-binding) (#108)
#108 sub-fold (a): TY_OPAQUE exists, is name-bindable, and carries an
UNDEFINED size sentinel. Mirrors the #85 `size` fold pattern at every
site, both stages (rule-10).

opaque is abstract + UNSIZED: prim()'d with size=align=SIZE_UNDEFINED
(NOT 0 — a 0 would let a bare `let x: opaque` fabricate a 0-byte local),
mirroring harec builtin_type_opaque (ref/harec/src/types.c:1446). ww had
no incomplete-size sentinel, so this fold ADDS one: cstage
`#define SIZE_UNDEFINED ((u64)-1)` (== harec types.h:58 (size_t)-1) and
wwstage `def SIZE_UNDEFINED: u64 = 18446744073709551615`.

Legal only behind indirection: `*opaque` (8B ptr) and `[]opaque` (24B
slice header) construct correctly because type_ptr/type_slice (and the
wwstage typeptr/typeslice) size themselves independent of the element.
opaque is deliberately absent from is-int/unsigned/num/float and from
the size-classification switches (let_emit_size / tupleelemslot /
fieldslotsize) on both stages — it only reaches those as TY_PTR/TY_SLICE.

The use-restriction GUARDS (reject bare opaque / size(opaque) / opaque
field / [N]opaque / []opaque-indexing), assignability, and cgen-verify
are the separate sub-folds (b)/(c)/(d) — NOT here.

opaque is unused by the bootstrap, so 990-997 stay byte-identical
(inert, like #85). Regenerates the w6c/wwdump combined.ww (typ.ww +
check.ww embedded). New probe 960_opaque_decl_run exercises `*opaque`
and `[]opaque` (.len/.ptr) behind indirection.
2026-05-26 09:02:08 +09:00
5e4d67d90a check: widen const def-ref to declared int type in def init (#113)
A def initializer whose rhs references another def -- `def INT_MIN: int
= I32_MIN;`, `def SIZE_MAX: size = U64_MAX;` -- failed to compile: an
N_IDENT->SK_DEF types as the referent's DECLARED type (i32, u64), so the
def-init assignability check (type_assignable) rejected i32 -> int /
u64 -> size, even though the value is a compile-time constant that fits.
This blocked faithful types/types::c limit defs (no cast in the Hare
source).

In a def initializer the rhs is a flexible constant. When it folds to a
compile-time integer (the #88 eval_def_const path: sibling/imported def
refs, casts, arithmetic) and the value fits the declared integer target,
re-flexibilize it to UNTYPED_INT so the existing untyped-int->typed
assignability path accepts it. This emulates Hare's flexible-constant
promotion (ICONST -> promote_flexible/lower_flexible,
ref/harec/src/types.c:860); def_cast_fits is the range check that keeps a
genuine out-of-range narrowing a loud "not assignable" error, never a
silent truncation (rule 7). It is strictly the const subset: the general
CONCRETE (non-const) integer widening Hare does at types.c:1021-1037 is
intentionally stricter in ww -- #115.

cstage-only: the wwstage checker (selfhost/cmd/wcc/check.ww, "let init /
return assignability") intentionally never checks def-init assignability
(it stays quiet, leaving full inference to the C side), so it never
rejected the widening -- the #88 stamp already laid the correct DATA row.
Relaxing the cstage aligns the richer side DOWN to the leaner side
(rule 10); both stages stamp the identical folded value, so emitted asm
is byte-identical. The bootstrap corpus has zero cross-prim-width def-ref
defs, so the new path is dead there and 990-997 are unperturbed.

Coverage: test/wcc/760_def_widen_const (i32->int neg, u64->size, byte-id
on each, cstage-only out-of-range narrowing fail-loud).
2026-05-26 02:39:35 +09:00
9a265a31f5 wcc: name-bind the size type in type position (#85 fold-2)
Resolve `size` -> TY_SIZE at the type-name resolver (C lookup_builtin /
ww tinfofornode's N_TNAME chain), mirroring uintptr, both stages. This
makes `size` writable as a type (`let x: size`, struct field, etc.),
the prerequisite for lib/types SIZE_MAX.

Twins every NAME-keyed uintptr arm in the wwstage so it behaves like
the cstage's kind-keyed Type switches (already TY_SIZE-aware from
fold-1): primtypesize + astalign (8B/8-align), primsize + letscalarprim
(8B scalar slot), isinttypeast + isnumerictname (int/numeric). rule-10
symmetric; dead on the size-free selfhost corpus so 990-997 stay byte-id.

Coexists with the size(T) size-of operator (separate c.top SK_FN seed +
N_CALL fold, NOT a type path) and `.size` field access (N_DOT); neither
touched. No c.top SK_TYPE "size" seed (would collide with the operator
seed at check.ww:96). Regenerates w6c/wwdump combined.ww (checker
embedded). New probe 957_size_type_run exercises type-position `size`
and the operator in one scope.
2026-05-26 01:55:54 +09:00
bd7181ae1f wcc: add the size primitive type (TY_SIZE), classify as unsigned int (#85)
fold-1: type exists + classifies; mirrors TY_UINTPTR at every site, both stages. size(T)/len() return types UNCHANGED (fold-2). Regenerates the 5 combined.ww (lib/ww embedded).
2026-05-26 01:26:03 +09:00
0d1ae17dd0 check: def rhs const-fold resolves sibling/imported defs + casts (#88)
ww top-level def rhs const-fold was literal-only (fold_int_literal at the codegen emit-defs step), so a def referencing another def, an imported def, or a cast was inexpressible -- blocking faithful types/types::c/math/strconv ports whose defs cross-reference.

Fold at CHECK time: a recursive eval_def_const (pass-2 N_DEF arm, both stages) resolves N_IDENT/N_DOT via the checker's existing scope lookup to the target def's rhs, evaluates N_BIN through a shared fold_binop core (factored out of eval_enum_value so both compile-time-int-eval paths share one wrap/shift/divide table), strips identity/widening casts, and stamps rhs -> N_INTLIT. cgen is UNTOUCHED -- its existing literal-emit lays the DATA row. Gated to fire only when the plain literal fold fails, so existing defs keep their node and emitted asm is byte-identical (990-997 unperturbed by construction).

Guards (rule 7): recursion depth cap fails loud on a def cycle (same/cross-module); a narrowing cast (rhs outside target range) fails loud rather than silently truncating. Both stages' eval_def_const stamp identically (shared fold_binop semantics) so the substituted literal -- and byte-id -- holds across stages (rule 10, at the check pass).

a1 (same-module) + a2 (cross-module imported def) land together: the driver concatenates imports into one flat scope. Coverage: test/wcc/732_def_const_fold.
2026-05-25 11:27:00 +09:00
fb4c567e0d wcc: populate str.sub = u8 -- Phase 2 F1 foundation (both stages)
str IS []u8 (#1 landed the 24B layout); F1 populates the element type
so the step-3 checker collapse can read str.sub instead of special-
casing TY_STR. No reader consumes str.sub yet, so this is byte-id-
neutral: every shared ->sub reader a TY_STR value can reach is
invariant under NULL->u8 -- u8 is unsigned + size-1, matching the
prior NULL-defaults (size->1, signed->0, isstr/istagged->false); the
only ->size derefs are guarded behind esz>1, which stays false for
str.

Verified inert: compiling a fixed source with the pre- and post-F1
compilers emits byte-identical asm on both stages; cross-stage
byte-id holds and full make test (135 tests incl. 990-997) is green.

cstage cmd/wcc/type.c, wwstage lib/ww/typ.ww; combined.ww regenerated
via the canonical make path.
2026-05-24 09:34:54 +09:00
1140a590bf wcc: str -> 24B {ptr,len,cap}, 3-reg ABI -- parity with []u8 (both stages)
A ww `str` becomes a 24-byte {ptr,len,cap} value, identical in layout to
[]u8 -- the enabling prerequisite for the Phase 2 `str == []u8` collapse.

Both stages, atomically:
- ty_str 16->24B; str value flows 3-reg AX/BX/CX (was 2-reg); str literals
  emit cap (=len).
- str in a tagged union grows to a 32B slot, using the AX/DX/CX/R8 4th-word
  path already used by 32B slice-variant unions -- str-variant is now
  structurally identical.
- tuple (scalar,str) return: 4-reg AX/DX/CX/R8 + 32B receive, extending the
  existing type-keyed return (no sret).
- str == []u8 for index and .ptr/.len/.cap, kind-gated where size-based
  dispatch collided at 24B; cstage and wwstage mirror exactly.
- table-driven runtime coverage: test/wcc/928_str_abi_run.c.

Cannot be split (rule 10/11): a 24B str and a 16B str cannot coexist across
the two compiler stages without breaking byte-identity, so the size change
and every dependent ABI/codegen site land in one atomic commit, both stages.

Known follow-ups (zero corpus impact, tracked): str-literal global .cap
static-init; >16B struct by-value (pre-existing); tagged-union
match-scrutinee stage divergence (pre-existing).
2026-05-24 08:11:14 +09:00
a376ec89eb lib/rt: rename rt_alloc → rt_malloc; rt.alloc → rt.malloc
Hare's canonical runtime allocator is rt::malloc with linker symbol
rt.malloc (ref/hare/rt/malloc.ha:27,78). ww kept the dot→underscore
Plan 9 convention (CLAUDE.md rule 4) so the linker symbol becomes
rt_malloc; the lib/rt exported function name becomes malloc; ww
callers say rt.malloc(...).

The language builtin keyword stays `alloc(T)!` — unchanged from Hare
(ref/hare/hare/lex/token.ha:21 ltok::ALLOC, parse/expr.ha:398
builtin()). The rename only touches the lowered linker symbol and the
exported function name behind it; the user-facing syntax for
heap-allocation is identical to Hare.

Surface:
- rt/alloc.s: TEXT rt_alloc → TEXT rt_malloc, labels updated
- lib/rt/malloc.ww: @symbol("rt_malloc") fn malloc(...) (was rt_alloc/alloc)
- rt/ensure.ww: local FFI decl + call site updated to malloc; `!` dropped
  on the direct FFI call (rt_malloc returns *void, not a tagged union)
- 18 .ww callers: rt.alloc(...) → rt.malloc(...)
- cstage cmd/wcc/check.c + wwstage selfhost/cmd/wcc/check.ww
  alloc-builtin suppression gate routes through ffi_resolve("malloc")
  for the lowering; the user-shadow check still keys on the BUILTIN
  KEYWORD "alloc" since that is what `alloc(...)` parses as. Adding
  "malloc" to the user-shadow check was unnecessary and was reverted
  during pre-commit review.
- cstage cmd/w6c/cgen.c: 2× ffi_resolve("alloc") → ffi_resolve("malloc")
- wwstage cgenexpr/cgenstmt: 2× ffiresolve(c, "alloc") → ffiresolve(c, "malloc")
- Test fixtures (700_e2e, 758_cgalloc_str_field, 990_selfhost, 992_w6l_ww,
  selfhost/test/tagged_ptr_ret.ww): updated inline ww sources to the new
  decl + call form

This is commit 2 of 3 in the lib/rt extraction (#38). Commit 3 closes
the OOM contract — return type becomes nullable *void and the builtin
lowering null-checks + propagates nomem.

Verified 132/132 + 995_self_rebuild byte-identity (5 wwstage tools
round-trip identical) + make clean cold rebuild.
2026-05-20 22:11:34 +09:00