Commit Graph

19 Commits

Author SHA1 Message Date
b8b32ab80c cstage+selfhost+test: refuse same-block let / param redecl (#32)
cmd/wcc/check.c silently accepted `let a; let a;` in the same block
and similar redecls. Pre-#27 the localoff dedup masked it; post-#27
last-write-wins via head-first localfind. Surfaced by worker-27
during the #27 review.

Cstage: 5 guard sites (check_scope_define-NULL → err) covering
N_LET block-bind, N_MLET tuple binders (incl. same-tuple
`let (a,a)`), N_FORRANGE tuple binders, top-level let, fn param.
Voice: "<kind> '<name>' redeclared in same scope" for inner;
"duplicate let %s" for top-let, matching the existing
"duplicate <kind>" idiom at 1812/1851/1872.

Wwstage: TODO(#11) comments at the 4 mirror sites (installdecl,
N_FORRANGE, N_LET, installparams). Full enforcement waits on the
checkfile pass per rob.

**Unmasked by #32 (worth flagging):** selfhost/cmd/wcc/cgenexpr.ww
cgcall had `let callee: *node = n.lhs;` twice at fn-body scope
(copy-paste, identical value). Pre-fix silent-redecl absorbed it;
post-fix the new guard rejects. Removed the second decl — outer
`callee` stays visible across the intermediate block.

Test 712 (redecl): 10 rows (6 neg + 4 pos), cstage-only per rob.
Negative rows cover all 5 guard sites + same-tuple-dup. Positive
rows pin the legal counter-shapes (cross-block, name-only bucket,
forrange body, mcase-per-arm).

Test 300 row 34 ("shadowing in inner scope; same scope flagged")
was incorrectly asserting the bug; flipped to expect "redeclared"
and added a sibling row pinning cross-block shadow stays ok. Test
709's `same_block_redecl_pin` canary (explicitly documented as
flipping under #32) removed; pointer to 712 left in its place.
2026-05-16 11:17:20 +09:00
c9bbfcb6a6 cstage+selfhost+test: refuse let/param shadow of imported module (#19)
When `use fmt;` is in scope and a local/param named `fmt` shadows it,
`fmt.X` in the body silently resolved to the str-typed value sym and
emitted `CALL AX` through str.ptr → runtime crash. Surfaced during
#15 (lib/log's printfln family); worked around by renaming the param
`fmt`→`format`.

Per rob + user, option (C): "value names and module names are
disjoint." Refuse the shadow at the decl site. Single rule, no
non-local reasoning, no silent footgun if a future lib/X exports a
new leaf.

cstage: src_imports walks file->list for N_USE entries (skipping
self-imports where u->module == u->str — same-module fixtures like
lib/fmt/fmttest.ww carry these); check_module_shadow runs before
each SK_PARAM / SK_VAR scope_define (param, clet, mlet, forrange
single + tuple, mcase). Wwstage mirror in check.ww; wwdump-only
diagnostic today, full enforcement waits on #11 checkfile pass.

Bootstrap byte-id holds — no codegen change. One source patch in
selfhost/cmd/w6a/main.ww renames an outer `let asm: asm_;` to `s` to
sidestep task #27 (cstage localoff scope-blind dedup); unrelated to
#19 but the new rule's first run flagged it as a self-shadow.

Test 708 (param_shadow_mod): 4 rows — neg_param (param shadow errs
at fn decl line), neg_let (let shadow errs at let decl), pos_rename
(rename compiles + runs), pos_selfimp (in-module use is skipped).
4 wired sites without dedicated rows deferred to task #28.

Follow-up: lib/log can revert format→fmt now that the silent
crash is impossible.
2026-05-16 08:39:35 +09:00
7f60ebbe44 cstage+test: SK_USE→SK_X promotion sets use_alias
In cmd/wcc/check.c the pass-1.5 SK_USE→SK_DEF/SK_FN/SK_VAR promotion
sites forgot to set prev->use_alias = 1 when the imported module's
top-level decl shadowed the SK_USE leaf in flat scope. Downstream
dot-prefixed lookups (resolve_typename L77, N_DOT L709) gate the
module-head walk on (SK_USE || use_alias), so `mod.flag` resolution
fell through to "unknown type". The SK_TYPE precedent at L1660 had
the line; the three sister sites at L1709/L1722/L1736 now do too,
in the same one-line shape and field-set order.

The wwstage selfhost/cmd/wcc/check.ww uses coexistence rather than
in-place promotion: SK_USE and same-leaf SK_TYPE/FN/DEF/VAR live as
separate entries differentiated by sym.mod, and scopelookupinmodule's
mod-filter already disambiguates dotted lookups — no use_alias flag
needed, so the cstage bug is structurally non-reachable there. An
architectural note at installdecl documents this divergence-by-design
and warns against porting the flag (adding a field to `sym` changes
its size and risks the wwstage cgen amalloc-undersize trap).

Audit covered every SK_USE→SK_X promotion path in check.c (4 sites:
SK_TYPE already-correct as precedent, SK_DEF/SK_FN/SK_VAR fixed). The
surfacing case was lib/fnmatch: `fn fnmatch(...)` shadows the SK_USE
leaf, so `fnmatch.flag` failed in worker-fnmatch's WIP — that test
(972_fnmatch_run) now flips PASS as live integration proof.
test/wcc/699_use_promote_alias.c pins all four rows with a single
table-driven driver (type/fn/def/var → use mod; let m: mod.flag =
mod.flag.A; return m: i32, expecting exit 42 per row). 995_self_rebuild
byte-identity holds.
2026-05-15 15:06:37 +09:00
e6045f3ada w6c+selfhost: same-module preference for bare-leaf lookup 2026-05-15 11:24:33 +09:00
66d6408cbe w6c+selfhost: cross-module same-leaf type disambiguation via Sym.mod 2026-05-15 10:26:20 +09:00
73c0cf4c78 w6c+selfhost: match-arm scope/spill + alloc(structlit) sugar
Three gaps in the wwstage cgen relative to C w6c, plus a matching
C-side bug surfaced along the way.

cgmatch (selfhost) handles non-ident scrutinees: `match (foo())` now
spills the AX:DX:CX return triple into a 24B `@match_spill` slot
rather than reading garbage off BP+0. scanlocals counts the slot so
the prologue SUBQ stays in sync. For N_CALL we recover the return
type via fnretlookup so nullable dispatch picks the pointer-vs-null
discriminator. Mirrors @match_spill in cmd/w6c/cgen.c N_MATCH.

cgcall (selfhost) special-cases `alloc(structlit{...})`: lower to
rt_alloc(totsize) + per-field MOV* at the struct's field offsets,
mirroring cmd/w6c/cgen.c's existing path. Previously the structlit
fell into pushargsrev and produced wrong code.

check.ww's N_MCASE branch now pushes a fresh scope around each arm
body. Without this, `case let e: str` inside a fn with an outer
`let e: *T` collided with scopedefine's same-scope dedup, the inner
binding silently dropped, and references to `e` inside the arm
resolved through the outer type.

Both cgens save/restore the locals head around case bodies so arm
binds (and nested arm-body lets) don't leak past the arm — code
after the match resolves names back through the outer scope.

w6c gains `local_alloc`: same as `localoff` minus the dedup. N_MATCH
case-bind allocation switches to it. Previously `let e: *T` (8B)
shadowed by `case let e: str` (16B) reused the outer 8B slot and the
inner str.len store overflowed into the saved BP, segfaulting on
return.

Tests 26/26.
2026-05-12 18:40:41 +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
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
077d0f95ed selfhost: graduate SK_* defs to skind enum 2026-05-12 04:52:42 +09:00
cb78abf9e9 selfhost: is/as validity + let/return assignability checks
Three more structural checks from C check.c ported to selfhost,
at the AST level (no resolved tinfo).

is/as validity: e is T / e as T require e's declared type to be a
tagged union and T to name a variant. Mirrors the case-variant
check that just landed.

let init-type and return-type assignability: a new exprtype helper
infers an AST type-node for literal/ident/call/cast/?/as/is
expressions; isassignable approximates C type_assignable on the
shapes we can resolve — exact match, untyped numeric → typed
numeric, untyped nil → ptr/slice/chan/fn, variant inclusion, and
two-primitive-mismatch.

isassignable returns (ok, confident). When confident=false the
check emits no error — better to miss a real bug than fire a
false positive on a binary-op expression we can't infer. This
keeps existing selfhost code clean while still catching the
common typo cases (let x: bool = 42; return "hi" from i32 fn).

Naming: all new helpers follow Plan 9 run-together convention per
CLAUDE.md (`typeeqast`, `isassignable`, `exprtype`, ...). Earlier
work that used snake_case helpers (`case_variant_in`,
`check_match_exhaustive`, ...) got the same treatment — bulk
renamed in this commit.

Five new rows in 950_selfcheck exercise the new checks
(is-not-a-variant, two let mismatches, return mismatch, plus the
case-variant row already there).
2026-05-12 03:49:09 +09:00
751271a6bd wcc: case T => variant validity check (C + selfhost)
`match (u) { case T => ... }` where T isn't a variant of u was
silently accepted by both checkers. The cgen would emit a tag
comparison against an index that never appears, leaving the arm
unreachable — wasted code that's almost always a bug or typo.

C check.c now mirrors the existing is/as rule for match arms:
each `case T` and each alt of multi-pattern `case T1 | T2` is
checked against the scrutinee's variant list via variant_present.

selfhost check.ww gets the same shape with AST-level type_eq_ast
comparison. Both checks land in the same scope-aware pass that
already runs exhaustiveness and ? subset.

New test rows in 300_check (C side) and 950_selfcheck (selfhost
side) exercise both single-pattern and multi-pattern alt typos.
The 950 driver's err_present detector picks up the new
"case: not a variant" prefix.
2026-05-12 03:37:21 +09:00
68bd8197d5 selfhost: port match exhaustiveness, ?-subset, !-flag checks to check.ww
The selfhost checker did name resolution only — anything tagged-
union-shaped sailed through silently. The C check.c implements
three structural checks; this commit mirrors them at the AST level
in selfhost/cmd/wcc/check.ww:

1. Match exhaustiveness: every variant of the scrutinee's tagged
   union must be covered by a case arm (incl. multi-pattern alts)
   or a default arm. Operates on the scrutinee's declared type
   (N_TTAGGED via N_IDENT's sym.decl.lhs).

2. ? subset propagation: each error variant of the operand's type
   must be a variant of the enclosing fn's return type. Enclosing
   return must itself be a tagged union when the operand has any
   errors.

3. !-flag semantics: in flag-aware mode (any variant marked `!T`),
   error subset = flagged variants. Legacy mode (no flags) =
   everything-but-first. is_error_variant unifies both rules.

No tinfo / type-inference work: the checks read declared AST type
nodes directly. `resolvealias` chases N_TNAME → typedecl body to
handle aliased tagged unions. `type_eq_ast` does structural
comparison on the subset of type-expression shapes the checks
encounter (TNAME by string, TPTR/TSLICE/TCHAN recursive).

Folded into resolvewalk rather than a separate second pass, so the
checks see the same per-statement scope state as resolve. fnret is
threaded through resolvefnbody so ? can find the enclosing return.

New test/wcc/950_selfcheck.c — five rows exercising each error path
(missing variant, non-tagged enclosing, missing error subset
member, the flag-aware happy path, the flag-aware missing-error
case). Test suite now reports 21 ok.
2026-05-12 03:24:25 +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
fa070b6d07 wcc: tagged-union foundations (never, void, flatten, exhaust)
- `never` bottom type: TY_NEVER, assignable to anything; size 0.
- Type-set normalization for N_TTAGGED in resolve_type:
  - flatten nested anonymous (A|B)|C → (A|B|C); named aliases stay
    nominal (not flattened through)
  - dedup duplicates (NAMED pointer-id; others structural)
  - drop `never` variants
  - collapse single-element set: (T|never) → T, (T|T) → T
- Match exhaustiveness: error when a variant is unhandled and no
  default arm covers it. Multi-pattern `case T1 | T2 =>` counts
  each alt.
- (T | void) optionals: bare `return;` from a tagged-union-returning
  fn emits the void variant's tag (payload undefined; void size 0).

selfhost mirrored: TY_NEVER constant + tynever in tctx + seedprim
entry; voidvariantindex helper; cgreturn bare-return handling.
2026-05-12 01:31:35 +09:00
7ed6b39744 selfhost/cmd/wcc/typ: drop ty_ prefix underscore on tctx primitives 2026-05-11 14:38:20 +09:00
6204c4cd27 selfhost/cmd/wcc: drop snake_case from 119 cross-cutting identifiers 2026-05-11 14:37:45 +09:00
2c33228b7e ww: rename toolchain to w-prefix + hare-style build/run/test driver
Plan 9-style w-prefix on the per-arch tools, disambiguating from the
real Plan 9 6c/6a/6l in ref/plan9front/:

    cmd/wwc/      → cmd/wcc/        libwwc.a → libwcc.a
    cmd/6{c,a,l}  → cmd/w6{c,a,l}   binary names too
    test/wwc/     → test/wcc/       6 test files w/ w6 prefix
    selfhost/cmd  mirror in lockstep
    bootstrap/amd64/{w6c,w6a,w6l}   snapshot binaries (gitignored)
    WW_6{C,A,L}   → WW_W6{C,A,L}    env-var overrides

Plan 9 source-tree refs ("Plan 9 6c shape", ref/plan9front/, etc.)
preserved. Hare-style driver, both C and ww sides:

    ww test [path]   discover *_test.ww in a directory module, run
                     each; single-file mode for `ww test foo.ww`
    Module-by-name   `ww build foo` resolves to foo.ww or foo/foo.ww
                     via search path (cwd : -I dirs : $WW_LIB)
    Default-to-cwd   `ww build` / `ww test` build the cwd module
    Run pass-through `ww run path arg1 arg2` reaches the program

lib/os: getcwd (79) and getdents64 (217) syscalls power `.` resolution
and directory enumeration on the ww side.

Makefile: wwstage tool deps now include lib/os/os.ww (+ lib/strconv
for wwdump_ww) so lib/* edits force their rebuild instead of leaving
stale binaries — surfaced when test 995 first failed against a stale
w6c_ww built before the lib/os additions.

Test 993 byte-identical parity gate (C-side ww vs ww-side ww_ww on a
build corpus) stays green; all 19 tests pass.
2026-05-11 13:49:27 +09:00