Commit Graph

32 Commits

Author SHA1 Message Date
5cab22ecec lib/ww/parse: error on unknown top-level decl in parsefile fallback (#55)
parsefile's recovery fallback chewed an unrecognized top-level
construct to the next ';' without emitting an error or bumping p.errs,
so a typo'd keyword / stray token silently vanished from the AST and
the build succeeded rc=0 with the declared work gone — no link error
catches a dropped @test or unreferenced exported fn. The C twin
(parse.c:1395-1400) errorf+p->errs++ and rejects. Emit errmsg in the
fallback arm; wwstage now rejects in lockstep with cstage.

lib/ww embeds into the w6c/wwdump combined.ww snapshots; both regen'd.
989_unknowndecl_reject pins the reject-matrix on both driver twins.
2026-06-13 10:33:48 +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
79e647b34d lib/ww/parse: drop redundant type annotations (Wave-1)
Drop 112 redundant `let x: T = rhs` annotations where the rhs already
infers T (newnode→*node, p.curfile/curtext→str, p.curline/curcol→i32,
accepttok/== →bool, parse*→*node). stmt.ww (75) + parse.ww (37).
Regenerate the two embedders' combined.ww (w6c, wwdump). Byte-id-neutral:
cstage-w6c asm of each combined.ww is identical pre/post.
2026-06-02 21:27:09 +09:00
688275bfd6 lib,wcc,w6a,w6l: char-literals for remaining source magic decimals (Wave-1) 2026-06-02 21:02:26 +09:00
805c841f34 selfhost+lib/ww: N_TPARAM wrapper for tuple chains (A.6.2.0b-pre)
A.6.2.0b worker hit a real shared-`.next`-aliasing bug and stopped
per rule 7. Wwstage's N_TTUPLE chained element type ASTs via the
nodes' own `.next` field. `exprtype` routinely returns shared
nodes (sym.decl.lhs, struct field's `.lhs`, another N_TTUPLE's
`.list` element). Naive chain construction in the checker
corrupts source ASTs.

Introduce N_TPARAM = 67 as a chain wrapper for N_TTUPLE.list:

  - `.lhs` holds the (possibly-shared) element type AST.
  - `.next` chains within the parent N_TTUPLE.
  - Other fields unused; never appears outside N_TTUPLE.list.

Mirrors cstage's Tparam at cmd/wcc/check.c:1437-1451. Cstage
keeps it at the Type layer; wwstage has no separate type layer
for tuple chains so the wrapper sits at the AST. Hare's design
intent at ref/hare/hare/ast/type.ha:117 uses `[]*_type` slice-of-
pointer — same principle, slice-flavored.

Migrations:
  - lib/ww/ast.ww: kind + nkname + pr() unwrap (transparent for
    the 990 -a astprint byte-diff).
  - lib/ww/parse/parse.ww: parsetype N_TTUPLE construction wraps
    each element in N_TPARAM (sole construction site).
  - selfhost/cmd/wcc/check.ww: 4 readers (astalign, astsize,
    tinfofornode TY_TUPLE, exprtype N_DOT-tuple-positional). The
    last change retires the latent A.6.1.5b shared-`p` return.
  - selfhost/cmd/wcc/cgenutil.ww: slotsize TY_TUPLE arm.
  - selfhost/cmd/wcc/cgenexpr.ww: cgdot tuple-positional
    (size/load op + str-check).
  - selfhost/cmd/wcc/cgenstmt.ww: cglet TTUPLE init, cgmlet
    call-return walk, cgforrange elem-size + bind-walk.

Out of scope: N_TFN params, N_TTAGGED variants, N_TSTRUCT fields.
N_TFIELD already wraps struct fields; N_TFN/N_TTAGGED aren't
currently chain-mutated by checker synthesis. If they ever are,
the same pattern applies.

Unblocks A.6.2.0b stamp on a clean foundation. Retires task #16.

Verified 132/132 incl. 990 AST byte-diff (astprint unwrap) + 995
self-rebuild byte-identity.
2026-05-21 20:55:40 +09:00
353dffb5e8 lib/ww + wcc + w6c + wwdump: strip *arena cascade (γ-6)
amalloc has 0 callers post-γ-2; the *arena threaded through
newnode/newscope/newtype/prim/typesinit/type{ptr,slice,array,chan,
named}/lexinit/parserinit/joindotted/checkinit/arenau64tos/cgeninit
and the scope.a / tctx.a / lex.a / parser.a / checker.a / cgen.a
fields are vestigial.

Drop `import mem;` from 15 files, remove six struct fields, strip
*arena from 14 signatures, update ~120 call sites across lib/ww +
wcc + w6c + wwdump. selfhost/test/sym_link.ww fixture drops the
newarena/freearena probe; still exits 42 on scopedefine/scopelookup.
Both main.combined.ww auto-regenerated.

Comments retidied: typ.ww "once per arena" → "once per program";
parse.ww drops "arena-build" qualifier on joindotted; sym.ww drops
mem-sibling-imports rationale.

Verified 132/132 incl. 994_w6c_ww + 995_self_rebuild byte-identity
(the primary symmetric-stages gate).
2026-05-21 13:01:57 +09:00
6ff38f52bb lib/ww: migrate 5 typed amalloc sites to alloc(T{...})! (typed-9)
Phase 0 critical lib/ww/ gap (frontend used by BOTH cstage + wwstage,
not covered by phase0-mapper's selfhost/cmd/ audit). 5 typed-struct
amalloc sites + 1 γ pointer-array + 1 α byte buffer:

 - lib/ww/ast.ww newnode (node, 20 fields, was 208u64 over-sized)
 - lib/ww/typ.ww newtype (tinfo, 13 fields, was 112u64 over-sized)
 - lib/ww/typ.ww tinfocachebind (tinfocacheent, was 32u64 sizelint-ok)
 - lib/ww/sym.ww newscope (scope, 6 fields, was 64u64) + buckets γ
 - lib/ww/sym.ww scopedefineinmodule (sym, 10 fields, was 112u64)
 - lib/ww/parse/parse.ww joindotted (α []u8 + .ptr extract)

Retires 4 rule-7 over-sized amalloc workarounds plus a #36
tinfocacheent sizelint-ok. WHY-comments documenting the workarounds
are dropped (no longer applicable — alloc(T{...})! sizes from the
type table).

ast.ww newnode's `fval = 0: f64` carries a 3-line WHY comment naming
the 990_selfhost TK_FLOAT-count diff probe (lex.ww:382 precedent for
the same cast pattern). Bare `0.0` here would shift the dump-diff
token-input scope and break 990's byte-identity probe.

β grow loops in lib/ww/lex/lex.ww:570,592 deferred — separate sweep.

Verified 132/132 + 995_self_rebuild byte-identity. Net -63 lines.
2026-05-21 04:18:52 +09:00
9e0816e199 cmd+selfhost+lib+test: directory-as-module enumeration in driver (#22)
Replace the cmd/ww + selfhost driver's file-walk import resolver
with true directory enumeration. `import encoding.utf8;` now finds
the lib/encoding/utf8/ directory and concatenates every *.ww file
in it (excluding *test.ww and the driver's *.combined.ww artifacts)
in byte-wise sorted order, instead of just finding the single
lib/encoding/utf8/utf8.ww file. Mirrors Hare's
hare/module/srcs.ha:183 _findsrcs minus tag handling.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

111/111 ok (110 + new 738_module_decl sentinel). 995_self_rebuild
byte-id holds (ww2 == ww3 == ww4). All 5 frozen
selfhost/cmd/*/main.combined.ww regenerated under the new driver.
CLAUDE.md rule 5 amended with the language-layer divergence note.
2026-05-18 18:25:36 +09:00
19fb3a3b3c selfhost: parser tsuffix plumb + cgen N_UN peel for tagged-store variant index (closes #32)
Typed-int literal assigned into a tagged-union slot (`h.e = 42i64;` where
e: (i32 | i64)) wrote tag = 0 (the i32 slot) instead of tag = 1 (the i64
slot). Cstage was correct: parse.c parseprimary copies tok.tsuffix onto
N_INTLIT, check.c stamps node.type = ty_i64, and cg_widen_tagged_store →
cg_tag_for_variant walks variants matching by structural type_eq —
ty_i64 lands at index 1. Wwstage had two gaps:

1. The parser (lib/ww/parse/expr.ww parseprimary) read p.curuval and
   p.curtext from the current token but never the tsuffix field. Token-
   side capture has been in place since the lexer's `i8/i16/.../u64/f32/
   f64` glue suffix landed (lib/ww/lex/lex.ww sets out.tsuffix); the
   parser side was missed. So an N_INTLIT for `42i64` carried tsuffix=""
   into cgen. Mirror of cmd/wcc/parse.c parseprimary's `n->tsuffix =
   t.tsuffix` line. Same plumb for N_FLOATLIT.

2. Wwstage has no checker stage to stamp N_UN's type from its inner
   expression's type. `-42i64` parses as N_UN(MINUS, N_INTLIT(42,
   tsuffix="i64")) and rhstargetname stopped at N_UN, returning "" and
   falling through to taggedvariantindex's "first non-str variant"
   fallback — which picked tag 0 (i32) for any numeric rhs in an
   (i32|i64) union. Cstage's cunop returns the inner type for
   TK_MINUS / TK_PLUS / TK_TILDE so the N_UN gets ty_i64 stamped
   naturally; wwstage gets the equivalent via an explicit peel in
   rhstargetname, recursing into rhs.lhs for these three ops. The
   recursion also covers nested unary (`- -42i64`), which parseunary
   builds as N_UN over N_UN over N_INTLIT.

The lib/ww/parse change is mirrored in selfhost/cmd/{w6c,wwdump}/
main.combined.ww so the bootstrap snapshot stays consistent with the
working frontend source. parser.curtsuffix is a new str field; refill
copies t.tsuffix into it; parseprimary TK_INT / TK_FLOAT copy it onto
the new node before advance.

Cstage handled both `42i64` and `-42i64` correctly already; no cstage
mirror needed.

Test 694_tagged_store_intlit — eleven rows running on both stages: i64
lit in (i32|i64); i32 lit (existing-working pin); i64 lit in
(i32|i64|str) with the str fallback at tail; u8 lit at head of
(u8|i32|i64); i64 lit at tail of (u8|i32|i64) with a +100 marker so
mis-binding into u8 can't masquerade as success; negative-i64 lit
(N_UN MINUS peel + sign extension through match-arm bind);
unary-plus i64 lit (N_UN PLUS peel); bitwise-not i64 lit (N_UN TILDE
peel; `~0i64 == -1i64`); nested unary `- -42i64` (recursion through
two N_UN levels); direct `let x: ev = 42i64;` (cglet's tagged-init
code path, separate write site from cgassign's field-write);
negative-control str field (pins the existing str-fallback path
through rhstargetname).

Pre-fix run on wwstage: 8/11 rows fail (every typed-i64 case including
all three unary operators, nested unary, and the direct let-init);
cstage 11/11 pass. Post-fix: 22/22 across both stages. make test
41/41. Bootstrap ww2 == ww3 == ww4 byte-identical.
2026-05-15 01:39:37 +09:00
956a20701b w6c+selfhost+lib: zero-init multi-word no-rhs lets
`let x: T;` for str/slice/tuple/struct/tagged previously left the slot
holding stack garbage — only 8B-primitive slots were zeroed. This bit
`expectbindname` in lib/ww/parse: `let empty: str; *into = empty;` was
copying stack bytes (often a recently-vacated str descriptor) into the
caller's `id`, so wwstage emitted `_` discard nodes carrying random
text instead of "". Both stages now zero the full slot on no-rhs lets;
`[N]T` arrays keep the per-index-write contract.

Also tightens the two known buggy sites: parse.ww `expectbindname`
writes `*into = ""` directly, expr.ww `_` primary returns the bare
newnode (amalloc already zeroes).
2026-05-13 12:46:02 +09:00
b6cf68f2b8 w6c+selfhost+lib: Hare-style variadic call sites
Param-decl `name: T...` (Tparam.variadic=1, type []T), call-site
gather of N args into a fresh `[N]T`, forward via `xs...`, full
selfhost mirror, and lib/fmt graduated to the Hare shape.

Frontend:
  - parse: `T...` after a param's type stamps Node.op=TK_ELLIPSIS
    and breaks out (variadic must be last).
  - check: resolve_type N_TFN / build_fn_type wrap the param type
    as []T and set tp->variadic. N_CALL accepts either a tail of
    args assignable to T (gather) or a single `xs...` spread of
    []T (forward); both bypass the "too many args" check on the
    variadic slot.
  - type: type_eq compares Tparam.variadic.

Cgen (cstage):
  - call site: when the callee has a variadic last param,
    materialise the tail args into a frame-resident `[N]T` via
    localoff, write a 24B slice descriptor (ptr,len,cap), and
    splice a synthesised N_IDENT into args[] so the downstream
    widen/eval/pop loops see one slice slot. Tagged-element types
    route each store through cg_widen_tagged_store. Forwarding
    skips gather: the N_SPREAD wrapper is replaced with its inner
    slice expression. Empty form writes {nil,0,0}. args[] / widen[]
    bump from 16 to 64 to accommodate Hare's mixed-arg printers.

Selfhost mirror:
  - lib/ww/parse: `T...` mark on N_PARAM.op.
  - cgen: varargseq counter on Cg; scanlocals reserves
    @vararg_d_N + @vararg_sl_N per variadic call (seq recorded on
    N_CALL.uval so cgcall picks the same names). cgcall does the
    same gather/forward and N_IDENT splice. cgfnparams treats
    variadic params as 24B slice slots via a synthesised TSLICE
    tnode. pushargsrev skips the tagged-widen detection for
    variadic params (effective type is []T, not tagged).
  - rhstargetname now recognises N_TRUE/N_FALSE/N_RUNELIT and
    typed N_INTLIT so the variant-tag lookup finds bool/rune/iN
    variants instead of falling through to "first non-str" (which
    misassigned tag 0 to bool in tagged unions like formattable).

lib/fmt graduated: print/println/fprint/fprintln/errorln/fatal
take `args: formattable...`. Bare `error` (no -ln) is skipped —
the leaf name collides with strconv's `type error = !(invalid |
overflow)` under the driver's flat namespace.

Tests: 5 new e2e rows (plain gather, zero-arg, tagged element,
forwarding, fmt.println end-to-end). lib/CLAUDE.md workaround
paragraph replaced with the Hare-shape description.
2026-05-13 08:56:01 +09:00
9133251269 w6c+wcc: widen struct/tagged-subset, parse ... spread
Three tagged-union gaps:

  1. Struct-payload widening was broken at every site (call, let,
     assign, return, struct-field init). cg_widen_tagged_store now
     materialises str / scalar / struct-lit / struct-ident / tagged
     payloads at slot+8+field_off and writes the tag last. Call sites
     route through cg_widen_tagged_push (scratch slot + push high→low).

  2. Tagged → wider tagged widening forwarded the source tag verbatim.
     cg_widen_tag_remap emits a CMPQ-chain switch that translates each
     source variant index to the destination's, then zero-pads to the
     wider slot. type_eq grew a TY_TAGGED arm (was returning 1 for any
     two unions); type_assignable now accepts variant-subset and
     rejects the rest.

  3. `(...inner | T)` spread parses (cmd/wcc/parse.c, lib/ww/parse).
     Marks Node.op = TK_ELLIPSIS; resolve_type unwraps NAMED + flattens
     when the spread bit is set so aliases inline like Hare's
     tagged_type unwrap flag.

Selfhost mirror: spread parser ported. Cgen widen helpers not yet
mirrored — wwstage stays byte-identical to cstage on the existing
test corpus, but will emit wrong asm if user code uses the new
patterns (probe sp2 shows the divergence).

700_e2e: 9 new rows covering call/let/assign/return × struct +
tagged subset, plus the spread-flatten case.
2026-05-13 05:30:20 +09:00
e087c843e9 selfhost: port forrange — N_FORRANGE parser + cgen + tuple destructure 2026-05-12 16:21:13 +09:00
ce5d66e18a selfhost: port append + spread — N_SPREAD parser + rt_ensure builtin 2026-05-12 16:04:17 +09:00
f67c07cbae selfhost: port switch — N_SWITCH parser + cgen + scratch slot 2026-05-12 15:10:00 +09:00
5155ba55f3 selfhost: port float lex + expression cgen — feature parity with C
Lexer: `lexnum` now parses the digit/exponent tail into an f64 via a
new `parsef64` (decimal-only, integer-arith driver + pow-10 multiply,
no strtod). The IEEE bits are also stashed in tok.uval via pointer
reinterpret so cgen consumers stay integer-only.

Parser: TK_FLOAT → N_FLOATLIT, carrying both fval and uval. Parser
state grows curfval to plumb the lexer's f64 through refill.

cgen:
  - cgfloatlit reads n.uval and materialises X0 via the standard
    MOVQ-PUSHQ-MOVSD-ADDQ trampoline.
  - cglet, cgident, cgassign learn float-typed branches: MOVSS/MOVSD
    for locals; LEAQ-indirect MOVSS/MOVSD for globals.
  - cgbin handles ADDSD/SUBSD/MULSD/DIVSD (+ SS variants) and
    UCOMISD/UCOMISS-based comparisons. cgun handles float negate
    via the `0 - X0` shape C cgen uses.
  - cgcast routes int↔float and f32↔f64 through CVTSI2SD/CVTTSD2SI/
    CVTSD2SS/CVTSS2SD and their SS twins.
  - cgcall + pushargsrev push float args via SUBQ+MOVSD and pop into
    the X0..X7 stream, tracked by a per-class counter alongside the
    int DI..R9 stream. cgfnparams loads float params from the same
    stream.
  - emitletdataw bakes FLOATLIT init bits into DATAW (4B for f32,
    8B for f64).

Tests: smoke programs (literal init, reassign, arithmetic, fn args/
returns, casts) produce byte-identical asm through `w6c` and
`wwdump_ww -c`, and the resulting binary exits with the same value
whether compiled by the C or wwstage toolchain. Full `make test` is
26/26 and `make bootstrap` still reaches its byte-identical
ww2==ww3==ww4 fixed point.
2026-05-12 14:21:50 +09:00
922877309b ww+wcc: Hare-strict enum types — back out the int↔enum relaxation
Cascades the four enum kinds through every signature and local that
holds one of their values, then removes the type_assignable /
unify_arith relaxation that previously let bare i32 mix with the
named enum types.

Signature updates:
  - kwlookup() now returns `tkind` (not i32); tokname() takes `tkind`
  - accepttok / expecttok / bprec / isassignop take `tkind`
  - parsearglist's closekind is `tkind`
  - newtype / prim take `tykind`; scopedefine takes `skind`
  - newnode / nkname take `nkind`

Struct fields:
  - tok.kind is `tkind`; parser.curkind is `tkind`
  - node.kind is `nkind`; node.op is `tkind`
  - tinfo.kind is `tykind`; sym.skind is `skind`

Locals holding kinds across lex/parse/check/cgen are now typed with
their enum, including sentinel patterns like `let lkind: nkind =
nkind.N_NONE; if (...) lkind = tn.kind;`.

The selfhost cgen had a load-width bug exposed by this: fieldsize()
fell back to 8 bytes for any TNAME that wasn't a struct or primitive.
For a tkind-typed field that gave `MOVQ (BX), AX` instead of `MOVL`,
diverging from the C cgen on tok.kind / parser.curkind / etc. Two
fixes:
  - fieldsize now consults the enum registry and returns the storage
    type's size (4 for `enum i32`)
  - collectenums runs before collectstructs in cgfile so the registry
    is populated when registerstruct asks for field sizes

All 22 tests stay green; 990/993/995 byte-identity probes pass with
the strict typing in place.
2026-05-12 05:04:33 +09:00
3affe01705 selfhost: graduate N_* defs to nkind enum 2026-05-12 04:54:23 +09:00
408ea2a322 ww+wcc: graduate selfhost TK_* defs to tkind enum
`type tkind = enum i32 { TK_NONE = 0, TK_EOF = 1, ... TK_LAST = 86 }`
replaces the 87-line `def TK_*: i32 = N` cluster in lib/ww/lex/tok.ww.
Numeric values explicit so 990_selfhost's byte-diff against the C-side
`Tkind` enum still passes.

All ~270 reference sites in lib/ww and selfhost/cmd/{wcc,wwdump}
sed-renamed `TK_X` → `tkind.TK_X`. Struct fields (`tok.kind`,
`parser.curkind`) intentionally kept as `i32` — making them `tkind`
shifted some byte-positions in the cgen output and broke 990/993/995
byte-identity probes without an obvious win.

To make the rename non-cascading on every signature, type_assignable
and unify_arith in cmd/wcc/check+type relax to allow enum ↔ int
mixing when storage matches (a `tkind` value flows into an `i32`
slot and vice versa, no explicit cast). This deviates from Hare's
strict enum semantics; doc'd as an explicit pragmatic relaxation
for the compiler's internal enum-shaped kinds. External user code
can still get the type-safety benefit if they declare their
parameters with the enum type.

combined.ww files regenerated by ww build.
2026-05-12 04:50:36 +09:00
5bf30f209c selfhost: mirror enum tokens + AST + parsetype branch
Parses byte-identical to the C frontend on enum sources (verified
via `diff` of wwdump vs wwdump_ww -a on an enum-using fixture).
The selfhost side reserves the slot in the AST and TY_* enums so
later check.ww and cgen mirror work doesn't shift numeric IDs.

Codegen-side enum support (member-value folding in cgdot, enum↔int
pass-through in cgtypeassert) is deferred — current selfhost sources
don't use enum, so 990_selfhost / 995_self_rebuild stay green.

main.combined.ww in wwdump/ and w6c/ regenerated by `ww build` as
a side effect of `make wwstage`.
2026-05-12 04:20:59 +09:00
906e17b128 wcc: @test marker attributes + runner
Hare-style `@test fn check_foo() void = { ... }` now parses. The
attribute is recognised by making the args list optional in
parseattrs: `@symbol("rt_syscall")` still requires the parens;
`@test` doesn't. Same change mirrored in lib/ww/parse/decl.ww.

The runner (test/wcc/910_at_test.c) scans a fixture for
`@test fn IDENT(`, synthesises a wrapper `main()` that calls each
test fn, builds it via `ww run`, and asserts exit 0. A failing
@test would either explicitly call abort or trip a runtime trap
(div-by-zero, etc.) and the whole driver exits non-zero.

The 910_at_test target sits alongside the existing C-side test
binaries; `make test` now runs 20 tests instead of 19.

Fixture: test/wcc/data/attest_pass.ww exercises two passing tests
(simple arithmetic and a match-with-yield).
2026-05-12 03:14:20 +09:00
f267f99a2b wcc: match-as-expression with yield
`match (e) { ... }` can now sit in expression position, with each
arm using `yield expr;` to produce the match's value:

    let v = match (r) {
    case let n: i32 => yield n + 1;
    case let s: str => yield s.len: i32 + 100;
    };

TK_YIELD keyword + N_YIELD AST node, both appended at the tail of
their enums to keep prior numeric values byte-stable for the
wwdump-diff gates.

Checker: cexpr for N_MATCH walks each arm's body looking for the
first N_YIELD; the match's type is the unified yield type (or
ty_void if no yield, preserving the statement-form semantics).
Mismatched arm yields are flagged.

Cgen: a yield-target stack (separate from the loop break stack)
holds each enclosing match's end label. N_YIELD evaluates its
expression into AX (and BX for str) and JMPs to the topmost entry.
cgmatch pushes its end label on entry and pops on exit.

Selfhost mirror: lib/ww/lex/tok.ww kwtab+name, lib/ww/ast.ww
N_YIELD def+print, lib/ww/parse/stmt.ww yield-stmt; selfhost cgen
adds a yieldbuf to the cgen struct and a cgyield helper. Verified
end-to-end: a yield-using program compiled via the wwstage cgen
matches the C-cgen build's exit code.
2026-05-12 03:08:00 +09:00
dc8405429e selfhost: ?/! postfix in parser + cgen; use ! in lex.ww escape path
Selfhost parser (lib/ww/parse/expr.ww) recognises postfix `?` and
`!` at the same level as `as`/`is`/`:`. Selfhost cgen
(selfhost/cmd/wcc/cgenexpr.ww) emits matching code: cmp AX against
the success tag (0 in legacy mode), branch over the propagate /
abort path, then unwrap (DX → AX, CX → BX for str). Mirrors the C
cgen but without the tag-remap loop — none of the selfhost code
that uses `?` today needs cross-shape remapping.

lib/ww/lex/lex.ww \\x escape handling switched from 5-line match
blocks to one-liners: ascii.digitval(c: rune)!. Both digits are
already validated by isxdigit above; the void variant is
unreachable, so `!` collapses correctly. 995 fixed-point gate
verifies the selfhost cgen produces the same `!` codegen as C cgen.
2026-05-12 02:45:27 +09:00
594a2bad62 wcc: Hare-style !T error marker on tagged-union variants
A type prefixed with `!` is flagged as an error variant. When any
variant in a tagged union carries the flag, `?` propagation uses
those (and only those) as the error subset; the unflagged variant
is the success type. The legacy "first variant = success" rule still
applies when no `!`-flag is present, so existing code keeps working.

- TK_NOT in parsetype → N_TBANG wrapper (lhs = inner type expr).
  Appended to Nkind tail for wwdump-diff byte stability.
- resolve_type N_TBANG: wraps primitives in a fresh Type copy so the
  iserror bit doesn't taint shared globals like ty_str/ty_i32; flips
  the bit in place on NAMED (already unique per alias decl).
- Type.iserror; type_named and typedecl inherit it from under.
- New check.c helpers: tagged_has_errflag, tagged_is_error_variant,
  tagged_success_type. N_TRYPROP uses them to find the error subset
  and verify each error variant is propagatable to the enclosing
  return.
- cgen mirrors with cg_tagged_success_tag + cg_variant_is_error.
  `?` compares AX against the success tag (no longer always 0) and
  remaps each error variant's tag for the enclosing fn. `!` aborts
  on any non-success tag.

strconv.invalid and strconv.overflow now use `!`-flagged shape
(`!i32` and `!void`) — visible signal in the API surface that they
are error types, matching Hare. The (i64 | invalid | overflow)
return shape and behavior are unchanged for callers; their match
arms still bind the same way.

Selfhost: lib/ww/parse/parse.ww recognises `!T` and emits N_TBANG.
The selfhost typechecker and cgen ignore the flag — none of the
selfhost sources use `!`, so byte-identity gates are unaffected.
The selfhost mirror catches up when there's a source using it.
2026-05-12 02:39:54 +09:00
fd45aedf6c os: graduate filesize/readall/writeall to (i64 | oserror)
`type oserror = i64` carries -errno (Hare's errors::errno-shaped
named-i64). The three convenience wrappers move off the i64 = -1
sentinel and onto the tagged-union surface.

Callers updated across the selfhost (wwdump, w6c, w6a, w6l, ww
driver). The slurp paths in w6c/w6a/w6l/wwdump now match on the
filesize and readall results; the ELF-emitting writeall sites in
w6a/obj.ww are wrapped through two small local helpers (`wrn` for
"wrote N bytes ok?", `wrdrop` for fire-and-forget) so the existing
11-callsite write loop stays readable.

selfhost/test/smoke.ww kept using raw os.read instead of
os.readall: the 990 cgen-match probe compiles smoke.ww standalone
(no `use` expansion), and cross-module type references like
`os.oserror` can't be resolved in that mode.

Two selfhost-side gaps surfaced and got plugged:
- lib/ww/parse/parse.ww parsetype now collapses dotted type names
  (`pkg.Type` → single N_TNAME with the joined string), mirroring C
  parsetype's dotted-path loop. Local `joindotted` helper because
  there's no arena-based string-concat in the selfhost lib yet.
- selfhost/cmd/wcc/check.ww name-resolver applies the dotted-prefix
  rule from cmd/wcc/check.c's resolve_typename: split at the last
  dot, look up the head as a `use` import, then the leaf as a type.
2026-05-12 02:25:40 +09:00
4085742853 strconv: graduate to (T | invalid | overflow); add void expression
`type invalid = i32` (payload: byte index of first bad rune; mirrors
Hare's strconv::invalid = !size) and `type overflow = void` (Hare's
overflow = !void). stoi64/stou64 now return these instead of the
str-error placeholder. atoi64 dropped — lib/CLAUDE.md says graduate
in one go, don't keep both shapes around.

To produce the void variant payload, `void` is now a real
expression literal (TK_VOID kw, N_VOIDLIT). It evaluates to ty_void;
codegen emits MOVQ $0, AX. Both kinds are appended at the tail of
their enums to keep prior numeric values byte-stable for the
wwdump-diff fixtures.

check_file reorder: USE declarations are now installed in pass 1
alongside the type-decl placeholders so dotted type references
(`strconv.invalid` from a typedecl body) resolve. DEF/FN/LET silently
overwrite a USE-occupied slot — matches the old behavior where USE
silently no-op'd when a same-name fn/def existed (the conflict
manifested in selfhost main.combined.ww at `use parse;` colliding
with `export fn parse(a)`).

selfhost mirror: lib/ww/lex/tok.ww kwtab+name; lib/ww/ast.ww
N_VOIDLIT def+print; lib/ww/parse/{expr,parse}.ww TK_VOID handling;
selfhost/cmd/wcc/cgenexpr.ww N_VOIDLIT codegen.
2026-05-12 02:02:08 +09:00
dd188ca460 ww: add Hare-style is/as postfix ops on tagged unions
`e is T` returns bool (variant tag == T's index); `e as T` unwraps
to T or exit(1) on mismatch. Postfix, same precedence as `:` cast.
TK_IS / N_TYPETEST / N_TYPEASSERT appended at the tail of their
enums so every prior numeric value stays unchanged — the
990_selfhost wwdump-diff stays byte-clean.

Cgen mirrors the match-case slot-based load (tag at +0, value at
+8/+16), so an N_IDENT tagged-union local works just like a
match scrutinee. Selfhost cgen inlines the slot resolution
because the wwstage cgen drops sign bits on `*i32` output
parameters in this position.

Renames `errors.is` -> `errors.equal` (the only naming collision;
the existing comment already noted it shared shape with
strings.equal/bytes.equal).
2026-05-11 23:21:08 +09:00
c5f30f2fce ww: cgen trap batch (def-str field, chained-ptr write, scalar+str tuple ABI) 2026-05-11 20:49:21 +09:00
07f4e93b2b ww: rename streq_local -> streqlocal 2026-05-11 19:50:32 +09:00
6219a47c6f ww: hare-feature batch (_, const, [_]T, ..., size/offset, assert, for-else) 2026-05-11 19:38:12 +09:00
8ffe6dbee6 ww: split parse.ww into parse/{parse,expr,stmt,decl}.ww submodule 2026-05-11 16:48:58 +09:00