diff --git a/CLAUDE.md b/CLAUDE.md index 0c70ee37..b729480b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,7 @@ 3. cmd/ is the C bootstrap toolchain (wcc frontend lib, w6c/w6a/w6l per-arch, ww driver); selfhost/ is the ww reimplementation 4. All C code must strictly align with plan 9 coding style 5. lib/ is the standard library; follow Hare APIs (signatures, layout, error idioms) — consult ref/hare/ before designing new modules. Package declaration form is Go-style explicit `package foo;` and imports use `import foo;` (executables declare `package main;`) per language design (rob-pike + plan-9 sensibility); lib/ API surface still mirrors Hare. -6. ref/hare and ref/plan9front are read-only references — consult before inventing data shapes or syntax +6. ref/hare and ref/plan9front are read-only references — consult before inventing data shapes or syntax. Carve-out: the ww compiler frontend is ONE package `lib/ww/syntax/` (tok+lex+ast+sym+typ+parse), a deliberate Go-over-Hare departure modelled on Go's `cmd/compile/internal/syntax` rather than ref/hare/hare's `{ast,lex,parse}` split. Rationale: the split's only payoff is third-party reuse (an IDE wanting `ast` without the parser), which ww has zero of — its frontend is consumed by exactly one client, the `wcc` backend. Consolidating dissolves the cross-package export sprawl (a fn over an unexported sibling type re-triggers sep-build's check_exported_type). Rule 6/12 still bind the internal data shapes (AST kinds, token model, lexer/parser state mirror ref/hare/hare); only the module DECOMPOSITION collapses. The surviving export surface is `syntax`'s public API consumed by `wcc` (a small Hare-faithful set). Ref: USER-approved #74; spec .ai/rob-frontend-reorg.md (rob, drew2 fidelity-confirmed). 7. No workarounds. If a bug forces a workaround, STOP and report with a precise repro. Document any retained divergence at the site with a pointer to the filed task. Never silent. 8. Comments are WHY-only. Never narrate WHAT the code does — names carry the WHAT. Comment only non-obvious WHY: a constraint, a divergence from a reference, a citation to a filed task. 9. Hare-fidelity over convenience. No ad-hoc extensions, renames, or convenience wrappers in lib/. Cite ref/hare// for every signature ported. Carve-out: the Hare `_unsafe` suffix convention is dropped wholesale (ww is C/Plan-9-lineage, an unmanaged systems language — no GC, no "safe" baseline to be unsafe relative to). bytes→str is a pure reinterpret (`strings.frombytes`, renamed from Hare's `fromutf8_unsafe`); validation is opt-in via `utf8.validate(b)?` at the IO source, never wrapped per-construction. Rationale: ref/hare/strings/utf8.ha:10,22 — the suffix flags Hare's managed-bytes-safety axis, which ww doesn't have. The honest name (`frombytes`) reserves `fromutf8` for a future true validating helper. Carve-out: lib/ tests use Go's external-test-package idiom (`package _test;` + `import ;`), a sanctioned Go-over-Hare departure — ww hard-rejects self-import (full Go), and Hare's same-package `@test` colocation now has a ww analogue: in-package white-box `@test` files (`package ;`, colocated) are sanctioned alongside the external black-box `package _test;` split — Go's `foo`/`foo_test` model — now that the non-T `@test` drop landed (#6, harec check.c:3941, 08a76cf; #5 closed). Rule 5/9 binds lib/ *API* surface, not test-authoring style. Ref: Go `foo_test` convention; task #16. diff --git a/Makefile b/Makefile index a102ca7b..0ddf3e1a 100644 --- a/Makefile +++ b/Makefile @@ -117,13 +117,13 @@ $(OBJ)/w6l/%.o: cmd/w6l/%.c cmd/w6l/l.h | $(OBJ)/w6l $(CC) $(CFLAGS) -Icmd/w6l -c -o $@ $< # ---- ww-side wwdump (the lexer port, exercised by 990_selfhost) -------- -# Built via the user-facing ww driver. The introspection ports (lex, -# tok, ast, parse, typ, sym) live in lib/ww/; the compiler-only bits -# (check, cgen*) stay in selfhost/cmd/wcc/. Output named wwdump_ww +# Built via the user-facing ww driver. The frontend (lex, tok, ast, +# parse, typ, sym) is the `syntax` package in lib/ww/syntax/; the +# compiler-only bits (check, cgen*) stay in selfhost/cmd/wcc/. Output named wwdump_ww # to avoid colliding with the C-side wwdump in $(BIN). $(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \ - lib/ww/lex/lex.ww lib/ww/lex/tok.ww lib/ww/ast.ww \ - lib/ww/parse/parse.ww lib/ww/parse/expr.ww lib/ww/parse/stmt.ww lib/ww/parse/decl.ww lib/ww/typ.ww lib/ww/sym.ww \ + lib/ww/syntax/lex.ww lib/ww/syntax/tok.ww lib/ww/syntax/ast.ww \ + lib/ww/syntax/parse.ww lib/ww/syntax/expr.ww lib/ww/syntax/stmt.ww lib/ww/syntax/decl.ww lib/ww/syntax/typ.ww lib/ww/syntax/sym.ww \ selfhost/cmd/wcc/check.ww \ selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \ selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \ @@ -134,18 +134,16 @@ $(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \ @mkdir -p $(BIN)/wwdump_ww.d cd $(BIN)/wwdump_ww.d && $(CURDIR)/$(BIN)/ww build \ -I $(CURDIR)/lib/ww \ - -I $(CURDIR)/lib/ww/lex \ - -I $(CURDIR)/lib/ww/parse \ -I $(CURDIR)/selfhost/cmd/wcc \ $(CURDIR)/selfhost/cmd/wwdump/main.ww mv $(BIN)/wwdump_ww.d/main $@ # ---- ww-side w6c (compiler port, exercised by 994_w6c_ww) ------------- -# Thin driver: parse + cgen. The frontend (lex/parse/ast/...) lives in -# lib/ww/; cgen + check live in selfhost/cmd/wcc/. +# Thin driver: parse + cgen. The frontend (lex/parse/ast/...) is the +# `syntax` package in lib/ww/syntax/; cgen + check live in selfhost/cmd/wcc/. $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \ - lib/ww/lex/lex.ww lib/ww/lex/tok.ww lib/ww/ast.ww \ - lib/ww/parse/parse.ww lib/ww/parse/expr.ww lib/ww/parse/stmt.ww lib/ww/parse/decl.ww lib/ww/typ.ww lib/ww/sym.ww \ + lib/ww/syntax/lex.ww lib/ww/syntax/tok.ww lib/ww/syntax/ast.ww \ + lib/ww/syntax/parse.ww lib/ww/syntax/expr.ww lib/ww/syntax/stmt.ww lib/ww/syntax/decl.ww lib/ww/syntax/typ.ww lib/ww/syntax/sym.ww \ selfhost/cmd/wcc/check.ww selfhost/cmd/wcc/wwi.ww \ selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \ selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \ @@ -156,8 +154,6 @@ $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \ @mkdir -p $(BIN)/w6c_ww.d cd $(BIN)/w6c_ww.d && $(CURDIR)/$(BIN)/ww build \ -I $(CURDIR)/lib/ww \ - -I $(CURDIR)/lib/ww/lex \ - -I $(CURDIR)/lib/ww/parse \ -I $(CURDIR)/selfhost/cmd/wcc \ $(CURDIR)/selfhost/cmd/w6c/main.ww mv $(BIN)/w6c_ww.d/main $@ @@ -3606,8 +3602,6 @@ nocc: @# Pass -I $(CURDIR)/lib so `use os` etc. resolve to source. @cd $(NOCC_BIN) && ./ww build \ -I $(CURDIR)/lib/ww \ - -I $(CURDIR)/lib/ww/lex \ - -I $(CURDIR)/lib/ww/parse \ -I $(CURDIR)/selfhost/cmd/wcc \ -I $(CURDIR)/lib $(CURDIR)/selfhost/cmd/w6c/main.ww && mv main w6c_ww1 @cd $(NOCC_BIN) && ./ww build -I $(CURDIR)/selfhost/cmd/w6a \ diff --git a/lib/ww/ast.ww b/lib/ww/syntax/ast.ww similarity index 99% rename from lib/ww/ast.ww rename to lib/ww/syntax/ast.ww index cdc0b900..7f15cffb 100644 --- a/lib/ww/ast.ww +++ b/lib/ww/syntax/ast.ww @@ -1,4 +1,4 @@ -// lib/ww/ast.ww — port of cmd/wcc/ast.c (Node defs + printer). +// lib/ww/syntax/ast.ww — port of cmd/wcc/ast.c (Node defs + printer). // // Status: AST printer is fully ported. Constructor `newnode` is here. // The parser (parse.ww) is currently minimal — see its file header. @@ -7,11 +7,10 @@ // by value (8 *node pointers + 2 strs + a few ints), so callers always // hand around `*node`. Only `newnode` allocates and returns a *node. -package ww; +package syntax; import os; import strconv; -import tok; // ---- Nkind ------------------------------------------------------------ // diff --git a/lib/ww/asttest.ww b/lib/ww/syntax/asttest.ww similarity index 98% rename from lib/ww/asttest.ww rename to lib/ww/syntax/asttest.ww index 60e71b8a..6462b4df 100644 --- a/lib/ww/asttest.ww +++ b/lib/ww/syntax/asttest.ww @@ -1,6 +1,6 @@ // asttest — functional-equivalence pin for [[nkname]] after the // if-ladder → switch fold (struct fold S2). Run with -// `ww run lib/ww/asttest.ww`. +// `ww run -I lib/ww lib/ww/syntax/asttest.ww`. // // nkname is checked against every nkind value (the full ladder the // switch replaced) plus the out-of-band fallback ("?"). A failing row @@ -10,7 +10,7 @@ package main; -import ast; +import syntax; fn checkname(k: nkind, want: str) void = { diff --git a/lib/ww/parse/decl.ww b/lib/ww/syntax/decl.ww similarity index 98% rename from lib/ww/parse/decl.ww rename to lib/ww/syntax/decl.ww index a23dbf9e..f8957462 100644 --- a/lib/ww/parse/decl.ww +++ b/lib/ww/syntax/decl.ww @@ -1,10 +1,9 @@ -// lib/ww/parse/decl.ww — declaration parsing, split out of parse.ww. +// lib/ww/syntax/decl.ww — declaration parsing, split out of parse.ww. -package parse; +package syntax; import os; import strings; -import tok; // `import encoding.utf8;` — the driver resolves the dotted path to // a directory; only the leaf (`utf8`) is needed downstream as the diff --git a/lib/ww/parse/expr.ww b/lib/ww/syntax/expr.ww similarity index 99% rename from lib/ww/parse/expr.ww rename to lib/ww/syntax/expr.ww index 2db0852c..09eb31f5 100644 --- a/lib/ww/parse/expr.ww +++ b/lib/ww/syntax/expr.ww @@ -1,9 +1,8 @@ -// lib/ww/parse/expr.ww — expression parsing, split out of parse.ww. +// lib/ww/syntax/expr.ww — expression parsing, split out of parse.ww. -package parse; +package syntax; import os; -import tok; // streqlocal — str-to-str compare. Inlined here to avoid a cross- // module `use sym;` for one call site. diff --git a/lib/ww/lex/lex.ww b/lib/ww/syntax/lex.ww similarity index 99% rename from lib/ww/lex/lex.ww rename to lib/ww/syntax/lex.ww index 049efc78..ce879cde 100644 --- a/lib/ww/lex/lex.ww +++ b/lib/ww/syntax/lex.ww @@ -1,4 +1,4 @@ -// lib/ww/lex/lex.ww — port of cmd/wcc/lex.c. +// lib/ww/syntax/lex.ww — port of cmd/wcc/lex.c. // // The DFA, the helpers, and the order of decisions all mirror the C // version exactly. The 990_selfhost test diffs the resulting token @@ -11,10 +11,8 @@ // in shape, not in observable behaviour. Token kind values stay // numerically identical. -package lex; +package syntax; -// Sibling import (tok) auto-resolves via task #22 dir-enum when -// callers `import lex;` (which dir-enums lib/ww/lex/). import os; import ascii; import strings; diff --git a/lib/ww/parse/parse.ww b/lib/ww/syntax/parse.ww similarity index 98% rename from lib/ww/parse/parse.ww rename to lib/ww/syntax/parse.ww index c0cd2570..3cbd6121 100644 --- a/lib/ww/parse/parse.ww +++ b/lib/ww/syntax/parse.ww @@ -1,4 +1,4 @@ -// lib/ww/parse/parse.ww — port of cmd/wcc/parse.c (entry + plumbing). +// lib/ww/syntax/parse.ww — port of cmd/wcc/parse.c (entry + plumbing). // // Split into Hare-style submodule: parse.ww (here) holds the parser // struct, lexer plumbing, parsetype, parsefile (entry). Expression, @@ -10,14 +10,12 @@ // stores the current token as flat primitive fields rather than a // nested `tok` struct; `refill` copies a freshly lexed token in. -package parse; +package syntax; -// Sibling imports (expr, stmt, decl) auto-resolve via task #22 -// dir-enum when callers `import parse;` (which dir-enums -// lib/ww/parse/). +// Sibling files (expr, stmt, decl) are the same `package syntax`, +// auto-resolved via task #22 dir-enum of lib/ww/syntax/. import os; import strings; -import tok; type parser = struct { l: *lex, diff --git a/lib/ww/parse/stmt.ww b/lib/ww/syntax/stmt.ww similarity index 99% rename from lib/ww/parse/stmt.ww rename to lib/ww/syntax/stmt.ww index c9879cf6..2f118e15 100644 --- a/lib/ww/parse/stmt.ww +++ b/lib/ww/syntax/stmt.ww @@ -1,9 +1,8 @@ -// lib/ww/parse/stmt.ww — statement parsing, split out of parse.ww. +// lib/ww/syntax/stmt.ww — statement parsing, split out of parse.ww. -package parse; +package syntax; import os; -import tok; fn parseletlocal(p: *parser) *node = { let pf = p.curfile; diff --git a/lib/ww/sym.ww b/lib/ww/syntax/sym.ww similarity index 96% rename from lib/ww/sym.ww rename to lib/ww/syntax/sym.ww index 59109a4b..0a1e8d3e 100644 --- a/lib/ww/sym.ww +++ b/lib/ww/syntax/sym.ww @@ -1,10 +1,10 @@ -// lib/ww/sym.ww — port of cmd/wcc/sym.c. +// lib/ww/syntax/sym.ww — port of cmd/wcc/sym.c. // // Per-scope hashtable, chained to the parent. Lookup walks up. // Plan 9 / Hare flavoured. Duplicate definitions in the same scope // return nil; the caller flags the error. -package ww; +package syntax; // Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind. type skind = enum i32 { @@ -108,11 +108,10 @@ export fn scopelookup(s: *scope, name: str) *sym = { // Same FNV bucket + hashnext chain + parent walk as scopelookup, with // an `skind == SK_TYPE` filter. Used to disambiguate the bare-TNAME // vs imported-module-bareword collision: when scopelookup returns the -// SK_USE sym for a leaf that ALSO names a type (e.g. `tok` struct -// declared in lib/ww/lex/tok.ww with `package lex;` while -// `import tok;` registers a same-name SK_USE), the resolver needs -// the type entry — the struct's mod may differ from the leaf (lex/tok -// pair) so scopelookupinmodule(c, leaf, leaf) won't find it. +// SK_USE sym for a leaf that ALSO names a type (a same-name `import X;` +// SK_USE shadowing a struct X declared in another module), the resolver +// needs the type entry — the struct's mod may differ from the leaf so +// scopelookupinmodule(c, leaf, leaf) won't find it. // // #58/#50: within each scope, Pass-1 prefers an SK_TYPE whose `sym.mod` // matches `mod`; Pass-2 falls back to the first SK_TYPE regardless of diff --git a/lib/ww/lex/tok.ww b/lib/ww/syntax/tok.ww similarity index 99% rename from lib/ww/lex/tok.ww rename to lib/ww/syntax/tok.ww index cafd8557..47b23b3a 100644 --- a/lib/ww/lex/tok.ww +++ b/lib/ww/syntax/tok.ww @@ -1,4 +1,4 @@ -// lib/ww/lex/tok.ww — port of cmd/wcc/tok.c plus the Tkind / +// lib/ww/syntax/tok.ww — port of cmd/wcc/tok.c plus the Tkind / // Tok / Pos shapes from cmd/wcc/ww.h. // // Token kind values must stay numerically equal to the C side: the @@ -9,7 +9,7 @@ // Bottom of file: tokprint, which emits one token per line in a // format identical to cmd/wcc/tok.c:tokprint(). -package lex; +package syntax; import os; import strconv; diff --git a/lib/ww/lex/toktest.ww b/lib/ww/syntax/toktest.ww similarity index 99% rename from lib/ww/lex/toktest.ww rename to lib/ww/syntax/toktest.ww index f338356c..9ae74fba 100644 --- a/lib/ww/lex/toktest.ww +++ b/lib/ww/syntax/toktest.ww @@ -1,7 +1,7 @@ // toktest — functional-equivalence pin for [[tokname]], [[kwlookup]] // (struct fold S1) and [[tokprint]]/fputq (struct fold S9, the // if-ladder → switch folds in tok.ww). -// Run with `ww run -I lib/ww/lex lib/ww/lex/toktest.ww`. +// Run with `ww run -I lib/ww lib/ww/syntax/toktest.ww`. // // tokname is checked against every tkind value (the full ladder the // switch replaced, plus the unknown-kind fallback); kwlookup is @@ -22,8 +22,7 @@ package main; import os; -import tok; -import lex; +import syntax; fn checkname(k: tkind, want: str) void = { diff --git a/lib/ww/typ.ww b/lib/ww/syntax/typ.ww similarity index 99% rename from lib/ww/typ.ww rename to lib/ww/syntax/typ.ww index 98494ddb..8dedc17a 100644 --- a/lib/ww/typ.ww +++ b/lib/ww/syntax/typ.ww @@ -1,4 +1,4 @@ -// lib/ww/typ.ww — port of cmd/wcc/type.c. +// lib/ww/syntax/typ.ww — port of cmd/wcc/type.c. // // Status: full structural port. The C version uses module-globals for // the primitive types (tyvoid, tyi32, …); ww doesn't have writable @@ -6,7 +6,7 @@ // the checker passes around explicitly. typesinit fills the tctx // once per program. -package ww; +package syntax; import os; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 9b7724d7..db34166f 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -6417,413 +6417,1063 @@ export fn strerror(e: error) str = { return strings.dup(""); }; -//ww:module-reset -// lib/ww/lex/tok.ww — port of cmd/wcc/tok.c plus the Tkind / -// Tok / Pos shapes from cmd/wcc/ww.h. +//ww:module syntax +// lib/ww/syntax/ast.ww — port of cmd/wcc/ast.c (Node defs + printer). // -// Token kind values must stay numerically equal to the C side: the -// 990_selfhost test diffs ww-side wwdump output against C-side -// wwdump output, byte-for-byte. Reordering this list shifts the -// integers and breaks the diff. +// Status: AST printer is fully ported. Constructor `newnode` is here. +// The parser (parse.ww) is currently minimal — see its file header. // -// Bottom of file: tokprint, which emits one token per line in a -// format identical to cmd/wcc/tok.c:tokprint(). +// Calling-convention shim: same as tok/lex — `node` is too big to pass +// by value (8 *node pointers + 2 strs + a few ints), so callers always +// hand around `*node`. Only `newnode` allocates and returns a *node. -package lex; +package syntax; import os; import strconv; -import strings; -// ---- tkind ------------------------------------------------------------ -// Mirror of the C `Tkind` enum in cmd/wcc/ww.h. Numeric values are -// explicit and must stay in sync — the 990_selfhost test diffs wwdump -// output against the C side, byte for byte. - -type tkind = enum i32 { - TK_NONE = 0, - TK_EOF = 1, - TK_ERR = 2, - TK_IDENT = 3, - TK_INT = 4, - TK_FLOAT = 5, - TK_RUNE = 6, - TK_STR = 7, - - TK_FN = 8, - TK_LET = 9, - TK_DEF = 10, - TK_IF = 11, - TK_ELSE = 12, - TK_FOR = 13, - TK_SWITCH = 14, - TK_CASE = 15, - TK_RETURN = 16, - TK_USE = 17, - TK_TYPE = 18, - TK_STRUCT = 19, - TK_DEFER = 20, - TK_BREAK = 21, - TK_CONTINUE = 22, - TK_EXPORT = 23, - TK_PROC = 24, - TK_CHAN = 25, - TK_NIL = 26, - TK_TRUE = 27, - TK_FALSE = 28, - TK_AS = 29, - TK_STATIC = 30, - TK_MATCH = 31, - TK_CONST = 32, - TK_UNDER = 33, - - TK_LPAREN = 34, - TK_RPAREN = 35, - TK_LBRACE = 36, - TK_RBRACE = 37, - TK_LBRACK = 38, - TK_RBRACK = 39, - TK_COMMA = 40, - TK_SEMI = 41, - TK_COLON = 42, - TK_DOT = 43, - TK_ELLIPSIS = 44, - TK_DOTDOT = 45, - TK_AT = 46, - TK_QUESTION = 47, - - TK_ASSIGN = 48, - TK_PLUSEQ = 49, - TK_MINUSEQ = 50, - TK_STAREQ = 51, - TK_SLASHEQ = 52, - TK_PERCENTEQ = 53, - TK_AMPEQ = 54, - TK_PIPEEQ = 55, - TK_CARETEQ = 56, - TK_LSHIFTEQ = 57, - TK_RSHIFTEQ = 58, - - TK_PLUS = 59, - TK_MINUS = 60, - TK_STAR = 61, - TK_SLASH = 62, - TK_PERCENT = 63, - TK_AMP = 64, - TK_PIPE = 65, - TK_CARET = 66, - TK_TILDE = 67, - TK_LSHIFT = 68, - TK_RSHIFT = 69, - - TK_EQ = 70, - TK_NEQ = 71, - TK_LT = 72, - TK_LE = 73, - TK_GT = 74, - TK_GE = 75, - - TK_AND = 76, - TK_OR = 77, - TK_NOT = 78, - - TK_LARROW = 79, - TK_ARROW = 80, - TK_FATARROW = 81, - - // Tail-appended values — keeps every prior TK_* numeric value - // stable for the 990_selfhost byte-diff against the C side. - TK_IS = 82, - TK_VOID = 83, - TK_YIELD = 84, - TK_ENUM = 85, - TK_MODULE = 86, // `module foo;` — directory-as-module decl - TK_MODRESET = 87, // `//ww:module-reset` driver bundle boundary: - // reset curmod to "" before a package-less file - // (#16 option-B; cstage TK_MODRESET twin) - TK_MODPATH = 88, // `//ww:module ` driver import - // boundary; decls mangle on the path, not the - // leaf `package` clause (M1 #22; cstage twin) - TK_LAST = 89, -}; - -// ---- Pos / Tok -------------------------------------------------------- +// ---- Nkind ------------------------------------------------------------ // -// `pos` is used at error-reporting boundaries; we always pass it via -// *pos so the value never gets struct-copied (w6c can't yet copy a -// 24-byte struct). -// -// `tok` is flat — file/line/col live directly on the token rather than -// nested inside a `pos` field. Same reason: nested struct field -// assignment isn't supported, and flat primitives are. +// Mirror of cmd/wcc/ww.h Nkind. Values must stay numerically equal so +// the AST diff probe in 990_selfhost works. -type pos = struct { - file: str, - line: i32, - col: i32, +// Mirror of the C `Nkind` enum in cmd/wcc/ww.h. Numeric values are +// explicit and must stay in sync — the 990_selfhost test diffs +// astprint against the C side byte-for-byte. Tail-appended entries +// (TYPETEST onward) preserve every prior N_* value. +type nkind = enum i32 { + N_NONE = 0, + + N_INTLIT = 1, + N_FLOATLIT = 2, + N_STRLIT = 3, + N_RUNELIT = 4, + N_TRUE = 5, + N_FALSE = 6, + N_NIL = 7, + N_IDENT = 8, + + N_BIN = 9, + N_UN = 10, + N_CALL = 11, + N_INDEX = 12, + N_DOT = 13, + N_CAST = 14, + N_STRUCTLIT = 15, + N_ARRLIT = 16, + N_FIELD = 17, + N_ASSIGN = 18, + N_ALLOC = 19, + N_FREE = 20, + N_RECV = 21, + N_SLICE = 22, + N_SPREAD = 23, + + N_BLOCK = 24, + N_EXPRSTMT = 25, + N_LET = 26, + N_RETURN = 27, + N_IF = 28, + N_FOR = 29, + N_FORRANGE = 30, + N_DEFER = 31, + N_BREAK = 32, + N_CONTINUE = 33, + N_SWITCH = 34, + N_CASE = 35, + + N_FILE = 36, + N_USE = 37, + N_DEF = 38, + N_TYPEDECL = 39, + N_FNDECL = 40, + N_PARAM = 41, + + N_TNAME = 42, + N_TPTR = 43, + N_TSLICE = 44, + N_TARRAY = 45, + N_TFN = 46, + N_TSTRUCT = 47, + N_TFIELD = 48, + N_TCHAN = 49, + + N_ATTR = 50, + N_TTUPLE = 51, + N_TTAGGED = 52, + N_TUPLE = 53, + N_MATCH = 54, + N_MCASE = 55, + N_TRYPROP = 56, + N_TRYUNW = 57, + N_MLET = 58, + N_MASSIGN = 59, + + N_TYPETEST = 60, + N_TYPEASSERT = 61, + N_VOIDLIT = 62, + N_TBANG = 63, + N_YIELD = 64, + N_TENUM = 65, + N_TENUMMEMBER = 66, + + // N_TPARAM — chain wrapper for N_TTUPLE.list elements. Mirror of + // cstage's Tparam (cmd/wcc/check.c:1437-1451) lifted to the AST so + // `exprtype` can return shared element-type nodes (sym.decl.lhs, + // struct field's .lhs, another N_TTUPLE's .list element) without + // corrupting source ASTs by reusing their .next. .lhs holds the + // element type AST (possibly shared); .next chains within the + // parent N_TTUPLE.list. Cstage keeps Tparam at the Type-layer; ww + // has no separate type layer for tuple chains, so the wrapper sits + // at the AST layer. Other node fields are unused. Never appears + // outside an N_TTUPLE.list; astprint unwraps transparently to keep + // the 990 -a byte-diff against cstage. + N_TPARAM = 67, + + N_LAST = 68, }; -type tok = struct { - kind: tkind, - file: str, // path of the source the token came from - line: i32, - col: i32, - text: str, // arena-owned token text (tkind.TK_IDENT, tkind.TK_STR, tkind.TK_ERR) - uval: u64, // tkind.TK_INT, tkind.TK_RUNE - fval: f64, // tkind.TK_FLOAT - tsuffix: str, // typed numeric literal suffix or empty +// ---- Node ------------------------------------------------------------- + +type node = struct { + kind: nkind, + file: str, + line: i32, + col: i32, + op: tkind, // for nkind.N_BIN / nkind.N_UN / nkind.N_ASSIGN + str: str, + uval: u64, + fval: f64, + lhs: *node, + rhs: *node, + cond: *node, + body: *node, + els: *node, + list: *node, + next: *node, + attr: *node, + exported: i32, // bool — `export` keyword present + type_: *void, // filled in by checker; type.ww treats it as *tinfo + tsuffix: str, // typed numeric literal suffix ("i32", "u64", ...) + nmod: str, // originating module from `// MODULE: foo`; "" if none + usepath: str, // on an N_USE: full dotted IMPORT path vs leaf alias + // in `str` (M1 #22); "" otherwise + imported: i32, // M1 #22: decl reached via `//ww:module ` import + // boundary (vs root/primary); gates root-only bare main }; -// ---- keyword lookup --------------------------------------------------- +export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = { + // fval cast-init: 990's wwdump TK_FLOAT diff requires this file + // to tokenise identically through C and ww (lex.ww:382 has the + // same workaround for the cstage %g-formats vs ww-skips divergence). + let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, type_=nil, tsuffix="", nmod="", usepath="", imported=0})!; + return n; +}; -// keep alphabetised, so kwlookup is easy to read — mirrors the C twin -// cmd/wcc/tok.c:18-47. Two parallel arrays, not a [N]kwent array-of- -// struct: a str *inside* an aggregate element is the filed #18 follow-up -// (str-in-aggregate). `let`, not `def`: #18's module-level [N]str static -// init is scoped to the DATAW (`let`) directive (A_DATAR needs a DATAW -// holder); `def [N]str` is the same filed follow-up. kwkinds is a plain -// [N]tkind enum byte-array (pre-#18 path). Explicit [30], NOT [_]: -// [_] static-init silently miscompiles to a zero-length array in-tree -// (probe at cc69daf — len() returns 0, no diagnostic); filed bug. -let kwnames: [30]str = [ - "as", "break", "case", "chan", "const", "continue", "def", "defer", - "else", "enum", "export", "false", "fn", "for", "if", "is", - "import", "let", "match", "nil", "package", "proc", "return", - "static", "struct", "switch", "true", "type", "void", "yield", -]; -let kwkinds: [30]tkind = [ - tkind.TK_AS, tkind.TK_BREAK, tkind.TK_CASE, tkind.TK_CHAN, - tkind.TK_CONST, tkind.TK_CONTINUE, tkind.TK_DEF, tkind.TK_DEFER, - tkind.TK_ELSE, tkind.TK_ENUM, tkind.TK_EXPORT, tkind.TK_FALSE, - tkind.TK_FN, tkind.TK_FOR, tkind.TK_IF, tkind.TK_IS, - tkind.TK_USE, tkind.TK_LET, tkind.TK_MATCH, tkind.TK_NIL, - tkind.TK_MODULE, tkind.TK_PROC, tkind.TK_RETURN, tkind.TK_STATIC, - tkind.TK_STRUCT, tkind.TK_SWITCH, tkind.TK_TRUE, tkind.TK_TYPE, - tkind.TK_VOID, tkind.TK_YIELD, -]; +// ---- printer ---------------------------------------------------------- -// kwlookup — returns the matching TK_* keyword kind for a byte run, -// or tkind.TK_NONE if it's an ordinary identifier. Linear scan over the -// table, matching cmd/wcc/tok.c:kwlookup (N=30, no hash). -export fn kwlookup(p: *u8, n: i32) tkind = { - let cand: str; - cand.ptr = p; - cand.len = n; +export fn nkname(k: nkind) str = { + switch (k) { + case nkind.N_NONE: return "none"; + case nkind.N_INTLIT: return "int"; + case nkind.N_FLOATLIT: return "float"; + case nkind.N_STRLIT: return "str"; + case nkind.N_RUNELIT: return "rune"; + case nkind.N_TRUE: return "true"; + case nkind.N_FALSE: return "false"; + case nkind.N_NIL: return "nil"; + case nkind.N_IDENT: return "id"; + + case nkind.N_BIN: return "bin"; + case nkind.N_UN: return "un"; + case nkind.N_CALL: return "call"; + case nkind.N_INDEX: return "index"; + case nkind.N_DOT: return "dot"; + case nkind.N_CAST: return "cast"; + case nkind.N_STRUCTLIT: return "structlit"; + case nkind.N_ARRLIT: return "arrlit"; + case nkind.N_FIELD: return "field"; + case nkind.N_ASSIGN: return "assign"; + case nkind.N_ALLOC: return "alloc"; + case nkind.N_FREE: return "free"; + case nkind.N_RECV: return "recv"; + case nkind.N_SLICE: return "slice"; + case nkind.N_SPREAD: return "spread"; + + case nkind.N_BLOCK: return "block"; + case nkind.N_EXPRSTMT: return "exprstmt"; + case nkind.N_LET: return "let"; + case nkind.N_RETURN: return "return"; + case nkind.N_IF: return "if"; + case nkind.N_FOR: return "for"; + case nkind.N_FORRANGE: return "forrange"; + case nkind.N_DEFER: return "defer"; + case nkind.N_BREAK: return "break"; + case nkind.N_CONTINUE: return "continue"; + case nkind.N_SWITCH: return "switch"; + case nkind.N_CASE: return "case"; + + case nkind.N_FILE: return "file"; + case nkind.N_USE: return "use"; + case nkind.N_DEF: return "def"; + case nkind.N_TYPEDECL: return "typedecl"; + case nkind.N_FNDECL: return "fn"; + case nkind.N_PARAM: return "param"; + + case nkind.N_TNAME: return "tname"; + case nkind.N_TPTR: return "tptr"; + case nkind.N_TSLICE: return "tslice"; + case nkind.N_TARRAY: return "tarray"; + case nkind.N_TFN: return "tfn"; + case nkind.N_TSTRUCT: return "tstruct"; + case nkind.N_TFIELD: return "tfield"; + case nkind.N_TCHAN: return "tchan"; + + case nkind.N_ATTR: return "attr"; + case nkind.N_TTUPLE: return "ttuple"; + case nkind.N_TTAGGED: return "ttagged"; + case nkind.N_TUPLE: return "tuple"; + case nkind.N_MATCH: return "match"; + case nkind.N_MCASE: return "mcase"; + case nkind.N_TRYPROP: return "tryprop"; + case nkind.N_TRYUNW: return "tryunw"; + case nkind.N_MLET: return "mlet"; + case nkind.N_MASSIGN: return "massign"; + + case nkind.N_TYPETEST: return "typetest"; + case nkind.N_TYPEASSERT: return "typeassert"; + case nkind.N_VOIDLIT: return "voidlit"; + case nkind.N_TBANG: return "tbang"; + case nkind.N_YIELD: return "yield"; + case nkind.N_TENUM: return "tenum"; + case nkind.N_TENUMMEMBER: return "tenummember"; + case nkind.N_TPARAM: return "tparam"; + case nkind.N_LAST: return "last"; + }; + return "?"; +}; + +fn ind(fd: i32, d: i32) void = { let i: i32 = 0; - for (i < len(kwnames)) { - if (strings.compare(cand, kwnames[i]) == 0) { - return kwkinds[i]; - }; + for (i < d) { + os.write(fd, " ".ptr, 2u64); i += 1; }; - return tkind.TK_NONE; }; -// ---- tokname ---------------------------------------------------------- -// -// Returns the canonical printable spelling for a token kind. Matches -// the C tokname()'s output exactly so wwdump output diffs cleanly. - -export fn tokname(k: tkind) str = { - switch (k) { - case tkind.TK_NONE: return ""; - case tkind.TK_EOF: return "EOF"; - case tkind.TK_ERR: return "ERR"; - case tkind.TK_IDENT: return "IDENT"; - case tkind.TK_INT: return "INT"; - case tkind.TK_FLOAT: return "FLOAT"; - case tkind.TK_RUNE: return "RUNE"; - case tkind.TK_STR: return "STR"; - - case tkind.TK_FN: return "fn"; - case tkind.TK_LET: return "let"; - case tkind.TK_DEF: return "def"; - case tkind.TK_IF: return "if"; - case tkind.TK_ELSE: return "else"; - case tkind.TK_FOR: return "for"; - case tkind.TK_SWITCH: return "switch"; - case tkind.TK_CASE: return "case"; - case tkind.TK_RETURN: return "return"; - case tkind.TK_USE: return "import"; - case tkind.TK_TYPE: return "type"; - case tkind.TK_STRUCT: return "struct"; - case tkind.TK_DEFER: return "defer"; - case tkind.TK_BREAK: return "break"; - case tkind.TK_CONTINUE: return "continue"; - case tkind.TK_EXPORT: return "export"; - case tkind.TK_PROC: return "proc"; - case tkind.TK_CHAN: return "chan"; - case tkind.TK_NIL: return "nil"; - case tkind.TK_TRUE: return "true"; - case tkind.TK_FALSE: return "false"; - case tkind.TK_AS: return "as"; - case tkind.TK_IS: return "is"; - case tkind.TK_VOID: return "void"; - case tkind.TK_YIELD: return "yield"; - case tkind.TK_STATIC: return "static"; - case tkind.TK_MATCH: return "match"; - case tkind.TK_CONST: return "const"; - case tkind.TK_UNDER: return "_"; - case tkind.TK_ENUM: return "enum"; - case tkind.TK_MODULE: return "package"; - case tkind.TK_MODRESET: return "//ww:module-reset"; - case tkind.TK_MODPATH: return "//ww:module"; - - case tkind.TK_LPAREN: return "("; - case tkind.TK_RPAREN: return ")"; - case tkind.TK_LBRACE: return "{"; - case tkind.TK_RBRACE: return "}"; - case tkind.TK_LBRACK: return "["; - case tkind.TK_RBRACK: return "]"; - case tkind.TK_COMMA: return ","; - case tkind.TK_SEMI: return ";"; - case tkind.TK_COLON: return ":"; - case tkind.TK_DOT: return "."; - case tkind.TK_ELLIPSIS: return "..."; - case tkind.TK_DOTDOT: return ".."; - case tkind.TK_AT: return "@"; - case tkind.TK_QUESTION: return "?"; - - case tkind.TK_ASSIGN: return "="; - case tkind.TK_PLUSEQ: return "+="; - case tkind.TK_MINUSEQ: return "-="; - case tkind.TK_STAREQ: return "*="; - case tkind.TK_SLASHEQ: return "/="; - case tkind.TK_PERCENTEQ: return "%="; - case tkind.TK_AMPEQ: return "&="; - case tkind.TK_PIPEEQ: return "|="; - case tkind.TK_CARETEQ: return "^="; - case tkind.TK_LSHIFTEQ: return "<<="; - case tkind.TK_RSHIFTEQ: return ">>="; - - case tkind.TK_PLUS: return "+"; - case tkind.TK_MINUS: return "-"; - case tkind.TK_STAR: return "*"; - case tkind.TK_SLASH: return "/"; - case tkind.TK_PERCENT: return "%"; - case tkind.TK_AMP: return "&"; - case tkind.TK_PIPE: return "|"; - case tkind.TK_CARET: return "^"; - case tkind.TK_TILDE: return "~"; - case tkind.TK_LSHIFT: return "<<"; - case tkind.TK_RSHIFT: return ">>"; - - case tkind.TK_EQ: return "=="; - case tkind.TK_NEQ: return "!="; - case tkind.TK_LT: return "<"; - case tkind.TK_LE: return "<="; - case tkind.TK_GT: return ">"; - case tkind.TK_GE: return ">="; - - case tkind.TK_AND: return "&&"; - case tkind.TK_OR: return "||"; - case tkind.TK_NOT: return "!"; - - case tkind.TK_LARROW: return "<-"; - case tkind.TK_ARROW: return "->"; - case tkind.TK_FATARROW: return "=>"; - - case tkind.TK_LAST: return ""; - }; - return ""; -}; - -// ---- writer for tokprint ---------------------------------------------- -// -// fputq mirrors cmd/wcc/tok.c:fputq — quote the string with C-style -// escapes for \, ", \n, \t, \r and \xNN for other non-printables. - -fn fputcbyte(fd: i32, b: u8) void = { +fn putc1(fd: i32, b: u8) void = { let buf: [1]u8; buf[0] = b; os.write(fd, buf.ptr, 1u64); }; -fn fputsstr(fd: i32, s: str) void = { - os.write(fd, s.ptr, s.len: u64); -}; - -fn hexchar(n: u8) u8 = { - if (n < 10u8) { return n + 48u8; }; // '0'..'9' - return (n - 10u8) + 97u8; // 'a'..'f' -}; - -fn fputhex2(fd: i32, b: u8) void = { - let out: [4]u8; - out[0] = '\\'; - out[1] = 'x'; - out[2] = hexchar(b >> 4u8); - out[3] = hexchar(b & 15u8); - os.write(fd, out.ptr, 4u64); -}; - -fn fputq(fd: i32, p: *u8, n: i32) void = { - fputcbyte(fd, '"'); +fn putq(fd: i32, s: str) void = { + putc1(fd, '"'); let i: i32 = 0; - for (i < n) { - let c: u8 = p[i]; - switch (c) { - case '\\': fputsstr(fd, "\\\\"); - case '"': fputsstr(fd, "\\\""); - case '\n': fputsstr(fd, "\\n"); - case '\t': fputsstr(fd, "\\t"); - case '\r': fputsstr(fd, "\\r"); - case: - if (c < ' ' || c == 127u8) { - fputhex2(fd, c); - } else { - fputcbyte(fd, c); - }; - }; + for (i < s.len) { + let c: u8 = s[i]; + if (c == '"') { + os.write(fd, "\\\"".ptr, 2u64); + } else { if (c == '\\') { + os.write(fd, "\\\\".ptr, 2u64); + } else { if (c == '\n') { + os.write(fd, "\\n".ptr, 2u64); + } else { if (c == '\t') { + os.write(fd, "\\t".ptr, 2u64); + } else { if (c < 32u8) { + let hi: u8 = c >> 4u8; + let lo: u8 = c & 15u8; + let h: u8 = 0u8; + let l: u8 = 0u8; + if (hi < 10u8) { h = hi + 48u8; } else { h = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { l = lo + 48u8; } else { l = (lo - 10u8) + 97u8; }; + let buf: [4]u8; + buf[0] = 92u8; + buf[1] = 120u8; + buf[2] = h; + buf[3] = l; + os.write(fd, buf.ptr, 4u64); + } else { + putc1(fd, c); + };};};};}; i += 1; }; - fputcbyte(fd, '"'); + putc1(fd, 34u8); }; -// tokprint — write one token line to fd. Format must match -// cmd/wcc/tok.c:tokprint() byte-for-byte: that's the diff anchor. -// ":: [ ]\n" -// -// Takes `t` by pointer because w6c can't yet pass a >16-byte struct -// by value; the C version takes Tok by value. -export fn tokprint(fd: i32, t: *tok) void = { - // Chained-dot field reads (`t.x.y`) on str sub-fields aren't yet - // reduced by w6c — `t.x.y` returns the whole str. Lift the str - // fields into locals so we can use the str pseudo-field path. - let tfile: str = t.file; - let ttext: str = t.text; - if (tfile.len > 0) { - fputsstr(fd, tfile); +fn pr(fd: i32, n: *node, d: i32) void = { + if (n == nil) { + ind(fd, d); + os.write(fd, "()\n".ptr, 3u64); + return; + }; + // N_TPARAM wraps an N_TTUPLE.list element so exprtype can return + // shared element-type nodes without corrupting their .next chain. + // Cstage has no AST-level wrapper, so unwrap here to keep the 990 + // -a byte-diff with cstage's astprint. + if (n.kind == nkind.N_TPARAM) { + pr(fd, n.lhs, d); + return; + }; + ind(fd, d); + putc1(fd, '('); + let nm: str = nkname(n.kind); + os.write(fd, nm.ptr, nm.len: u64); + + if (n.kind == nkind.N_INTLIT) { + putc1(fd, 32u8); + let s: str = strconv.u64tos(n.uval, strconv.base.DEC); + os.write(fd, s.ptr, s.len: u64); + } else { if (n.kind == nkind.N_RUNELIT) { + putc1(fd, 32u8); + let s: str = strconv.u64tos(n.uval, strconv.base.DEC); + os.write(fd, s.ptr, s.len: u64); + } else { if ( + n.kind == nkind.N_STRLIT || + n.kind == nkind.N_IDENT || + n.kind == nkind.N_USE || + n.kind == nkind.N_DOT || + n.kind == nkind.N_DEF || + n.kind == nkind.N_TYPEDECL || + n.kind == nkind.N_FNDECL || + n.kind == nkind.N_PARAM || + n.kind == nkind.N_LET || + n.kind == nkind.N_TNAME || + n.kind == nkind.N_TFIELD || + n.kind == nkind.N_TENUMMEMBER || + n.kind == nkind.N_FIELD || + n.kind == nkind.N_ATTR + ) { + // Match C ast.c: print the str field whenever it's non-nil, + // even if its length is zero (e.g. an empty STRLIT prints + // `(str ""`). + let s: str = n.str; + if (s.ptr != nil) { + putc1(fd, 32u8); + putq(fd, s); + }; + } else { if ( + n.kind == nkind.N_BIN || + n.kind == nkind.N_UN || + n.kind == nkind.N_ASSIGN + ) { + putc1(fd, 32u8); + let on: str = tokname(n.op); + os.write(fd, on.ptr, on.len: u64); + };};};}; + + if (n.kind == nkind.N_FNDECL) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + if (n.kind == nkind.N_DEF) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + if (n.kind == nkind.N_TYPEDECL) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + putc1(fd, '\n'); + + if (n.attr != nil) { + ind(fd, d + 1); + os.write(fd, "(@\n".ptr, 3u64); + let m: *node = n.attr; + for (m != nil) { + pr(fd, m, d + 2); + m = m.next; + }; + ind(fd, d + 1); + os.write(fd, ")\n".ptr, 2u64); + }; + if (n.lhs != nil) { pr(fd, n.lhs, d + 1); }; + if (n.rhs != nil) { pr(fd, n.rhs, d + 1); }; + if (n.cond != nil) { pr(fd, n.cond, d + 1); }; + if (n.body != nil) { pr(fd, n.body, d + 1); }; + if (n.els != nil) { pr(fd, n.els, d + 1); }; + if (n.list != nil) { + ind(fd, d + 1); + os.write(fd, "(list\n".ptr, 6u64); + let m: *node = n.list; + for (m != nil) { + pr(fd, m, d + 2); + m = m.next; + }; + ind(fd, d + 1); + os.write(fd, ")\n".ptr, 2u64); + }; + ind(fd, d); + os.write(fd, ")\n".ptr, 2u64); +}; + +export fn astprint(fd: i32, n: *node) void = { + pr(fd, n, 0); +}; + +//ww:module syntax +// lib/ww/syntax/decl.ww — declaration parsing, split out of parse.ww. + +package syntax; + +import os; +import strings; + +// `import encoding.utf8;` — the driver resolves the dotted path to +// a directory; only the leaf (`utf8`) is needed downstream as the +// module bareword for n_use → decl disambiguation, mirroring Hare's +// `use encoding::utf8;` → `utf8::name` (ref/hare/hare/ast/import.ha:7 +// stores `[]str` but identifier-resolution uses the last component). +fn parseuse(p: *parser) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); // past `use` + let n: *node = newnode(nkind.N_USE, pf, pl, pc); + n.nmod = p.curmod; + // M1 #22: accumulate the full dotted import path (n.usepath) for the + // checker's path-keyed module match; n.str stays the leaf alias the + // user writes (`utf8.x`). + let leaf: str; + expectident(p, &leaf); + let path: str = leaf; + for (p.curkind == tkind.TK_DOT) { + advance(p); // past `.` + expectident(p, &leaf); + path = strings.concat(path, ".", leaf); + }; + n.str = leaf; + n.usepath = path; + expecttok(p, tkind.TK_SEMI, "expected ';' after use"); + return n; +}; + +fn parsedef(p: *parser, exported: i32) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); // past `def` + let n: *node = newnode(nkind.N_DEF, pf, pl, pc); + n.nmod = p.curmod; + let id: str; + expectident(p, &id); + n.str = id; + expecttok(p, tkind.TK_COLON, "expected ':' in def"); + n.lhs = parsetype(p); + // A value-LESS `def X: T;` is an interface prototype (an aggregate- + // init `.wwi` def whose DATA lives in the defining package — BUG-2 / + // task #71), mirroring the fn bodyless-prototype arm. rhs stays nil; + // the checker fold pass and cgen emit_defs/emit_lets already guard + // the rhs==nil shape. + if (accepttok(p, tkind.TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; + expecttok(p, tkind.TK_SEMI, "expected ';' after def"); + n.exported = exported; + return n; +}; + +fn parselet(p: *parser, exported: i32) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + // Accept `let` or `const`. Const-bound bindings are marked via + // n.op = tkind.TK_CONST so the checker can reject reassignment. + let is_const: i32 = 0; + if (p.curkind == tkind.TK_CONST) { is_const = 1; }; + advance(p); + let n: *node = newnode(nkind.N_LET, pf, pl, pc); + n.nmod = p.curmod; + let id: str; + expectbindname(p, &id); + n.str = id; + if (accepttok(p, tkind.TK_COLON)) { + n.lhs = parsetype(p); + }; + if (accepttok(p, tkind.TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; + expecttok(p, tkind.TK_SEMI, "expected ';' after let"); + n.exported = exported; + if (is_const != 0) { n.op = tkind.TK_CONST; }; + return n; +}; + +fn parseattrs(p: *parser) *node = { + let head: *node = nil; + let tail: *node = nil; + for (p.curkind == tkind.TK_AT) { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); + let a: *node = newnode(nkind.N_ATTR, pf, pl, pc); + let id: str; + expectident(p, &id); + a.str = id; + // `@name(args...)` for FFI-style attrs; `@name` for marker- + // only attrs like @test (no parens). + if (accepttok(p, tkind.TK_LPAREN)) { + let arghead: *node = nil; + parsearglist(p, tkind.TK_RPAREN, &arghead); + a.list = arghead; + expecttok(p, tkind.TK_RPAREN, "expected ')' after attribute args"); + }; + if (head == nil) { head = a; tail = a; } + else { tail.next = a; tail = a; }; + }; + return head; +}; + +fn parseparams(p: *parser) *node = { + if (p.curkind == tkind.TK_RPAREN) { return nil; }; + let head: *node = nil; + let tail: *node = nil; + for (true) { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + let n: *node = newnode(nkind.N_PARAM, pf, pl, pc); + // Param form: (IDENT|'_') ':' type. Anonymous-type-only params + // (used in fn type expressions) aren't yet wired here. + let id: str; + expectbindname(p, &id); + n.str = id; + expecttok(p, tkind.TK_COLON, "expected ':' in parameter"); + n.lhs = parsetype(p); + // Hare-style variadic: `name: T...`. Marker on n.op so check + // promotes the param's type to []T and call sites gather / + // forward. Mirrors cmd/wcc/parse.c parseparams. + if (accepttok(p, tkind.TK_ELLIPSIS)) { + n.op = tkind.TK_ELLIPSIS; + }; + if (head == nil) { head = n; tail = n; } + else { tail.next = n; tail = n; }; + if (n.op == tkind.TK_ELLIPSIS) { + break; // variadic must be the last param + }; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + if (p.curkind == tkind.TK_RPAREN) { break; }; + }; + return head; +}; + +fn parsefn(p: *parser, exported: i32, attrs: *node) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); // past `fn` + let n: *node = newnode(nkind.N_FNDECL, pf, pl, pc); + n.nmod = p.curmod; + let id: str; + expectident(p, &id); + n.str = id; + expecttok(p, tkind.TK_LPAREN, "expected '(' after fn name"); + n.list = parseparams(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after params"); + if (p.curkind != tkind.TK_ASSIGN) { + if (p.curkind != tkind.TK_SEMI) { + n.lhs = parsetype(p); + }; + }; + if (accepttok(p, tkind.TK_ASSIGN)) { + n.body = parseblock(p); + expecttok(p, tkind.TK_SEMI, "expected ';' after fn body"); } else { - fputsstr(fd, ""); + // Body-less fn: FFI declaration (`fn name(args) ret;`). + expecttok(p, tkind.TK_SEMI, "expected ';' after fn header"); }; - fputcbyte(fd, ':'); - let ls: str = strconv.i64tos(t.line: i64, strconv.base.DEC); - os.write(fd, ls.ptr, ls.len: u64); - fputcbyte(fd, ':'); - let cs: str = strconv.i64tos(t.col: i64, strconv.base.DEC); - os.write(fd, cs.ptr, cs.len: u64); - fputcbyte(fd, ' '); - fputsstr(fd, tokname(t.kind)); - - switch (t.kind) { - case tkind.TK_IDENT, tkind.TK_STR, tkind.TK_ERR: - fputcbyte(fd, ' '); - fputq(fd, ttext.ptr, ttext.len); - case tkind.TK_INT, tkind.TK_RUNE: - fputcbyte(fd, ' '); - let us: str = strconv.u64tos(t.uval, strconv.base.DEC); - os.write(fd, us.ptr, us.len: u64); - }; - // tkind.TK_FLOAT is intentionally not handled here — %g formatting - // won't byte-match across implementations. Diff fixtures must - // be float-free until we implement a stable float formatter. - - fputcbyte(fd, '\n'); + n.exported = exported; + n.attr = attrs; + return n; }; -//ww:module lex -// lib/ww/lex/lex.ww — port of cmd/wcc/lex.c. +fn parsetypedecl(p: *parser, exported: i32) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); // past `type` + let n: *node = newnode(nkind.N_TYPEDECL, pf, pl, pc); + n.nmod = p.curmod; + let id: str; + expectident(p, &id); + n.str = id; + expecttok(p, tkind.TK_ASSIGN, "expected '=' in type decl"); + n.lhs = parsetype(p); + expecttok(p, tkind.TK_SEMI, "expected ';' after type decl"); + n.exported = exported; + return n; +}; + + +//ww:module syntax +// lib/ww/syntax/expr.ww — expression parsing, split out of parse.ww. + +package syntax; + +import os; + +// streqlocal — str-to-str compare. Inlined here to avoid a cross- +// module `use sym;` for one call site. +fn streqlocal(a: str, b: str) bool = { + if (a.len != b.len) { return false; }; + let i: i32 = 0; + for (i < a.len) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +fn parseprimary(p: *parser) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + + if (p.curkind == tkind.TK_INT) { + let n: *node = newnode(nkind.N_INTLIT, pf, pl, pc); + n.uval = p.curuval; + n.str = p.curtext; + // Plumb the typed-int suffix (`42i64`, `3u8`) through to + // the node. Cgen's rhstargetname reads tsuffix to pick the + // matching tagged-union variant; without this, typed-int + // rhs of `h.e = 42i64;` falls through to the "first non-str + // variant" fallback and writes tag 0. Mirror of cmd/wcc/ + // parse.c parseprimary TK_INT. + n.tsuffix = p.curtsuffix; + advance(p); + return n; + }; + if (p.curkind == tkind.TK_FLOAT) { + let n: *node = newnode(nkind.N_FLOATLIT, pf, pl, pc); + n.fval = p.curfval; + // uval carries the IEEE 754 bit pattern — the lexer sets + // both, and cgen consumers prefer the integer view so they + // don't need a float ABI to materialise the constant. + n.uval = p.curuval; + n.str = p.curtext; + n.tsuffix = p.curtsuffix; + advance(p); + return n; + }; + if (p.curkind == tkind.TK_STR) { + let n: *node = newnode(nkind.N_STRLIT, pf, pl, pc); + n.str = p.curtext; + advance(p); + return n; + }; + if (p.curkind == tkind.TK_RUNE) { + let n: *node = newnode(nkind.N_RUNELIT, pf, pl, pc); + n.uval = p.curuval; + advance(p); + return n; + }; + if (p.curkind == tkind.TK_TRUE) { + advance(p); + return newnode(nkind.N_TRUE, pf, pl, pc); + }; + if (p.curkind == tkind.TK_FALSE) { + advance(p); + return newnode(nkind.N_FALSE, pf, pl, pc); + }; + if (p.curkind == tkind.TK_NIL) { + advance(p); + return newnode(nkind.N_NIL, pf, pl, pc); + }; + if (p.curkind == tkind.TK_VOID) { + advance(p); + return newnode(nkind.N_VOIDLIT, pf, pl, pc); + }; + if (p.curkind == tkind.TK_UNDER) { + // Bare `_` — valid only as a discard lvalue. Emit an N_IDENT + // with empty str (newnode zeroes the node, so str.len is + // already 0); the checker rejects it outside lvalue + // positions. + advance(p); + return newnode(nkind.N_IDENT, pf, pl, pc); + }; + if (p.curkind == tkind.TK_LBRACK) { + // Array literal `[a, b, c]` or `[v, w...]` (repeat suffix). + // The repeat marker is an nkind.N_FIELD node with str = "..." + // appended to the element list so cgen can detect it. + advance(p); + let n: *node = newnode(nkind.N_ARRLIT, pf, pl, pc); + let head: *node = nil; + let tail: *node = nil; + for (p.curkind != tkind.TK_RBRACK) { + if (p.curkind == tkind.TK_EOF) { break; }; + let e: *node = parseexpr(p); + if (head == nil) { head = e; tail = e; } + else { tail.next = e; tail = e; }; + if (accepttok(p, tkind.TK_ELLIPSIS)) { + let rep: *node = newnode(nkind.N_FIELD, + p.curfile, p.curline, p.curcol); + rep.str = "..."; + tail.next = rep; + tail = rep; + break; + }; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + expecttok(p, tkind.TK_RBRACK, "expected ']' after array literal"); + n.list = head; + return n; + }; + if (p.curkind == tkind.TK_LPAREN) { + advance(p); + let e: *node = parseexpr(p); + // Tuple literal: (a, b, ...) + if (accepttok(p, tkind.TK_COMMA)) { + let t: *node = newnode(nkind.N_TUPLE, pf, pl, pc); + t.list = e; + let tail: *node = e; + // Parse each element BEFORE the RPAREN-break so `(a,)` + // (a single elem + trailing comma) is a loud parse error; + // a trailing comma is legal only after >=2 elems. Mirror + // cstage cmd/wcc/parse.c:552-558 loop order. + for (true) { + let en: *node = parseexpr(p); + tail.next = en; + tail = en; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + if (p.curkind == tkind.TK_RPAREN) { break; }; + }; + expecttok(p, tkind.TK_RPAREN, "expected ')' in tuple"); + return t; + }; + expecttok(p, tkind.TK_RPAREN, "expected ')'"); + return e; + }; + if (p.curkind == tkind.TK_IDENT) { + let n: *node = newnode(nkind.N_IDENT, pf, pl, pc); + n.str = p.curtext; + advance(p); + // `IDENT {` — struct literal. Disambiguate: only consume as a + // struct lit when we're not in a context where '{' starts a + // block (e.g. `if (cond) {`). The parser is called from + // expressions, never directly from cond contexts that need a + // block; in stmt parsing, the for/if drivers consume their + // own paren/cond, so this is safe. + if (p.curkind == tkind.TK_LBRACE) { + advance(p); + let s: *node = newnode(nkind.N_STRUCTLIT, pf, pl, pc); + s.lhs = n; + let head: *node = nil; + let tail: *node = nil; + for (p.curkind != tkind.TK_RBRACE) { + if (p.curkind == tkind.TK_EOF) { break; }; + // Trailing `...` autofill marker. Stash on s.op so + // cgen can zero-fill the slot before per-field stores. + if (p.curkind == tkind.TK_ELLIPSIS) { + advance(p); + s.op = tkind.TK_ELLIPSIS; + break; + }; + let fpf: str = p.curfile; + let fpl: i32 = p.curline; + let fpc: i32 = p.curcol; + let id: str; + expectident(p, &id); + expecttok(p, tkind.TK_ASSIGN, "expected '=' in struct lit field"); + let v: *node = parseexpr(p); + let f: *node = newnode(nkind.N_FIELD, fpf, fpl, fpc); + f.str = id; + f.lhs = v; + if (head == nil) { head = f; tail = f; } + else { tail.next = f; tail = f; }; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + expecttok(p, tkind.TK_RBRACE, "expected '}' after struct literal"); + s.list = head; + return s; + }; + return n; + }; + if (p.curkind == tkind.TK_MATCH) { + // match (e) { case let v: T => stmt; case T => stmt; case => stmt; }; + advance(p); + expecttok(p, tkind.TK_LPAREN, "expected '(' after match"); + let m: *node = newnode(nkind.N_MATCH, pf, pl, pc); + m.lhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after match scrutinee"); + expecttok(p, tkind.TK_LBRACE, "expected '{' to open match body"); + let head: *node = nil; + let tail: *node = nil; + for (p.curkind == tkind.TK_CASE) { + let cf: str = p.curfile; + let cl: i32 = p.curline; + let cc: i32 = p.curcol; + advance(p); // past `case` + let mc: *node = newnode(nkind.N_MCASE, cf, cl, cc); + if (p.curkind == tkind.TK_LET) { + advance(p); + let id: str; + expectident(p, &id); + mc.str = id; + expecttok(p, tkind.TK_COLON, "expected ':' after match binding"); + mc.lhs = parsetype(p); + } else { if (p.curkind != tkind.TK_FATARROW) { + mc.lhs = parsetype(p); + };}; + expecttok(p, tkind.TK_FATARROW, "expected '=>' in match arm"); + mc.body = parsestmt(p); + if (head == nil) { head = mc; tail = mc; } + else { tail.next = mc; tail = mc; }; + }; + expecttok(p, tkind.TK_RBRACE, "expected '}' after match body"); + m.list = head; + return m; + }; + errmsg(p, "expected expression"); + advance(p); + return newnode(nkind.N_NONE, pf, pl, pc); +}; + +fn parsearglist(p: *parser, closekind: tkind, headout: **node) void = { + *headout = nil; + if (p.curkind == closekind) { return; }; + let head: *node = nil; + let tail: *node = nil; + for (true) { + let e: *node = parseexpr(p); + // Hare-style spread: `expr...` in an arg slot becomes a + // marker the callee/builtin can iterate over. Mirrors + // cmd/wcc/parse.c. The only consumer today is `append`. + if (accepttok(p, tkind.TK_ELLIPSIS)) { + let sp: *node = newnode(nkind.N_SPREAD, e.file, e.line, e.col); + sp.lhs = e; + e = sp; + }; + if (head == nil) { head = e; tail = e; } + else { tail.next = e; tail = e; }; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + if (p.curkind == closekind) { break; }; + }; + *headout = head; +}; + +fn parsepostfix(p: *parser, lhs: *node) *node = { + let cur: *node = lhs; + for (true) { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + if (p.curkind == tkind.TK_LPAREN) { + advance(p); + let n: *node = newnode(nkind.N_CALL, pf, pl, pc); + n.lhs = cur; + // size(T)/align(T): the single arg is a type expression, + // not a regular expression. Special-case at the parser. + let is_typeop: i32 = 0; + if (cur.kind == nkind.N_IDENT) { + if (streqlocal(cur.str, "size")) { is_typeop = 1; }; + if (streqlocal(cur.str, "align")) { is_typeop = 1; }; + }; + if (is_typeop != 0) { + n.list = parsetype(p); + } else { + let arghead: *node = nil; + parsearglist(p, tkind.TK_RPAREN, &arghead); + n.list = arghead; + }; + expecttok(p, tkind.TK_RPAREN, "expected ')' after args"); + cur = n; + continue; + }; + if (p.curkind == tkind.TK_LBRACK) { + advance(p); + // `[ : hi ]` — slice with implicit lo = 0. + if (p.curkind == tkind.TK_COLON) { + advance(p); + let n: *node = newnode(nkind.N_SLICE, pf, pl, pc); + n.lhs = cur; + if (p.curkind != tkind.TK_RBRACK) { + n.cond = parseexpr(p); + }; + expecttok(p, tkind.TK_RBRACK, "expected ']' in slice"); + cur = n; + continue; + }; + // Suppress cast inside `[...]` so ':' parses as slice + // separator rather than the postfix cast operator. + let prev: i32 = p.nocast; + p.nocast = 1; + let e: *node = parseexpr(p); + p.nocast = prev; + if (p.curkind == tkind.TK_COLON) { + advance(p); + let n: *node = newnode(nkind.N_SLICE, pf, pl, pc); + n.lhs = cur; + n.rhs = e; + if (p.curkind != tkind.TK_RBRACK) { + n.cond = parseexpr(p); + }; + expecttok(p, tkind.TK_RBRACK, "expected ']' in slice"); + cur = n; + continue; + }; + let n: *node = newnode(nkind.N_INDEX, pf, pl, pc); + n.lhs = cur; + n.rhs = e; + expecttok(p, tkind.TK_RBRACK, "expected ']' after index"); + cur = n; + continue; + }; + if (p.curkind == tkind.TK_DOT) { + advance(p); + let n: *node = newnode(nkind.N_DOT, pf, pl, pc); + n.lhs = cur; + // Hare-style tuple field access: `t.0`, `t.1`. The + // numeric literal becomes the field name string so the + // cgen tuple-positional path matches `cmd/wcc/parse.c`. + if (p.curkind == tkind.TK_INT) { + n.str = p.curtext; + advance(p); + } else { + let id: str; + expectident(p, &id); + n.str = id; + }; + cur = n; + continue; + }; + if (p.curkind == tkind.TK_COLON) { + if (p.nocast != 0) { + return cur; + }; + advance(p); + let n: *node = newnode(nkind.N_CAST, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; + // Hare-style postfix: + // `e as T` — assert lhs is variant T (abort otherwise) → T + // `e is T` — bool: does lhs currently hold variant T? + // Same precedence level as the `:` cast. + if (p.curkind == tkind.TK_AS) { + advance(p); + let n: *node = newnode(nkind.N_TYPEASSERT, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; + if (p.curkind == tkind.TK_IS) { + advance(p); + let n: *node = newnode(nkind.N_TYPETEST, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; + // `e?` — propagate error variant up the stack. + // `e!` — abort on error variant. + if (p.curkind == tkind.TK_QUESTION) { + advance(p); + let n: *node = newnode(nkind.N_TRYPROP, pf, pl, pc); + n.lhs = cur; + cur = n; + continue; + }; + if (p.curkind == tkind.TK_NOT) { + advance(p); + let n: *node = newnode(nkind.N_TRYUNW, pf, pl, pc); + n.lhs = cur; + cur = n; + continue; + }; + break; + }; + return cur; +}; + +fn parseunary(p: *parser) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + let k: tkind = p.curkind; + if (k == tkind.TK_MINUS) { + advance(p); + let n: *node = newnode(nkind.N_UN, pf, pl, pc); + n.op = tkind.TK_MINUS; n.lhs = parseunary(p); + return n; + }; + if (k == tkind.TK_PLUS) { + advance(p); + let n: *node = newnode(nkind.N_UN, pf, pl, pc); + n.op = tkind.TK_PLUS; n.lhs = parseunary(p); + return n; + }; + if (k == tkind.TK_NOT) { + advance(p); + let n: *node = newnode(nkind.N_UN, pf, pl, pc); + n.op = tkind.TK_NOT; n.lhs = parseunary(p); + return n; + }; + if (k == tkind.TK_TILDE) { + advance(p); + let n: *node = newnode(nkind.N_UN, pf, pl, pc); + n.op = tkind.TK_TILDE; n.lhs = parseunary(p); + return n; + }; + if (k == tkind.TK_STAR) { + advance(p); + let n: *node = newnode(nkind.N_UN, pf, pl, pc); + n.op = tkind.TK_STAR; n.lhs = parseunary(p); + return n; + }; + if (k == tkind.TK_AMP) { + advance(p); + let n: *node = newnode(nkind.N_UN, pf, pl, pc); + n.op = tkind.TK_AMP; n.lhs = parseunary(p); + return n; + }; + return parsepostfix(p, parseprimary(p)); +}; + +fn parsebin(p: *parser, lhs: *node, minp: i32) *node = { + let cur: *node = lhs; + for (true) { + let op: tkind = p.curkind; + let pr: i32 = bprec(op); + if (pr == 0) { return cur; }; + if (pr < minp) { return cur; }; + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); + let rhs: *node = parseunary(p); + for (true) { + let np: i32 = bprec(p.curkind); + if (np <= pr) { break; }; + rhs = parsebin(p, rhs, np); + }; + let n: *node = newnode(nkind.N_BIN, pf, pl, pc); + n.op = op; n.lhs = cur; n.rhs = rhs; + cur = n; + }; + return cur; +}; + +fn parseexpr(p: *parser) *node = { + let e: *node = parsebin(p, parseunary(p), 1); + if (isassignop(p.curkind)) { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + let op: tkind = p.curkind; + advance(p); + let n: *node = newnode(nkind.N_ASSIGN, pf, pl, pc); + n.op = op; + n.lhs = e; + n.rhs = parseexpr(p); // right-associative + return n; + }; + return e; +}; + + +//ww:module syntax +// lib/ww/syntax/lex.ww — port of cmd/wcc/lex.c. // // The DFA, the helpers, and the order of decisions all mirror the C // version exactly. The 990_selfhost test diffs the resulting token @@ -6836,10 +7486,8 @@ export fn tokprint(fd: i32, t: *tok) void = { // in shape, not in observable behaviour. Token kind values stay // numerically identical. -package lex; +package syntax; -// Sibling import (tok) auto-resolves via task #22 dir-enum when -// callers `import lex;` (which dir-enums lib/ww/lex/). import os; import ascii; import strings; @@ -7704,1066 +8352,8 @@ export fn lexnext(l: *lex, out: *tok) void = { out.text = strings.dup(view); }; -//ww:module-reset -// lib/ww/ast.ww — port of cmd/wcc/ast.c (Node defs + printer). -// -// Status: AST printer is fully ported. Constructor `newnode` is here. -// The parser (parse.ww) is currently minimal — see its file header. -// -// Calling-convention shim: same as tok/lex — `node` is too big to pass -// by value (8 *node pointers + 2 strs + a few ints), so callers always -// hand around `*node`. Only `newnode` allocates and returns a *node. - -package ww; - -import os; -import strconv; -import tok; - -// ---- Nkind ------------------------------------------------------------ -// -// Mirror of cmd/wcc/ww.h Nkind. Values must stay numerically equal so -// the AST diff probe in 990_selfhost works. - -// Mirror of the C `Nkind` enum in cmd/wcc/ww.h. Numeric values are -// explicit and must stay in sync — the 990_selfhost test diffs -// astprint against the C side byte-for-byte. Tail-appended entries -// (TYPETEST onward) preserve every prior N_* value. -type nkind = enum i32 { - N_NONE = 0, - - N_INTLIT = 1, - N_FLOATLIT = 2, - N_STRLIT = 3, - N_RUNELIT = 4, - N_TRUE = 5, - N_FALSE = 6, - N_NIL = 7, - N_IDENT = 8, - - N_BIN = 9, - N_UN = 10, - N_CALL = 11, - N_INDEX = 12, - N_DOT = 13, - N_CAST = 14, - N_STRUCTLIT = 15, - N_ARRLIT = 16, - N_FIELD = 17, - N_ASSIGN = 18, - N_ALLOC = 19, - N_FREE = 20, - N_RECV = 21, - N_SLICE = 22, - N_SPREAD = 23, - - N_BLOCK = 24, - N_EXPRSTMT = 25, - N_LET = 26, - N_RETURN = 27, - N_IF = 28, - N_FOR = 29, - N_FORRANGE = 30, - N_DEFER = 31, - N_BREAK = 32, - N_CONTINUE = 33, - N_SWITCH = 34, - N_CASE = 35, - - N_FILE = 36, - N_USE = 37, - N_DEF = 38, - N_TYPEDECL = 39, - N_FNDECL = 40, - N_PARAM = 41, - - N_TNAME = 42, - N_TPTR = 43, - N_TSLICE = 44, - N_TARRAY = 45, - N_TFN = 46, - N_TSTRUCT = 47, - N_TFIELD = 48, - N_TCHAN = 49, - - N_ATTR = 50, - N_TTUPLE = 51, - N_TTAGGED = 52, - N_TUPLE = 53, - N_MATCH = 54, - N_MCASE = 55, - N_TRYPROP = 56, - N_TRYUNW = 57, - N_MLET = 58, - N_MASSIGN = 59, - - N_TYPETEST = 60, - N_TYPEASSERT = 61, - N_VOIDLIT = 62, - N_TBANG = 63, - N_YIELD = 64, - N_TENUM = 65, - N_TENUMMEMBER = 66, - - // N_TPARAM — chain wrapper for N_TTUPLE.list elements. Mirror of - // cstage's Tparam (cmd/wcc/check.c:1437-1451) lifted to the AST so - // `exprtype` can return shared element-type nodes (sym.decl.lhs, - // struct field's .lhs, another N_TTUPLE's .list element) without - // corrupting source ASTs by reusing their .next. .lhs holds the - // element type AST (possibly shared); .next chains within the - // parent N_TTUPLE.list. Cstage keeps Tparam at the Type-layer; ww - // has no separate type layer for tuple chains, so the wrapper sits - // at the AST layer. Other node fields are unused. Never appears - // outside an N_TTUPLE.list; astprint unwraps transparently to keep - // the 990 -a byte-diff against cstage. - N_TPARAM = 67, - - N_LAST = 68, -}; - -// ---- Node ------------------------------------------------------------- - -type node = struct { - kind: nkind, - file: str, - line: i32, - col: i32, - op: tkind, // for nkind.N_BIN / nkind.N_UN / nkind.N_ASSIGN - str: str, - uval: u64, - fval: f64, - lhs: *node, - rhs: *node, - cond: *node, - body: *node, - els: *node, - list: *node, - next: *node, - attr: *node, - exported: i32, // bool — `export` keyword present - type_: *void, // filled in by checker; type.ww treats it as *tinfo - tsuffix: str, // typed numeric literal suffix ("i32", "u64", ...) - nmod: str, // originating module from `// MODULE: foo`; "" if none - usepath: str, // on an N_USE: full dotted IMPORT path vs leaf alias - // in `str` (M1 #22); "" otherwise - imported: i32, // M1 #22: decl reached via `//ww:module ` import - // boundary (vs root/primary); gates root-only bare main -}; - -export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = { - // fval cast-init: 990's wwdump TK_FLOAT diff requires this file - // to tokenise identically through C and ww (lex.ww:382 has the - // same workaround for the cstage %g-formats vs ww-skips divergence). - let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, type_=nil, tsuffix="", nmod="", usepath="", imported=0})!; - return n; -}; - -// ---- printer ---------------------------------------------------------- - -export fn nkname(k: nkind) str = { - switch (k) { - case nkind.N_NONE: return "none"; - case nkind.N_INTLIT: return "int"; - case nkind.N_FLOATLIT: return "float"; - case nkind.N_STRLIT: return "str"; - case nkind.N_RUNELIT: return "rune"; - case nkind.N_TRUE: return "true"; - case nkind.N_FALSE: return "false"; - case nkind.N_NIL: return "nil"; - case nkind.N_IDENT: return "id"; - - case nkind.N_BIN: return "bin"; - case nkind.N_UN: return "un"; - case nkind.N_CALL: return "call"; - case nkind.N_INDEX: return "index"; - case nkind.N_DOT: return "dot"; - case nkind.N_CAST: return "cast"; - case nkind.N_STRUCTLIT: return "structlit"; - case nkind.N_ARRLIT: return "arrlit"; - case nkind.N_FIELD: return "field"; - case nkind.N_ASSIGN: return "assign"; - case nkind.N_ALLOC: return "alloc"; - case nkind.N_FREE: return "free"; - case nkind.N_RECV: return "recv"; - case nkind.N_SLICE: return "slice"; - case nkind.N_SPREAD: return "spread"; - - case nkind.N_BLOCK: return "block"; - case nkind.N_EXPRSTMT: return "exprstmt"; - case nkind.N_LET: return "let"; - case nkind.N_RETURN: return "return"; - case nkind.N_IF: return "if"; - case nkind.N_FOR: return "for"; - case nkind.N_FORRANGE: return "forrange"; - case nkind.N_DEFER: return "defer"; - case nkind.N_BREAK: return "break"; - case nkind.N_CONTINUE: return "continue"; - case nkind.N_SWITCH: return "switch"; - case nkind.N_CASE: return "case"; - - case nkind.N_FILE: return "file"; - case nkind.N_USE: return "use"; - case nkind.N_DEF: return "def"; - case nkind.N_TYPEDECL: return "typedecl"; - case nkind.N_FNDECL: return "fn"; - case nkind.N_PARAM: return "param"; - - case nkind.N_TNAME: return "tname"; - case nkind.N_TPTR: return "tptr"; - case nkind.N_TSLICE: return "tslice"; - case nkind.N_TARRAY: return "tarray"; - case nkind.N_TFN: return "tfn"; - case nkind.N_TSTRUCT: return "tstruct"; - case nkind.N_TFIELD: return "tfield"; - case nkind.N_TCHAN: return "tchan"; - - case nkind.N_ATTR: return "attr"; - case nkind.N_TTUPLE: return "ttuple"; - case nkind.N_TTAGGED: return "ttagged"; - case nkind.N_TUPLE: return "tuple"; - case nkind.N_MATCH: return "match"; - case nkind.N_MCASE: return "mcase"; - case nkind.N_TRYPROP: return "tryprop"; - case nkind.N_TRYUNW: return "tryunw"; - case nkind.N_MLET: return "mlet"; - case nkind.N_MASSIGN: return "massign"; - - case nkind.N_TYPETEST: return "typetest"; - case nkind.N_TYPEASSERT: return "typeassert"; - case nkind.N_VOIDLIT: return "voidlit"; - case nkind.N_TBANG: return "tbang"; - case nkind.N_YIELD: return "yield"; - case nkind.N_TENUM: return "tenum"; - case nkind.N_TENUMMEMBER: return "tenummember"; - case nkind.N_TPARAM: return "tparam"; - case nkind.N_LAST: return "last"; - }; - return "?"; -}; - -fn ind(fd: i32, d: i32) void = { - let i: i32 = 0; - for (i < d) { - os.write(fd, " ".ptr, 2u64); - i += 1; - }; -}; - -fn putc1(fd: i32, b: u8) void = { - let buf: [1]u8; - buf[0] = b; - os.write(fd, buf.ptr, 1u64); -}; - -fn putq(fd: i32, s: str) void = { - putc1(fd, '"'); - let i: i32 = 0; - for (i < s.len) { - let c: u8 = s[i]; - if (c == '"') { - os.write(fd, "\\\"".ptr, 2u64); - } else { if (c == '\\') { - os.write(fd, "\\\\".ptr, 2u64); - } else { if (c == '\n') { - os.write(fd, "\\n".ptr, 2u64); - } else { if (c == '\t') { - os.write(fd, "\\t".ptr, 2u64); - } else { if (c < 32u8) { - let hi: u8 = c >> 4u8; - let lo: u8 = c & 15u8; - let h: u8 = 0u8; - let l: u8 = 0u8; - if (hi < 10u8) { h = hi + 48u8; } else { h = (hi - 10u8) + 97u8; }; - if (lo < 10u8) { l = lo + 48u8; } else { l = (lo - 10u8) + 97u8; }; - let buf: [4]u8; - buf[0] = 92u8; - buf[1] = 120u8; - buf[2] = h; - buf[3] = l; - os.write(fd, buf.ptr, 4u64); - } else { - putc1(fd, c); - };};};};}; - i += 1; - }; - putc1(fd, 34u8); -}; - -fn pr(fd: i32, n: *node, d: i32) void = { - if (n == nil) { - ind(fd, d); - os.write(fd, "()\n".ptr, 3u64); - return; - }; - // N_TPARAM wraps an N_TTUPLE.list element so exprtype can return - // shared element-type nodes without corrupting their .next chain. - // Cstage has no AST-level wrapper, so unwrap here to keep the 990 - // -a byte-diff with cstage's astprint. - if (n.kind == nkind.N_TPARAM) { - pr(fd, n.lhs, d); - return; - }; - ind(fd, d); - putc1(fd, '('); - let nm: str = nkname(n.kind); - os.write(fd, nm.ptr, nm.len: u64); - - if (n.kind == nkind.N_INTLIT) { - putc1(fd, 32u8); - let s: str = strconv.u64tos(n.uval, strconv.base.DEC); - os.write(fd, s.ptr, s.len: u64); - } else { if (n.kind == nkind.N_RUNELIT) { - putc1(fd, 32u8); - let s: str = strconv.u64tos(n.uval, strconv.base.DEC); - os.write(fd, s.ptr, s.len: u64); - } else { if ( - n.kind == nkind.N_STRLIT || - n.kind == nkind.N_IDENT || - n.kind == nkind.N_USE || - n.kind == nkind.N_DOT || - n.kind == nkind.N_DEF || - n.kind == nkind.N_TYPEDECL || - n.kind == nkind.N_FNDECL || - n.kind == nkind.N_PARAM || - n.kind == nkind.N_LET || - n.kind == nkind.N_TNAME || - n.kind == nkind.N_TFIELD || - n.kind == nkind.N_TENUMMEMBER || - n.kind == nkind.N_FIELD || - n.kind == nkind.N_ATTR - ) { - // Match C ast.c: print the str field whenever it's non-nil, - // even if its length is zero (e.g. an empty STRLIT prints - // `(str ""`). - let s: str = n.str; - if (s.ptr != nil) { - putc1(fd, 32u8); - putq(fd, s); - }; - } else { if ( - n.kind == nkind.N_BIN || - n.kind == nkind.N_UN || - n.kind == nkind.N_ASSIGN - ) { - putc1(fd, 32u8); - let on: str = tokname(n.op); - os.write(fd, on.ptr, on.len: u64); - };};};}; - - if (n.kind == nkind.N_FNDECL) { - if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; - }; - if (n.kind == nkind.N_DEF) { - if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; - }; - if (n.kind == nkind.N_TYPEDECL) { - if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; - }; - putc1(fd, '\n'); - - if (n.attr != nil) { - ind(fd, d + 1); - os.write(fd, "(@\n".ptr, 3u64); - let m: *node = n.attr; - for (m != nil) { - pr(fd, m, d + 2); - m = m.next; - }; - ind(fd, d + 1); - os.write(fd, ")\n".ptr, 2u64); - }; - if (n.lhs != nil) { pr(fd, n.lhs, d + 1); }; - if (n.rhs != nil) { pr(fd, n.rhs, d + 1); }; - if (n.cond != nil) { pr(fd, n.cond, d + 1); }; - if (n.body != nil) { pr(fd, n.body, d + 1); }; - if (n.els != nil) { pr(fd, n.els, d + 1); }; - if (n.list != nil) { - ind(fd, d + 1); - os.write(fd, "(list\n".ptr, 6u64); - let m: *node = n.list; - for (m != nil) { - pr(fd, m, d + 2); - m = m.next; - }; - ind(fd, d + 1); - os.write(fd, ")\n".ptr, 2u64); - }; - ind(fd, d); - os.write(fd, ")\n".ptr, 2u64); -}; - -export fn astprint(fd: i32, n: *node) void = { - pr(fd, n, 0); -}; - -//ww:module parse -// lib/ww/parse/decl.ww — declaration parsing, split out of parse.ww. - -package parse; - -import os; -import strings; -import tok; - -// `import encoding.utf8;` — the driver resolves the dotted path to -// a directory; only the leaf (`utf8`) is needed downstream as the -// module bareword for n_use → decl disambiguation, mirroring Hare's -// `use encoding::utf8;` → `utf8::name` (ref/hare/hare/ast/import.ha:7 -// stores `[]str` but identifier-resolution uses the last component). -fn parseuse(p: *parser) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - advance(p); // past `use` - let n: *node = newnode(nkind.N_USE, pf, pl, pc); - n.nmod = p.curmod; - // M1 #22: accumulate the full dotted import path (n.usepath) for the - // checker's path-keyed module match; n.str stays the leaf alias the - // user writes (`utf8.x`). - let leaf: str; - expectident(p, &leaf); - let path: str = leaf; - for (p.curkind == tkind.TK_DOT) { - advance(p); // past `.` - expectident(p, &leaf); - path = strings.concat(path, ".", leaf); - }; - n.str = leaf; - n.usepath = path; - expecttok(p, tkind.TK_SEMI, "expected ';' after use"); - return n; -}; - -fn parsedef(p: *parser, exported: i32) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - advance(p); // past `def` - let n: *node = newnode(nkind.N_DEF, pf, pl, pc); - n.nmod = p.curmod; - let id: str; - expectident(p, &id); - n.str = id; - expecttok(p, tkind.TK_COLON, "expected ':' in def"); - n.lhs = parsetype(p); - // A value-LESS `def X: T;` is an interface prototype (an aggregate- - // init `.wwi` def whose DATA lives in the defining package — BUG-2 / - // task #71), mirroring the fn bodyless-prototype arm. rhs stays nil; - // the checker fold pass and cgen emit_defs/emit_lets already guard - // the rhs==nil shape. - if (accepttok(p, tkind.TK_ASSIGN)) { - n.rhs = parseexpr(p); - }; - expecttok(p, tkind.TK_SEMI, "expected ';' after def"); - n.exported = exported; - return n; -}; - -fn parselet(p: *parser, exported: i32) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - // Accept `let` or `const`. Const-bound bindings are marked via - // n.op = tkind.TK_CONST so the checker can reject reassignment. - let is_const: i32 = 0; - if (p.curkind == tkind.TK_CONST) { is_const = 1; }; - advance(p); - let n: *node = newnode(nkind.N_LET, pf, pl, pc); - n.nmod = p.curmod; - let id: str; - expectbindname(p, &id); - n.str = id; - if (accepttok(p, tkind.TK_COLON)) { - n.lhs = parsetype(p); - }; - if (accepttok(p, tkind.TK_ASSIGN)) { - n.rhs = parseexpr(p); - }; - expecttok(p, tkind.TK_SEMI, "expected ';' after let"); - n.exported = exported; - if (is_const != 0) { n.op = tkind.TK_CONST; }; - return n; -}; - -fn parseattrs(p: *parser) *node = { - let head: *node = nil; - let tail: *node = nil; - for (p.curkind == tkind.TK_AT) { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - advance(p); - let a: *node = newnode(nkind.N_ATTR, pf, pl, pc); - let id: str; - expectident(p, &id); - a.str = id; - // `@name(args...)` for FFI-style attrs; `@name` for marker- - // only attrs like @test (no parens). - if (accepttok(p, tkind.TK_LPAREN)) { - let arghead: *node = nil; - parsearglist(p, tkind.TK_RPAREN, &arghead); - a.list = arghead; - expecttok(p, tkind.TK_RPAREN, "expected ')' after attribute args"); - }; - if (head == nil) { head = a; tail = a; } - else { tail.next = a; tail = a; }; - }; - return head; -}; - -fn parseparams(p: *parser) *node = { - if (p.curkind == tkind.TK_RPAREN) { return nil; }; - let head: *node = nil; - let tail: *node = nil; - for (true) { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - let n: *node = newnode(nkind.N_PARAM, pf, pl, pc); - // Param form: (IDENT|'_') ':' type. Anonymous-type-only params - // (used in fn type expressions) aren't yet wired here. - let id: str; - expectbindname(p, &id); - n.str = id; - expecttok(p, tkind.TK_COLON, "expected ':' in parameter"); - n.lhs = parsetype(p); - // Hare-style variadic: `name: T...`. Marker on n.op so check - // promotes the param's type to []T and call sites gather / - // forward. Mirrors cmd/wcc/parse.c parseparams. - if (accepttok(p, tkind.TK_ELLIPSIS)) { - n.op = tkind.TK_ELLIPSIS; - }; - if (head == nil) { head = n; tail = n; } - else { tail.next = n; tail = n; }; - if (n.op == tkind.TK_ELLIPSIS) { - break; // variadic must be the last param - }; - if (!accepttok(p, tkind.TK_COMMA)) { break; }; - if (p.curkind == tkind.TK_RPAREN) { break; }; - }; - return head; -}; - -fn parsefn(p: *parser, exported: i32, attrs: *node) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - advance(p); // past `fn` - let n: *node = newnode(nkind.N_FNDECL, pf, pl, pc); - n.nmod = p.curmod; - let id: str; - expectident(p, &id); - n.str = id; - expecttok(p, tkind.TK_LPAREN, "expected '(' after fn name"); - n.list = parseparams(p); - expecttok(p, tkind.TK_RPAREN, "expected ')' after params"); - if (p.curkind != tkind.TK_ASSIGN) { - if (p.curkind != tkind.TK_SEMI) { - n.lhs = parsetype(p); - }; - }; - if (accepttok(p, tkind.TK_ASSIGN)) { - n.body = parseblock(p); - expecttok(p, tkind.TK_SEMI, "expected ';' after fn body"); - } else { - // Body-less fn: FFI declaration (`fn name(args) ret;`). - expecttok(p, tkind.TK_SEMI, "expected ';' after fn header"); - }; - n.exported = exported; - n.attr = attrs; - return n; -}; - -fn parsetypedecl(p: *parser, exported: i32) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - advance(p); // past `type` - let n: *node = newnode(nkind.N_TYPEDECL, pf, pl, pc); - n.nmod = p.curmod; - let id: str; - expectident(p, &id); - n.str = id; - expecttok(p, tkind.TK_ASSIGN, "expected '=' in type decl"); - n.lhs = parsetype(p); - expecttok(p, tkind.TK_SEMI, "expected ';' after type decl"); - n.exported = exported; - return n; -}; - - -//ww:module parse -// lib/ww/parse/expr.ww — expression parsing, split out of parse.ww. - -package parse; - -import os; -import tok; - -// streqlocal — str-to-str compare. Inlined here to avoid a cross- -// module `use sym;` for one call site. -fn streqlocal(a: str, b: str) bool = { - if (a.len != b.len) { return false; }; - let i: i32 = 0; - for (i < a.len) { - if (a[i] != b[i]) { return false; }; - i += 1; - }; - return true; -}; - -fn parseprimary(p: *parser) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - - if (p.curkind == tkind.TK_INT) { - let n: *node = newnode(nkind.N_INTLIT, pf, pl, pc); - n.uval = p.curuval; - n.str = p.curtext; - // Plumb the typed-int suffix (`42i64`, `3u8`) through to - // the node. Cgen's rhstargetname reads tsuffix to pick the - // matching tagged-union variant; without this, typed-int - // rhs of `h.e = 42i64;` falls through to the "first non-str - // variant" fallback and writes tag 0. Mirror of cmd/wcc/ - // parse.c parseprimary TK_INT. - n.tsuffix = p.curtsuffix; - advance(p); - return n; - }; - if (p.curkind == tkind.TK_FLOAT) { - let n: *node = newnode(nkind.N_FLOATLIT, pf, pl, pc); - n.fval = p.curfval; - // uval carries the IEEE 754 bit pattern — the lexer sets - // both, and cgen consumers prefer the integer view so they - // don't need a float ABI to materialise the constant. - n.uval = p.curuval; - n.str = p.curtext; - n.tsuffix = p.curtsuffix; - advance(p); - return n; - }; - if (p.curkind == tkind.TK_STR) { - let n: *node = newnode(nkind.N_STRLIT, pf, pl, pc); - n.str = p.curtext; - advance(p); - return n; - }; - if (p.curkind == tkind.TK_RUNE) { - let n: *node = newnode(nkind.N_RUNELIT, pf, pl, pc); - n.uval = p.curuval; - advance(p); - return n; - }; - if (p.curkind == tkind.TK_TRUE) { - advance(p); - return newnode(nkind.N_TRUE, pf, pl, pc); - }; - if (p.curkind == tkind.TK_FALSE) { - advance(p); - return newnode(nkind.N_FALSE, pf, pl, pc); - }; - if (p.curkind == tkind.TK_NIL) { - advance(p); - return newnode(nkind.N_NIL, pf, pl, pc); - }; - if (p.curkind == tkind.TK_VOID) { - advance(p); - return newnode(nkind.N_VOIDLIT, pf, pl, pc); - }; - if (p.curkind == tkind.TK_UNDER) { - // Bare `_` — valid only as a discard lvalue. Emit an N_IDENT - // with empty str (newnode zeroes the node, so str.len is - // already 0); the checker rejects it outside lvalue - // positions. - advance(p); - return newnode(nkind.N_IDENT, pf, pl, pc); - }; - if (p.curkind == tkind.TK_LBRACK) { - // Array literal `[a, b, c]` or `[v, w...]` (repeat suffix). - // The repeat marker is an nkind.N_FIELD node with str = "..." - // appended to the element list so cgen can detect it. - advance(p); - let n: *node = newnode(nkind.N_ARRLIT, pf, pl, pc); - let head: *node = nil; - let tail: *node = nil; - for (p.curkind != tkind.TK_RBRACK) { - if (p.curkind == tkind.TK_EOF) { break; }; - let e: *node = parseexpr(p); - if (head == nil) { head = e; tail = e; } - else { tail.next = e; tail = e; }; - if (accepttok(p, tkind.TK_ELLIPSIS)) { - let rep: *node = newnode(nkind.N_FIELD, - p.curfile, p.curline, p.curcol); - rep.str = "..."; - tail.next = rep; - tail = rep; - break; - }; - if (!accepttok(p, tkind.TK_COMMA)) { break; }; - }; - expecttok(p, tkind.TK_RBRACK, "expected ']' after array literal"); - n.list = head; - return n; - }; - if (p.curkind == tkind.TK_LPAREN) { - advance(p); - let e: *node = parseexpr(p); - // Tuple literal: (a, b, ...) - if (accepttok(p, tkind.TK_COMMA)) { - let t: *node = newnode(nkind.N_TUPLE, pf, pl, pc); - t.list = e; - let tail: *node = e; - // Parse each element BEFORE the RPAREN-break so `(a,)` - // (a single elem + trailing comma) is a loud parse error; - // a trailing comma is legal only after >=2 elems. Mirror - // cstage cmd/wcc/parse.c:552-558 loop order. - for (true) { - let en: *node = parseexpr(p); - tail.next = en; - tail = en; - if (!accepttok(p, tkind.TK_COMMA)) { break; }; - if (p.curkind == tkind.TK_RPAREN) { break; }; - }; - expecttok(p, tkind.TK_RPAREN, "expected ')' in tuple"); - return t; - }; - expecttok(p, tkind.TK_RPAREN, "expected ')'"); - return e; - }; - if (p.curkind == tkind.TK_IDENT) { - let n: *node = newnode(nkind.N_IDENT, pf, pl, pc); - n.str = p.curtext; - advance(p); - // `IDENT {` — struct literal. Disambiguate: only consume as a - // struct lit when we're not in a context where '{' starts a - // block (e.g. `if (cond) {`). The parser is called from - // expressions, never directly from cond contexts that need a - // block; in stmt parsing, the for/if drivers consume their - // own paren/cond, so this is safe. - if (p.curkind == tkind.TK_LBRACE) { - advance(p); - let s: *node = newnode(nkind.N_STRUCTLIT, pf, pl, pc); - s.lhs = n; - let head: *node = nil; - let tail: *node = nil; - for (p.curkind != tkind.TK_RBRACE) { - if (p.curkind == tkind.TK_EOF) { break; }; - // Trailing `...` autofill marker. Stash on s.op so - // cgen can zero-fill the slot before per-field stores. - if (p.curkind == tkind.TK_ELLIPSIS) { - advance(p); - s.op = tkind.TK_ELLIPSIS; - break; - }; - let fpf: str = p.curfile; - let fpl: i32 = p.curline; - let fpc: i32 = p.curcol; - let id: str; - expectident(p, &id); - expecttok(p, tkind.TK_ASSIGN, "expected '=' in struct lit field"); - let v: *node = parseexpr(p); - let f: *node = newnode(nkind.N_FIELD, fpf, fpl, fpc); - f.str = id; - f.lhs = v; - if (head == nil) { head = f; tail = f; } - else { tail.next = f; tail = f; }; - if (!accepttok(p, tkind.TK_COMMA)) { break; }; - }; - expecttok(p, tkind.TK_RBRACE, "expected '}' after struct literal"); - s.list = head; - return s; - }; - return n; - }; - if (p.curkind == tkind.TK_MATCH) { - // match (e) { case let v: T => stmt; case T => stmt; case => stmt; }; - advance(p); - expecttok(p, tkind.TK_LPAREN, "expected '(' after match"); - let m: *node = newnode(nkind.N_MATCH, pf, pl, pc); - m.lhs = parseexpr(p); - expecttok(p, tkind.TK_RPAREN, "expected ')' after match scrutinee"); - expecttok(p, tkind.TK_LBRACE, "expected '{' to open match body"); - let head: *node = nil; - let tail: *node = nil; - for (p.curkind == tkind.TK_CASE) { - let cf: str = p.curfile; - let cl: i32 = p.curline; - let cc: i32 = p.curcol; - advance(p); // past `case` - let mc: *node = newnode(nkind.N_MCASE, cf, cl, cc); - if (p.curkind == tkind.TK_LET) { - advance(p); - let id: str; - expectident(p, &id); - mc.str = id; - expecttok(p, tkind.TK_COLON, "expected ':' after match binding"); - mc.lhs = parsetype(p); - } else { if (p.curkind != tkind.TK_FATARROW) { - mc.lhs = parsetype(p); - };}; - expecttok(p, tkind.TK_FATARROW, "expected '=>' in match arm"); - mc.body = parsestmt(p); - if (head == nil) { head = mc; tail = mc; } - else { tail.next = mc; tail = mc; }; - }; - expecttok(p, tkind.TK_RBRACE, "expected '}' after match body"); - m.list = head; - return m; - }; - errmsg(p, "expected expression"); - advance(p); - return newnode(nkind.N_NONE, pf, pl, pc); -}; - -fn parsearglist(p: *parser, closekind: tkind, headout: **node) void = { - *headout = nil; - if (p.curkind == closekind) { return; }; - let head: *node = nil; - let tail: *node = nil; - for (true) { - let e: *node = parseexpr(p); - // Hare-style spread: `expr...` in an arg slot becomes a - // marker the callee/builtin can iterate over. Mirrors - // cmd/wcc/parse.c. The only consumer today is `append`. - if (accepttok(p, tkind.TK_ELLIPSIS)) { - let sp: *node = newnode(nkind.N_SPREAD, e.file, e.line, e.col); - sp.lhs = e; - e = sp; - }; - if (head == nil) { head = e; tail = e; } - else { tail.next = e; tail = e; }; - if (!accepttok(p, tkind.TK_COMMA)) { break; }; - if (p.curkind == closekind) { break; }; - }; - *headout = head; -}; - -fn parsepostfix(p: *parser, lhs: *node) *node = { - let cur: *node = lhs; - for (true) { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - if (p.curkind == tkind.TK_LPAREN) { - advance(p); - let n: *node = newnode(nkind.N_CALL, pf, pl, pc); - n.lhs = cur; - // size(T)/align(T): the single arg is a type expression, - // not a regular expression. Special-case at the parser. - let is_typeop: i32 = 0; - if (cur.kind == nkind.N_IDENT) { - if (streqlocal(cur.str, "size")) { is_typeop = 1; }; - if (streqlocal(cur.str, "align")) { is_typeop = 1; }; - }; - if (is_typeop != 0) { - n.list = parsetype(p); - } else { - let arghead: *node = nil; - parsearglist(p, tkind.TK_RPAREN, &arghead); - n.list = arghead; - }; - expecttok(p, tkind.TK_RPAREN, "expected ')' after args"); - cur = n; - continue; - }; - if (p.curkind == tkind.TK_LBRACK) { - advance(p); - // `[ : hi ]` — slice with implicit lo = 0. - if (p.curkind == tkind.TK_COLON) { - advance(p); - let n: *node = newnode(nkind.N_SLICE, pf, pl, pc); - n.lhs = cur; - if (p.curkind != tkind.TK_RBRACK) { - n.cond = parseexpr(p); - }; - expecttok(p, tkind.TK_RBRACK, "expected ']' in slice"); - cur = n; - continue; - }; - // Suppress cast inside `[...]` so ':' parses as slice - // separator rather than the postfix cast operator. - let prev: i32 = p.nocast; - p.nocast = 1; - let e: *node = parseexpr(p); - p.nocast = prev; - if (p.curkind == tkind.TK_COLON) { - advance(p); - let n: *node = newnode(nkind.N_SLICE, pf, pl, pc); - n.lhs = cur; - n.rhs = e; - if (p.curkind != tkind.TK_RBRACK) { - n.cond = parseexpr(p); - }; - expecttok(p, tkind.TK_RBRACK, "expected ']' in slice"); - cur = n; - continue; - }; - let n: *node = newnode(nkind.N_INDEX, pf, pl, pc); - n.lhs = cur; - n.rhs = e; - expecttok(p, tkind.TK_RBRACK, "expected ']' after index"); - cur = n; - continue; - }; - if (p.curkind == tkind.TK_DOT) { - advance(p); - let n: *node = newnode(nkind.N_DOT, pf, pl, pc); - n.lhs = cur; - // Hare-style tuple field access: `t.0`, `t.1`. The - // numeric literal becomes the field name string so the - // cgen tuple-positional path matches `cmd/wcc/parse.c`. - if (p.curkind == tkind.TK_INT) { - n.str = p.curtext; - advance(p); - } else { - let id: str; - expectident(p, &id); - n.str = id; - }; - cur = n; - continue; - }; - if (p.curkind == tkind.TK_COLON) { - if (p.nocast != 0) { - return cur; - }; - advance(p); - let n: *node = newnode(nkind.N_CAST, pf, pl, pc); - n.lhs = cur; - n.rhs = parsetype(p); - cur = n; - continue; - }; - // Hare-style postfix: - // `e as T` — assert lhs is variant T (abort otherwise) → T - // `e is T` — bool: does lhs currently hold variant T? - // Same precedence level as the `:` cast. - if (p.curkind == tkind.TK_AS) { - advance(p); - let n: *node = newnode(nkind.N_TYPEASSERT, pf, pl, pc); - n.lhs = cur; - n.rhs = parsetype(p); - cur = n; - continue; - }; - if (p.curkind == tkind.TK_IS) { - advance(p); - let n: *node = newnode(nkind.N_TYPETEST, pf, pl, pc); - n.lhs = cur; - n.rhs = parsetype(p); - cur = n; - continue; - }; - // `e?` — propagate error variant up the stack. - // `e!` — abort on error variant. - if (p.curkind == tkind.TK_QUESTION) { - advance(p); - let n: *node = newnode(nkind.N_TRYPROP, pf, pl, pc); - n.lhs = cur; - cur = n; - continue; - }; - if (p.curkind == tkind.TK_NOT) { - advance(p); - let n: *node = newnode(nkind.N_TRYUNW, pf, pl, pc); - n.lhs = cur; - cur = n; - continue; - }; - break; - }; - return cur; -}; - -fn parseunary(p: *parser) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - let k: tkind = p.curkind; - if (k == tkind.TK_MINUS) { - advance(p); - let n: *node = newnode(nkind.N_UN, pf, pl, pc); - n.op = tkind.TK_MINUS; n.lhs = parseunary(p); - return n; - }; - if (k == tkind.TK_PLUS) { - advance(p); - let n: *node = newnode(nkind.N_UN, pf, pl, pc); - n.op = tkind.TK_PLUS; n.lhs = parseunary(p); - return n; - }; - if (k == tkind.TK_NOT) { - advance(p); - let n: *node = newnode(nkind.N_UN, pf, pl, pc); - n.op = tkind.TK_NOT; n.lhs = parseunary(p); - return n; - }; - if (k == tkind.TK_TILDE) { - advance(p); - let n: *node = newnode(nkind.N_UN, pf, pl, pc); - n.op = tkind.TK_TILDE; n.lhs = parseunary(p); - return n; - }; - if (k == tkind.TK_STAR) { - advance(p); - let n: *node = newnode(nkind.N_UN, pf, pl, pc); - n.op = tkind.TK_STAR; n.lhs = parseunary(p); - return n; - }; - if (k == tkind.TK_AMP) { - advance(p); - let n: *node = newnode(nkind.N_UN, pf, pl, pc); - n.op = tkind.TK_AMP; n.lhs = parseunary(p); - return n; - }; - return parsepostfix(p, parseprimary(p)); -}; - -fn parsebin(p: *parser, lhs: *node, minp: i32) *node = { - let cur: *node = lhs; - for (true) { - let op: tkind = p.curkind; - let pr: i32 = bprec(op); - if (pr == 0) { return cur; }; - if (pr < minp) { return cur; }; - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - advance(p); - let rhs: *node = parseunary(p); - for (true) { - let np: i32 = bprec(p.curkind); - if (np <= pr) { break; }; - rhs = parsebin(p, rhs, np); - }; - let n: *node = newnode(nkind.N_BIN, pf, pl, pc); - n.op = op; n.lhs = cur; n.rhs = rhs; - cur = n; - }; - return cur; -}; - -fn parseexpr(p: *parser) *node = { - let e: *node = parsebin(p, parseunary(p), 1); - if (isassignop(p.curkind)) { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - let op: tkind = p.curkind; - advance(p); - let n: *node = newnode(nkind.N_ASSIGN, pf, pl, pc); - n.op = op; - n.lhs = e; - n.rhs = parseexpr(p); // right-associative - return n; - }; - return e; -}; - - -//ww:module parse -// lib/ww/parse/parse.ww — port of cmd/wcc/parse.c (entry + plumbing). +//ww:module syntax +// lib/ww/syntax/parse.ww — port of cmd/wcc/parse.c (entry + plumbing). // // Split into Hare-style submodule: parse.ww (here) holds the parser // struct, lexer plumbing, parsetype, parsefile (entry). Expression, @@ -8775,14 +8365,12 @@ fn parseexpr(p: *parser) *node = { // stores the current token as flat primitive fields rather than a // nested `tok` struct; `refill` copies a freshly lexed token in. -package parse; +package syntax; -// Sibling imports (expr, stmt, decl) auto-resolve via task #22 -// dir-enum when callers `import parse;` (which dir-enums -// lib/ww/parse/). +// Sibling files (expr, stmt, decl) are the same `package syntax`, +// auto-resolved via task #22 dir-enum of lib/ww/syntax/. import os; import strings; -import tok; type parser = struct { l: *lex, @@ -9311,13 +8899,12 @@ export fn parsefile(p: *parser) *node = { return f; }; -//ww:module parse -// lib/ww/parse/stmt.ww — statement parsing, split out of parse.ww. +//ww:module syntax +// lib/ww/syntax/stmt.ww — statement parsing, split out of parse.ww. -package parse; +package syntax; import os; -import tok; fn parseletlocal(p: *parser) *node = { let pf = p.curfile; @@ -9726,8 +9313,748 @@ fn parsestmt(p: *parser) *node = { }; -//ww:module-reset -// lib/ww/typ.ww — port of cmd/wcc/type.c. +//ww:module syntax +// lib/ww/syntax/sym.ww — port of cmd/wcc/sym.c. +// +// Per-scope hashtable, chained to the parent. Lookup walks up. +// Plan 9 / Hare flavoured. Duplicate definitions in the same scope +// return nil; the caller flags the error. + +package syntax; + +// Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind. +type skind = enum i32 { + SK_NONE = 0, + SK_VAR = 1, + SK_PARAM = 2, + SK_DEF = 3, + SK_TYPE = 4, + SK_FN = 5, + SK_USE = 6, + SK_FIELD = 7, +}; + +type sym = struct { + name: str, + skind: skind, + type_: *tinfo, + decl: *node, + exported: i32, + is_const: i32, // const-bound (assignment rejected) + use_alias: i32, // #30: this value/type decl ALSO names an imported + // module (the fnmatch.fnmatch / random.random shape). + // Set when installtop promotes a same-leaf SK_USE in + // place; the N_DOT guards treat such a sym as a module + // for `name.member`. Mirror cstage Sym.use_alias + // (cmd/wcc/check.c:2831-2951 promote + 87/1337 guards). + mod: str, // importing module's bareword for symbols + // from a `use`-imported module; "" for primary + // (root) compilation unit symbols. Used by + // scopelookupinmodule to disambiguate same-leaf- + // name types coming from different imports. + snext: *sym, // iteration order + hashnext: *sym, // hash bucket chain + scope: *scope, +}; + +def NBUCKETS: i32 = 16; + +type scope = struct { + parent: *scope, + first: *sym, + last: *sym, + buckets: **sym, // length = NBUCKETS + nbuckets: i32, +}; + +// FNV-1a 64 — same hash the C side uses, so bucket distribution is +// identical when both walk a scope in declaration order. +fn hashstr(s: str) u64 = { + let h: u64 = 14695981039346656037u64; + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + h = h ^ (c: u64); + h = h * 1099511628211u64; + i += 1; + }; + return h; +}; + +export fn newscope(parent: *scope) *scope = { + let buckets_sl: []*sym = alloc([], NBUCKETS: u64)!; + let s: *scope = alloc(scope{parent=parent, first=nil, last=nil, buckets=buckets_sl.ptr, nbuckets=NBUCKETS})!; + return s; +}; + +export fn streq(a: str, b: str) bool = { + if (a.len != b.len) { return false; }; + let i: i32 = 0; + for (i < a.len) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +export fn scopelookuplocal(s: *scope, name: str) *sym = { + if (s == nil) { return nil; }; + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + let bn: str = b.name; + if (streq(bn, name)) { return b; }; + b = b.hashnext; + }; + return nil; +}; + +export fn scopelookup(s: *scope, name: str) *sym = { + for (s != nil) { + let r: *sym = scopelookuplocal(s, name); + if (r != nil) { return r; }; + s = s.parent; + }; + return nil; +}; + +// scopelookuptype — find an SK_TYPE entry by name, same-module preferred. +// +// Same FNV bucket + hashnext chain + parent walk as scopelookup, with +// an `skind == SK_TYPE` filter. Used to disambiguate the bare-TNAME +// vs imported-module-bareword collision: when scopelookup returns the +// SK_USE sym for a leaf that ALSO names a type (a same-name `import X;` +// SK_USE shadowing a struct X declared in another module), the resolver +// needs the type entry — the struct's mod may differ from the leaf so +// scopelookupinmodule(c, leaf, leaf) won't find it. +// +// #58/#50: within each scope, Pass-1 prefers an SK_TYPE whose `sym.mod` +// matches `mod`; Pass-2 falls back to the first SK_TYPE regardless of +// mod (chain-first, the prior behavior). scopedefineinmodule PREPENDS, +// so chain-first = last-registered — when two modules export the same +// type leaf the bare walk silently picked the newest-installed one, +// install-order-dependent, while cstage is deterministic on cur_mod. +// Mirrors cstage cmd/wcc/sym.c scope_lookup_type(s, mod, name) (the +// kind-filtered + mod-preferring single walk); sole caller passes +// c.curmod. i32-correct: streq throughout, only `.len > 0` guards (the +// reverted attempt compared str.len as u64 — str.len is i32). +export fn scopelookuptype(s: *scope, mod: str, name: str) *sym = { + let p: *scope = s; + for (p != nil) { + let h: u64 = hashstr(name); + let bi: i32 = (h % (p.nbuckets: u64)): i32; + let b: *sym = p.buckets[bi]; + let fallback: *sym = nil; + for (b != nil) { + if (streq(b.name, name)) { + if (b.skind == skind.SK_TYPE) { + if (mod.len > 0) { + if (b.mod.len > 0) { + if (streq(b.mod, mod)) { + return b; + }; + }; + }; + if (fallback == nil) { fallback = b; }; + }; + }; + b = b.hashnext; + }; + if (fallback != nil) { return fallback; }; + p = p.parent; + }; + return nil; +}; + +// scopelookupuselocal — find a same-leaf SK_USE entry within ONE scope. +// +// Same FNV bucket + hashnext chain as scopelookuplocal, with a +// `skind == SK_USE` filter and NO parent walk. The dot-lhs twin of +// scopelookuptype: when a `use mod;` and a colliding top-level +// `fn mod` / `type mod` of the same leaf coexist (random.random, +// fnmatch.fnmatch), the mod-preferring scopelookupprefer returns the +// SK_FN/SK_TYPE whose mod matches the importing unit's package, masking +// the SK_USE. A dot-lhs `mod.x` must resolve `mod` to the SK_USE for the +// module-qualified arm to fire, so the resolver re-resolves through this +// filter — keyed on the scope where scopelookupprefer LANDED — when it +// lands on a non-USE same-leaf entry. +// +// Single-scope (not a parent walk) so a local binding that shares a leaf +// with a top-level `use` keeps value semantics: scopelookupprefer +// resolves the local in its inner scope, whose bucket holds no SK_USE, +// so this returns nil and the dot stays field access. Only a genuine +// same-scope coexistence (top-level use + top-level type/fn) re-resolves. +// +// #30 (design reversal): this serves the DISTINCT-mod two-sym case only — +// a `fn fnmatch` (mod="fnmatch") coexisting with `import fnmatch`'s SK_USE +// (mod=""), where scopelookupprefer lands on the value and the dot re- +// resolves to the SK_USE here. The SAME-mod collision (a primary-package +// decl whose leaf also names a bundled module — `type sym` vs `import sym`, +// `@test fn ascii` vs the fnmatch->ascii bundle floor) is NO LONGER left to +// two coexisting syms: that ripples into every bare-ref resolver (a missed +// site is a byte-id-consistent-but-wrong cat-A risk the gate can't prove +// away). Instead installtop now PROMOTES the SK_USE in place to the value +// kind with use_alias=1 (selfhost/cmd/wcc/check.ww installtop), mirroring +// cstage's Sym.use_alias promote exactly (cmd/wcc/check.c:2831-2951); the +// N_DOT guards honor `skind == SK_USE || use_alias` directly. ONE +// correctly-kinded sym → all resolvers correct by construction. Cite: +// task #30; project memory module_type_name_collision (cstage 2026-05-13). +export fn scopelookupuselocal(s: *scope, name: str) *sym = { + if (s == nil) { return nil; }; + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + if (streq(b.name, name)) { + if (b.skind == skind.SK_USE) { return b; }; + }; + b = b.hashnext; + }; + return nil; +}; + +// scopelookupinmodule — module-filtered chain walk. +// +// Same FNV bucket + hashnext chain + parent walk as scopelookup, plus +// a `b.mod.len > 0 && streq(b.mod, mod)` filter. When `mod` is empty +// we fall back to unfiltered scopelookup semantics, so callers that +// don't care about disambiguation get the default. +// +// Used by the dot-prefixed type-name lookup in selfhost/cmd/wcc/ +// check.ww to pick the right same-leaf-name type when two imports +// each export it (`bufio.stream` vs `io.stream`). +export fn scopelookupinmodule(s: *scope, mod: str, name: str) *sym = { + if (mod.len == 0) { return scopelookup(s, name); }; + for (s != nil) { + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + if (streq(b.name, name)) { + if (b.mod.len > 0) { + if (streq(b.mod, mod)) { + return b; + }; + }; + }; + b = b.hashnext; + }; + s = s.parent; + }; + return nil; +}; + +// scopelookupprefer — bare-leaf lookup with same-module preference. +// +// Walks the same FNV bucket + hashnext chain + parent walk scopelookup +// uses. Within each scope's bucket: Pass 1 prefers entries whose +// `sym.mod` matches `mod`; Pass 2 falls back to the first match +// regardless of mod (same semantics as scopelookup). We only descend +// to the parent scope when the current scope has no matching entry at +// all — so a local binding in a closer scope still shadows a same-name +// fn from a parent scope, even when the parent entry mod-matches. +// +// When `mod` is empty we just call scopelookup — there's no module +// identity to prefer. +// +// Used at bare-leaf lookup sites inside a known current module so that +// a bare `read` inside lib/os resolves to os.read rather than the +// io.read that happens to hash earlier into the flat scope. Mirrors +// cmd/wcc/sym.c scope_lookup_prefer. +export fn scopelookupprefer(s: *scope, mod: str, name: str) *sym = { + if (mod.len == 0) { return scopelookup(s, name); }; + let p: *scope = s; + for (p != nil) { + let h: u64 = hashstr(name); + let bi: i32 = (h % (p.nbuckets: u64)): i32; + let b: *sym = p.buckets[bi]; + let fallback: *sym = nil; + for (b != nil) { + if (streq(b.name, name)) { + if (b.mod.len > 0) { + if (streq(b.mod, mod)) { + return b; + }; + }; + if (fallback == nil) { fallback = b; }; + }; + b = b.hashnext; + }; + if (fallback != nil) { return fallback; }; + p = p.parent; + }; + return nil; +}; + +export fn scopedefine(s: *scope, name: str, k: skind, t: *tinfo, decl: *node) *sym = { + let empty: str; + return scopedefineinmodule(s, name, empty, k, t, decl); +}; + +// scopedefineinmodule — bucket insert with per-mod dedup. +// +// Same insertion as scopedefine, but the duplicate-rejection key is +// (name, mod) rather than name alone. This lets two imports each +// register their own `stream` SK_TYPE in the flat scope, and lets the +// primary register `stream` (mod="") alongside imported `stream`s. +// +// Within a single (name, mod) pair the first registration wins; later +// attempts return nil and the caller can flag the error. +export fn scopedefineinmodule(s: *scope, name: str, mod: str, k: skind, t: *tinfo, decl: *node) *sym = { + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + if (streq(b.name, name)) { + if (b.mod.len == 0) { + if (mod.len == 0) { return nil; }; + } else { + if (mod.len > 0) { + if (streq(b.mod, mod)) { return nil; }; + }; + }; + }; + b = b.hashnext; + }; + let sy: *sym = alloc(sym{name=name, skind=k, type_=t, decl=decl, exported=0, is_const=0, use_alias=0, mod=mod, snext=nil, hashnext=s.buckets[bi], scope=s})!; + s.buckets[bi] = sy; + if (s.first == nil) { s.first = sy; } else { s.last.snext = sy; }; + s.last = sy; + return sy; +}; + +// scopesamekeysym — the entry scopedefineinmodule(name, mod) treats as a +// duplicate (same name, same mod-key), or nil if the key is free. Lets a +// caller that got a nil from scopedefineinmodule learn WHAT it collided +// with (e.g. a pre-seeded builtin vs a genuine user redeclaration). The +// match logic mirrors scopedefineinmodule's reject branch exactly. +export fn scopesamekeysym(s: *scope, name: str, mod: str) *sym = { + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + if (streq(b.name, name)) { + if (b.mod.len == 0) { + if (mod.len == 0) { return b; }; + } else { + if (mod.len > 0) { + if (streq(b.mod, mod)) { return b; }; + }; + }; + }; + b = b.hashnext; + }; + return nil; +}; + +//ww:module syntax +// lib/ww/syntax/tok.ww — port of cmd/wcc/tok.c plus the Tkind / +// Tok / Pos shapes from cmd/wcc/ww.h. +// +// Token kind values must stay numerically equal to the C side: the +// 990_selfhost test diffs ww-side wwdump output against C-side +// wwdump output, byte-for-byte. Reordering this list shifts the +// integers and breaks the diff. +// +// Bottom of file: tokprint, which emits one token per line in a +// format identical to cmd/wcc/tok.c:tokprint(). + +package syntax; + +import os; +import strconv; +import strings; + +// ---- tkind ------------------------------------------------------------ +// Mirror of the C `Tkind` enum in cmd/wcc/ww.h. Numeric values are +// explicit and must stay in sync — the 990_selfhost test diffs wwdump +// output against the C side, byte for byte. + +type tkind = enum i32 { + TK_NONE = 0, + TK_EOF = 1, + TK_ERR = 2, + TK_IDENT = 3, + TK_INT = 4, + TK_FLOAT = 5, + TK_RUNE = 6, + TK_STR = 7, + + TK_FN = 8, + TK_LET = 9, + TK_DEF = 10, + TK_IF = 11, + TK_ELSE = 12, + TK_FOR = 13, + TK_SWITCH = 14, + TK_CASE = 15, + TK_RETURN = 16, + TK_USE = 17, + TK_TYPE = 18, + TK_STRUCT = 19, + TK_DEFER = 20, + TK_BREAK = 21, + TK_CONTINUE = 22, + TK_EXPORT = 23, + TK_PROC = 24, + TK_CHAN = 25, + TK_NIL = 26, + TK_TRUE = 27, + TK_FALSE = 28, + TK_AS = 29, + TK_STATIC = 30, + TK_MATCH = 31, + TK_CONST = 32, + TK_UNDER = 33, + + TK_LPAREN = 34, + TK_RPAREN = 35, + TK_LBRACE = 36, + TK_RBRACE = 37, + TK_LBRACK = 38, + TK_RBRACK = 39, + TK_COMMA = 40, + TK_SEMI = 41, + TK_COLON = 42, + TK_DOT = 43, + TK_ELLIPSIS = 44, + TK_DOTDOT = 45, + TK_AT = 46, + TK_QUESTION = 47, + + TK_ASSIGN = 48, + TK_PLUSEQ = 49, + TK_MINUSEQ = 50, + TK_STAREQ = 51, + TK_SLASHEQ = 52, + TK_PERCENTEQ = 53, + TK_AMPEQ = 54, + TK_PIPEEQ = 55, + TK_CARETEQ = 56, + TK_LSHIFTEQ = 57, + TK_RSHIFTEQ = 58, + + TK_PLUS = 59, + TK_MINUS = 60, + TK_STAR = 61, + TK_SLASH = 62, + TK_PERCENT = 63, + TK_AMP = 64, + TK_PIPE = 65, + TK_CARET = 66, + TK_TILDE = 67, + TK_LSHIFT = 68, + TK_RSHIFT = 69, + + TK_EQ = 70, + TK_NEQ = 71, + TK_LT = 72, + TK_LE = 73, + TK_GT = 74, + TK_GE = 75, + + TK_AND = 76, + TK_OR = 77, + TK_NOT = 78, + + TK_LARROW = 79, + TK_ARROW = 80, + TK_FATARROW = 81, + + // Tail-appended values — keeps every prior TK_* numeric value + // stable for the 990_selfhost byte-diff against the C side. + TK_IS = 82, + TK_VOID = 83, + TK_YIELD = 84, + TK_ENUM = 85, + TK_MODULE = 86, // `module foo;` — directory-as-module decl + TK_MODRESET = 87, // `//ww:module-reset` driver bundle boundary: + // reset curmod to "" before a package-less file + // (#16 option-B; cstage TK_MODRESET twin) + TK_MODPATH = 88, // `//ww:module ` driver import + // boundary; decls mangle on the path, not the + // leaf `package` clause (M1 #22; cstage twin) + TK_LAST = 89, +}; + +// ---- Pos / Tok -------------------------------------------------------- +// +// `pos` is used at error-reporting boundaries; we always pass it via +// *pos so the value never gets struct-copied (w6c can't yet copy a +// 24-byte struct). +// +// `tok` is flat — file/line/col live directly on the token rather than +// nested inside a `pos` field. Same reason: nested struct field +// assignment isn't supported, and flat primitives are. + +type pos = struct { + file: str, + line: i32, + col: i32, +}; + +type tok = struct { + kind: tkind, + file: str, // path of the source the token came from + line: i32, + col: i32, + text: str, // arena-owned token text (tkind.TK_IDENT, tkind.TK_STR, tkind.TK_ERR) + uval: u64, // tkind.TK_INT, tkind.TK_RUNE + fval: f64, // tkind.TK_FLOAT + tsuffix: str, // typed numeric literal suffix or empty +}; + +// ---- keyword lookup --------------------------------------------------- + +// keep alphabetised, so kwlookup is easy to read — mirrors the C twin +// cmd/wcc/tok.c:18-47. Two parallel arrays, not a [N]kwent array-of- +// struct: a str *inside* an aggregate element is the filed #18 follow-up +// (str-in-aggregate). `let`, not `def`: #18's module-level [N]str static +// init is scoped to the DATAW (`let`) directive (A_DATAR needs a DATAW +// holder); `def [N]str` is the same filed follow-up. kwkinds is a plain +// [N]tkind enum byte-array (pre-#18 path). Explicit [30], NOT [_]: +// [_] static-init silently miscompiles to a zero-length array in-tree +// (probe at cc69daf — len() returns 0, no diagnostic); filed bug. +let kwnames: [30]str = [ + "as", "break", "case", "chan", "const", "continue", "def", "defer", + "else", "enum", "export", "false", "fn", "for", "if", "is", + "import", "let", "match", "nil", "package", "proc", "return", + "static", "struct", "switch", "true", "type", "void", "yield", +]; +let kwkinds: [30]tkind = [ + tkind.TK_AS, tkind.TK_BREAK, tkind.TK_CASE, tkind.TK_CHAN, + tkind.TK_CONST, tkind.TK_CONTINUE, tkind.TK_DEF, tkind.TK_DEFER, + tkind.TK_ELSE, tkind.TK_ENUM, tkind.TK_EXPORT, tkind.TK_FALSE, + tkind.TK_FN, tkind.TK_FOR, tkind.TK_IF, tkind.TK_IS, + tkind.TK_USE, tkind.TK_LET, tkind.TK_MATCH, tkind.TK_NIL, + tkind.TK_MODULE, tkind.TK_PROC, tkind.TK_RETURN, tkind.TK_STATIC, + tkind.TK_STRUCT, tkind.TK_SWITCH, tkind.TK_TRUE, tkind.TK_TYPE, + tkind.TK_VOID, tkind.TK_YIELD, +]; + +// kwlookup — returns the matching TK_* keyword kind for a byte run, +// or tkind.TK_NONE if it's an ordinary identifier. Linear scan over the +// table, matching cmd/wcc/tok.c:kwlookup (N=30, no hash). +export fn kwlookup(p: *u8, n: i32) tkind = { + let cand: str; + cand.ptr = p; + cand.len = n; + let i: i32 = 0; + for (i < len(kwnames)) { + if (strings.compare(cand, kwnames[i]) == 0) { + return kwkinds[i]; + }; + i += 1; + }; + return tkind.TK_NONE; +}; + +// ---- tokname ---------------------------------------------------------- +// +// Returns the canonical printable spelling for a token kind. Matches +// the C tokname()'s output exactly so wwdump output diffs cleanly. + +export fn tokname(k: tkind) str = { + switch (k) { + case tkind.TK_NONE: return ""; + case tkind.TK_EOF: return "EOF"; + case tkind.TK_ERR: return "ERR"; + case tkind.TK_IDENT: return "IDENT"; + case tkind.TK_INT: return "INT"; + case tkind.TK_FLOAT: return "FLOAT"; + case tkind.TK_RUNE: return "RUNE"; + case tkind.TK_STR: return "STR"; + + case tkind.TK_FN: return "fn"; + case tkind.TK_LET: return "let"; + case tkind.TK_DEF: return "def"; + case tkind.TK_IF: return "if"; + case tkind.TK_ELSE: return "else"; + case tkind.TK_FOR: return "for"; + case tkind.TK_SWITCH: return "switch"; + case tkind.TK_CASE: return "case"; + case tkind.TK_RETURN: return "return"; + case tkind.TK_USE: return "import"; + case tkind.TK_TYPE: return "type"; + case tkind.TK_STRUCT: return "struct"; + case tkind.TK_DEFER: return "defer"; + case tkind.TK_BREAK: return "break"; + case tkind.TK_CONTINUE: return "continue"; + case tkind.TK_EXPORT: return "export"; + case tkind.TK_PROC: return "proc"; + case tkind.TK_CHAN: return "chan"; + case tkind.TK_NIL: return "nil"; + case tkind.TK_TRUE: return "true"; + case tkind.TK_FALSE: return "false"; + case tkind.TK_AS: return "as"; + case tkind.TK_IS: return "is"; + case tkind.TK_VOID: return "void"; + case tkind.TK_YIELD: return "yield"; + case tkind.TK_STATIC: return "static"; + case tkind.TK_MATCH: return "match"; + case tkind.TK_CONST: return "const"; + case tkind.TK_UNDER: return "_"; + case tkind.TK_ENUM: return "enum"; + case tkind.TK_MODULE: return "package"; + case tkind.TK_MODRESET: return "//ww:module-reset"; + case tkind.TK_MODPATH: return "//ww:module"; + + case tkind.TK_LPAREN: return "("; + case tkind.TK_RPAREN: return ")"; + case tkind.TK_LBRACE: return "{"; + case tkind.TK_RBRACE: return "}"; + case tkind.TK_LBRACK: return "["; + case tkind.TK_RBRACK: return "]"; + case tkind.TK_COMMA: return ","; + case tkind.TK_SEMI: return ";"; + case tkind.TK_COLON: return ":"; + case tkind.TK_DOT: return "."; + case tkind.TK_ELLIPSIS: return "..."; + case tkind.TK_DOTDOT: return ".."; + case tkind.TK_AT: return "@"; + case tkind.TK_QUESTION: return "?"; + + case tkind.TK_ASSIGN: return "="; + case tkind.TK_PLUSEQ: return "+="; + case tkind.TK_MINUSEQ: return "-="; + case tkind.TK_STAREQ: return "*="; + case tkind.TK_SLASHEQ: return "/="; + case tkind.TK_PERCENTEQ: return "%="; + case tkind.TK_AMPEQ: return "&="; + case tkind.TK_PIPEEQ: return "|="; + case tkind.TK_CARETEQ: return "^="; + case tkind.TK_LSHIFTEQ: return "<<="; + case tkind.TK_RSHIFTEQ: return ">>="; + + case tkind.TK_PLUS: return "+"; + case tkind.TK_MINUS: return "-"; + case tkind.TK_STAR: return "*"; + case tkind.TK_SLASH: return "/"; + case tkind.TK_PERCENT: return "%"; + case tkind.TK_AMP: return "&"; + case tkind.TK_PIPE: return "|"; + case tkind.TK_CARET: return "^"; + case tkind.TK_TILDE: return "~"; + case tkind.TK_LSHIFT: return "<<"; + case tkind.TK_RSHIFT: return ">>"; + + case tkind.TK_EQ: return "=="; + case tkind.TK_NEQ: return "!="; + case tkind.TK_LT: return "<"; + case tkind.TK_LE: return "<="; + case tkind.TK_GT: return ">"; + case tkind.TK_GE: return ">="; + + case tkind.TK_AND: return "&&"; + case tkind.TK_OR: return "||"; + case tkind.TK_NOT: return "!"; + + case tkind.TK_LARROW: return "<-"; + case tkind.TK_ARROW: return "->"; + case tkind.TK_FATARROW: return "=>"; + + case tkind.TK_LAST: return ""; + }; + return ""; +}; + +// ---- writer for tokprint ---------------------------------------------- +// +// fputq mirrors cmd/wcc/tok.c:fputq — quote the string with C-style +// escapes for \, ", \n, \t, \r and \xNN for other non-printables. + +fn fputcbyte(fd: i32, b: u8) void = { + let buf: [1]u8; + buf[0] = b; + os.write(fd, buf.ptr, 1u64); +}; + +fn fputsstr(fd: i32, s: str) void = { + os.write(fd, s.ptr, s.len: u64); +}; + +fn hexchar(n: u8) u8 = { + if (n < 10u8) { return n + 48u8; }; // '0'..'9' + return (n - 10u8) + 97u8; // 'a'..'f' +}; + +fn fputhex2(fd: i32, b: u8) void = { + let out: [4]u8; + out[0] = '\\'; + out[1] = 'x'; + out[2] = hexchar(b >> 4u8); + out[3] = hexchar(b & 15u8); + os.write(fd, out.ptr, 4u64); +}; + +fn fputq(fd: i32, p: *u8, n: i32) void = { + fputcbyte(fd, '"'); + let i: i32 = 0; + for (i < n) { + let c: u8 = p[i]; + switch (c) { + case '\\': fputsstr(fd, "\\\\"); + case '"': fputsstr(fd, "\\\""); + case '\n': fputsstr(fd, "\\n"); + case '\t': fputsstr(fd, "\\t"); + case '\r': fputsstr(fd, "\\r"); + case: + if (c < ' ' || c == 127u8) { + fputhex2(fd, c); + } else { + fputcbyte(fd, c); + }; + }; + i += 1; + }; + fputcbyte(fd, '"'); +}; + +// tokprint — write one token line to fd. Format must match +// cmd/wcc/tok.c:tokprint() byte-for-byte: that's the diff anchor. +// ":: [ ]\n" +// +// Takes `t` by pointer because w6c can't yet pass a >16-byte struct +// by value; the C version takes Tok by value. +export fn tokprint(fd: i32, t: *tok) void = { + // Chained-dot field reads (`t.x.y`) on str sub-fields aren't yet + // reduced by w6c — `t.x.y` returns the whole str. Lift the str + // fields into locals so we can use the str pseudo-field path. + let tfile: str = t.file; + let ttext: str = t.text; + if (tfile.len > 0) { + fputsstr(fd, tfile); + } else { + fputsstr(fd, ""); + }; + fputcbyte(fd, ':'); + let ls: str = strconv.i64tos(t.line: i64, strconv.base.DEC); + os.write(fd, ls.ptr, ls.len: u64); + fputcbyte(fd, ':'); + let cs: str = strconv.i64tos(t.col: i64, strconv.base.DEC); + os.write(fd, cs.ptr, cs.len: u64); + fputcbyte(fd, ' '); + fputsstr(fd, tokname(t.kind)); + + switch (t.kind) { + case tkind.TK_IDENT, tkind.TK_STR, tkind.TK_ERR: + fputcbyte(fd, ' '); + fputq(fd, ttext.ptr, ttext.len); + case tkind.TK_INT, tkind.TK_RUNE: + fputcbyte(fd, ' '); + let us: str = strconv.u64tos(t.uval, strconv.base.DEC); + os.write(fd, us.ptr, us.len: u64); + }; + // tkind.TK_FLOAT is intentionally not handled here — %g formatting + // won't byte-match across implementations. Diff fixtures must + // be float-free until we implement a stable float formatter. + + fputcbyte(fd, '\n'); +}; + +//ww:module syntax +// lib/ww/syntax/typ.ww — port of cmd/wcc/type.c. // // Status: full structural port. The C version uses module-globals for // the primitive types (tyvoid, tyi32, …); ww doesn't have writable @@ -9735,7 +10062,7 @@ fn parsestmt(p: *parser) *node = { // the checker passes around explicitly. typesinit fills the tctx // once per program. -package ww; +package syntax; import os; @@ -10343,342 +10670,6 @@ export fn typeeq(a: *tinfo, b: *tinfo) bool = { return true; // primitives match by kind alone }; -//ww:module-reset -// lib/ww/sym.ww — port of cmd/wcc/sym.c. -// -// Per-scope hashtable, chained to the parent. Lookup walks up. -// Plan 9 / Hare flavoured. Duplicate definitions in the same scope -// return nil; the caller flags the error. - -package ww; - -// Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind. -type skind = enum i32 { - SK_NONE = 0, - SK_VAR = 1, - SK_PARAM = 2, - SK_DEF = 3, - SK_TYPE = 4, - SK_FN = 5, - SK_USE = 6, - SK_FIELD = 7, -}; - -type sym = struct { - name: str, - skind: skind, - type_: *tinfo, - decl: *node, - exported: i32, - is_const: i32, // const-bound (assignment rejected) - use_alias: i32, // #30: this value/type decl ALSO names an imported - // module (the fnmatch.fnmatch / random.random shape). - // Set when installtop promotes a same-leaf SK_USE in - // place; the N_DOT guards treat such a sym as a module - // for `name.member`. Mirror cstage Sym.use_alias - // (cmd/wcc/check.c:2831-2951 promote + 87/1337 guards). - mod: str, // importing module's bareword for symbols - // from a `use`-imported module; "" for primary - // (root) compilation unit symbols. Used by - // scopelookupinmodule to disambiguate same-leaf- - // name types coming from different imports. - snext: *sym, // iteration order - hashnext: *sym, // hash bucket chain - scope: *scope, -}; - -def NBUCKETS: i32 = 16; - -type scope = struct { - parent: *scope, - first: *sym, - last: *sym, - buckets: **sym, // length = NBUCKETS - nbuckets: i32, -}; - -// FNV-1a 64 — same hash the C side uses, so bucket distribution is -// identical when both walk a scope in declaration order. -fn hashstr(s: str) u64 = { - let h: u64 = 14695981039346656037u64; - let i: i32 = 0; - for (i < s.len) { - let c: u8 = s[i]; - h = h ^ (c: u64); - h = h * 1099511628211u64; - i += 1; - }; - return h; -}; - -export fn newscope(parent: *scope) *scope = { - let buckets_sl: []*sym = alloc([], NBUCKETS: u64)!; - let s: *scope = alloc(scope{parent=parent, first=nil, last=nil, buckets=buckets_sl.ptr, nbuckets=NBUCKETS})!; - return s; -}; - -export fn streq(a: str, b: str) bool = { - if (a.len != b.len) { return false; }; - let i: i32 = 0; - for (i < a.len) { - if (a[i] != b[i]) { return false; }; - i += 1; - }; - return true; -}; - -export fn scopelookuplocal(s: *scope, name: str) *sym = { - if (s == nil) { return nil; }; - let h: u64 = hashstr(name); - let bi: i32 = (h % (s.nbuckets: u64)): i32; - let b: *sym = s.buckets[bi]; - for (b != nil) { - let bn: str = b.name; - if (streq(bn, name)) { return b; }; - b = b.hashnext; - }; - return nil; -}; - -export fn scopelookup(s: *scope, name: str) *sym = { - for (s != nil) { - let r: *sym = scopelookuplocal(s, name); - if (r != nil) { return r; }; - s = s.parent; - }; - return nil; -}; - -// scopelookuptype — find an SK_TYPE entry by name, same-module preferred. -// -// Same FNV bucket + hashnext chain + parent walk as scopelookup, with -// an `skind == SK_TYPE` filter. Used to disambiguate the bare-TNAME -// vs imported-module-bareword collision: when scopelookup returns the -// SK_USE sym for a leaf that ALSO names a type (e.g. `tok` struct -// declared in lib/ww/lex/tok.ww with `package lex;` while -// `import tok;` registers a same-name SK_USE), the resolver needs -// the type entry — the struct's mod may differ from the leaf (lex/tok -// pair) so scopelookupinmodule(c, leaf, leaf) won't find it. -// -// #58/#50: within each scope, Pass-1 prefers an SK_TYPE whose `sym.mod` -// matches `mod`; Pass-2 falls back to the first SK_TYPE regardless of -// mod (chain-first, the prior behavior). scopedefineinmodule PREPENDS, -// so chain-first = last-registered — when two modules export the same -// type leaf the bare walk silently picked the newest-installed one, -// install-order-dependent, while cstage is deterministic on cur_mod. -// Mirrors cstage cmd/wcc/sym.c scope_lookup_type(s, mod, name) (the -// kind-filtered + mod-preferring single walk); sole caller passes -// c.curmod. i32-correct: streq throughout, only `.len > 0` guards (the -// reverted attempt compared str.len as u64 — str.len is i32). -export fn scopelookuptype(s: *scope, mod: str, name: str) *sym = { - let p: *scope = s; - for (p != nil) { - let h: u64 = hashstr(name); - let bi: i32 = (h % (p.nbuckets: u64)): i32; - let b: *sym = p.buckets[bi]; - let fallback: *sym = nil; - for (b != nil) { - if (streq(b.name, name)) { - if (b.skind == skind.SK_TYPE) { - if (mod.len > 0) { - if (b.mod.len > 0) { - if (streq(b.mod, mod)) { - return b; - }; - }; - }; - if (fallback == nil) { fallback = b; }; - }; - }; - b = b.hashnext; - }; - if (fallback != nil) { return fallback; }; - p = p.parent; - }; - return nil; -}; - -// scopelookupuselocal — find a same-leaf SK_USE entry within ONE scope. -// -// Same FNV bucket + hashnext chain as scopelookuplocal, with a -// `skind == SK_USE` filter and NO parent walk. The dot-lhs twin of -// scopelookuptype: when a `use mod;` and a colliding top-level -// `fn mod` / `type mod` of the same leaf coexist (random.random, -// fnmatch.fnmatch), the mod-preferring scopelookupprefer returns the -// SK_FN/SK_TYPE whose mod matches the importing unit's package, masking -// the SK_USE. A dot-lhs `mod.x` must resolve `mod` to the SK_USE for the -// module-qualified arm to fire, so the resolver re-resolves through this -// filter — keyed on the scope where scopelookupprefer LANDED — when it -// lands on a non-USE same-leaf entry. -// -// Single-scope (not a parent walk) so a local binding that shares a leaf -// with a top-level `use` keeps value semantics: scopelookupprefer -// resolves the local in its inner scope, whose bucket holds no SK_USE, -// so this returns nil and the dot stays field access. Only a genuine -// same-scope coexistence (top-level use + top-level type/fn) re-resolves. -// -// #30 (design reversal): this serves the DISTINCT-mod two-sym case only — -// a `fn fnmatch` (mod="fnmatch") coexisting with `import fnmatch`'s SK_USE -// (mod=""), where scopelookupprefer lands on the value and the dot re- -// resolves to the SK_USE here. The SAME-mod collision (a primary-package -// decl whose leaf also names a bundled module — `type sym` vs `import sym`, -// `@test fn ascii` vs the fnmatch->ascii bundle floor) is NO LONGER left to -// two coexisting syms: that ripples into every bare-ref resolver (a missed -// site is a byte-id-consistent-but-wrong cat-A risk the gate can't prove -// away). Instead installtop now PROMOTES the SK_USE in place to the value -// kind with use_alias=1 (selfhost/cmd/wcc/check.ww installtop), mirroring -// cstage's Sym.use_alias promote exactly (cmd/wcc/check.c:2831-2951); the -// N_DOT guards honor `skind == SK_USE || use_alias` directly. ONE -// correctly-kinded sym → all resolvers correct by construction. Cite: -// task #30; project memory module_type_name_collision (cstage 2026-05-13). -export fn scopelookupuselocal(s: *scope, name: str) *sym = { - if (s == nil) { return nil; }; - let h: u64 = hashstr(name); - let bi: i32 = (h % (s.nbuckets: u64)): i32; - let b: *sym = s.buckets[bi]; - for (b != nil) { - if (streq(b.name, name)) { - if (b.skind == skind.SK_USE) { return b; }; - }; - b = b.hashnext; - }; - return nil; -}; - -// scopelookupinmodule — module-filtered chain walk. -// -// Same FNV bucket + hashnext chain + parent walk as scopelookup, plus -// a `b.mod.len > 0 && streq(b.mod, mod)` filter. When `mod` is empty -// we fall back to unfiltered scopelookup semantics, so callers that -// don't care about disambiguation get the default. -// -// Used by the dot-prefixed type-name lookup in selfhost/cmd/wcc/ -// check.ww to pick the right same-leaf-name type when two imports -// each export it (`bufio.stream` vs `io.stream`). -export fn scopelookupinmodule(s: *scope, mod: str, name: str) *sym = { - if (mod.len == 0) { return scopelookup(s, name); }; - for (s != nil) { - let h: u64 = hashstr(name); - let bi: i32 = (h % (s.nbuckets: u64)): i32; - let b: *sym = s.buckets[bi]; - for (b != nil) { - if (streq(b.name, name)) { - if (b.mod.len > 0) { - if (streq(b.mod, mod)) { - return b; - }; - }; - }; - b = b.hashnext; - }; - s = s.parent; - }; - return nil; -}; - -// scopelookupprefer — bare-leaf lookup with same-module preference. -// -// Walks the same FNV bucket + hashnext chain + parent walk scopelookup -// uses. Within each scope's bucket: Pass 1 prefers entries whose -// `sym.mod` matches `mod`; Pass 2 falls back to the first match -// regardless of mod (same semantics as scopelookup). We only descend -// to the parent scope when the current scope has no matching entry at -// all — so a local binding in a closer scope still shadows a same-name -// fn from a parent scope, even when the parent entry mod-matches. -// -// When `mod` is empty we just call scopelookup — there's no module -// identity to prefer. -// -// Used at bare-leaf lookup sites inside a known current module so that -// a bare `read` inside lib/os resolves to os.read rather than the -// io.read that happens to hash earlier into the flat scope. Mirrors -// cmd/wcc/sym.c scope_lookup_prefer. -export fn scopelookupprefer(s: *scope, mod: str, name: str) *sym = { - if (mod.len == 0) { return scopelookup(s, name); }; - let p: *scope = s; - for (p != nil) { - let h: u64 = hashstr(name); - let bi: i32 = (h % (p.nbuckets: u64)): i32; - let b: *sym = p.buckets[bi]; - let fallback: *sym = nil; - for (b != nil) { - if (streq(b.name, name)) { - if (b.mod.len > 0) { - if (streq(b.mod, mod)) { - return b; - }; - }; - if (fallback == nil) { fallback = b; }; - }; - b = b.hashnext; - }; - if (fallback != nil) { return fallback; }; - p = p.parent; - }; - return nil; -}; - -export fn scopedefine(s: *scope, name: str, k: skind, t: *tinfo, decl: *node) *sym = { - let empty: str; - return scopedefineinmodule(s, name, empty, k, t, decl); -}; - -// scopedefineinmodule — bucket insert with per-mod dedup. -// -// Same insertion as scopedefine, but the duplicate-rejection key is -// (name, mod) rather than name alone. This lets two imports each -// register their own `stream` SK_TYPE in the flat scope, and lets the -// primary register `stream` (mod="") alongside imported `stream`s. -// -// Within a single (name, mod) pair the first registration wins; later -// attempts return nil and the caller can flag the error. -export fn scopedefineinmodule(s: *scope, name: str, mod: str, k: skind, t: *tinfo, decl: *node) *sym = { - let h: u64 = hashstr(name); - let bi: i32 = (h % (s.nbuckets: u64)): i32; - let b: *sym = s.buckets[bi]; - for (b != nil) { - if (streq(b.name, name)) { - if (b.mod.len == 0) { - if (mod.len == 0) { return nil; }; - } else { - if (mod.len > 0) { - if (streq(b.mod, mod)) { return nil; }; - }; - }; - }; - b = b.hashnext; - }; - let sy: *sym = alloc(sym{name=name, skind=k, type_=t, decl=decl, exported=0, is_const=0, use_alias=0, mod=mod, snext=nil, hashnext=s.buckets[bi], scope=s})!; - s.buckets[bi] = sy; - if (s.first == nil) { s.first = sy; } else { s.last.snext = sy; }; - s.last = sy; - return sy; -}; - -// scopesamekeysym — the entry scopedefineinmodule(name, mod) treats as a -// duplicate (same name, same mod-key), or nil if the key is free. Lets a -// caller that got a nil from scopedefineinmodule learn WHAT it collided -// with (e.g. a pre-seeded builtin vs a genuine user redeclaration). The -// match logic mirrors scopedefineinmodule's reject branch exactly. -export fn scopesamekeysym(s: *scope, name: str, mod: str) *sym = { - let h: u64 = hashstr(name); - let bi: i32 = (h % (s.nbuckets: u64)): i32; - let b: *sym = s.buckets[bi]; - for (b != nil) { - if (streq(b.name, name)) { - if (b.mod.len == 0) { - if (mod.len == 0) { return b; }; - } else { - if (mod.len > 0) { - if (streq(b.mod, mod)) { return b; }; - }; - }; - }; - b = b.hashnext; - }; - return nil; -}; - //ww:module-reset // selfhost/cmd/wcc/check.ww — minimal port of cmd/wcc/check.c. // @@ -10702,7 +10693,7 @@ export fn scopesamekeysym(s: *scope, name: str, mod: str) *sym = { package wcc; import os; -import tok; +import syntax; import strconv; type checker = struct { @@ -18185,10 +18176,7 @@ export fn borrowedread(s: *stream, amt: i32) ([]u8 | io.eof) = { package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; // ---- variadic-call helpers (Hare-style `T...` param) ----------------- @@ -23742,10 +23730,7 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; // cgfloatbits — materialise a float constant in X0: MOVQ the IEEE bits @@ -36278,10 +36263,7 @@ fn cgassign(c: *cgen, n: *node) void = { package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; // ---- statement cgen -------------------------------------------------- @@ -40630,10 +40612,7 @@ fn cgcontinue(c: *cgen, n: *node) void = { package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; @@ -41311,10 +41290,7 @@ export fn cgfile(c: *cgen, file: *node) void = { package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; import strings; import io; @@ -45383,7 +45359,7 @@ export fn fargregname(i: i32) str = { package wcc; import os; -import tok; +import syntax; import strconv; // --- byte writers ------------------------------------------------------ @@ -46027,12 +46003,7 @@ package main; import os; import rt; import strings; -import tok; -import lex; -import ast; -import parse; -import typ; -import sym; +import syntax; import check; import cgen; import wwi; diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index a93cb401..84937add 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -15,12 +15,7 @@ package main; import os; import rt; import strings; -import tok; -import lex; -import ast; -import parse; -import typ; -import sym; +import syntax; import check; import cgen; import wwi; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 58a7756b..d31b0762 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -25,10 +25,7 @@ package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; import strings; import io; diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 585d8df4..bfbac4c9 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -13,10 +13,7 @@ package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index aa948cbb..3afc88c3 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -15,10 +15,7 @@ package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; // cgfloatbits — materialise a float constant in X0: MOVQ the IEEE bits diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index d2c8422c..cb816433 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -11,10 +11,7 @@ package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; // ---- statement cgen -------------------------------------------------- diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index b01ea6be..efb5cb7e 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -15,10 +15,7 @@ package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; // ---- variadic-call helpers (Hare-style `T...` param) ----------------- diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index f502ca1d..fb5f2771 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -20,7 +20,7 @@ package wcc; import os; -import tok; +import syntax; import strconv; type checker = struct { diff --git a/selfhost/cmd/wcc/wwi.ww b/selfhost/cmd/wcc/wwi.ww index 8792fdbb..c55c688c 100644 --- a/selfhost/cmd/wcc/wwi.ww +++ b/selfhost/cmd/wcc/wwi.ww @@ -21,7 +21,7 @@ package wcc; import os; -import tok; +import syntax; import strconv; // --- byte writers ------------------------------------------------------ diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 339c157e..e109b7e5 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -6417,413 +6417,1063 @@ export fn strerror(e: error) str = { return strings.dup(""); }; -//ww:module-reset -// lib/ww/lex/tok.ww — port of cmd/wcc/tok.c plus the Tkind / -// Tok / Pos shapes from cmd/wcc/ww.h. +//ww:module syntax +// lib/ww/syntax/ast.ww — port of cmd/wcc/ast.c (Node defs + printer). // -// Token kind values must stay numerically equal to the C side: the -// 990_selfhost test diffs ww-side wwdump output against C-side -// wwdump output, byte-for-byte. Reordering this list shifts the -// integers and breaks the diff. +// Status: AST printer is fully ported. Constructor `newnode` is here. +// The parser (parse.ww) is currently minimal — see its file header. // -// Bottom of file: tokprint, which emits one token per line in a -// format identical to cmd/wcc/tok.c:tokprint(). +// Calling-convention shim: same as tok/lex — `node` is too big to pass +// by value (8 *node pointers + 2 strs + a few ints), so callers always +// hand around `*node`. Only `newnode` allocates and returns a *node. -package lex; +package syntax; import os; import strconv; -import strings; -// ---- tkind ------------------------------------------------------------ -// Mirror of the C `Tkind` enum in cmd/wcc/ww.h. Numeric values are -// explicit and must stay in sync — the 990_selfhost test diffs wwdump -// output against the C side, byte for byte. - -type tkind = enum i32 { - TK_NONE = 0, - TK_EOF = 1, - TK_ERR = 2, - TK_IDENT = 3, - TK_INT = 4, - TK_FLOAT = 5, - TK_RUNE = 6, - TK_STR = 7, - - TK_FN = 8, - TK_LET = 9, - TK_DEF = 10, - TK_IF = 11, - TK_ELSE = 12, - TK_FOR = 13, - TK_SWITCH = 14, - TK_CASE = 15, - TK_RETURN = 16, - TK_USE = 17, - TK_TYPE = 18, - TK_STRUCT = 19, - TK_DEFER = 20, - TK_BREAK = 21, - TK_CONTINUE = 22, - TK_EXPORT = 23, - TK_PROC = 24, - TK_CHAN = 25, - TK_NIL = 26, - TK_TRUE = 27, - TK_FALSE = 28, - TK_AS = 29, - TK_STATIC = 30, - TK_MATCH = 31, - TK_CONST = 32, - TK_UNDER = 33, - - TK_LPAREN = 34, - TK_RPAREN = 35, - TK_LBRACE = 36, - TK_RBRACE = 37, - TK_LBRACK = 38, - TK_RBRACK = 39, - TK_COMMA = 40, - TK_SEMI = 41, - TK_COLON = 42, - TK_DOT = 43, - TK_ELLIPSIS = 44, - TK_DOTDOT = 45, - TK_AT = 46, - TK_QUESTION = 47, - - TK_ASSIGN = 48, - TK_PLUSEQ = 49, - TK_MINUSEQ = 50, - TK_STAREQ = 51, - TK_SLASHEQ = 52, - TK_PERCENTEQ = 53, - TK_AMPEQ = 54, - TK_PIPEEQ = 55, - TK_CARETEQ = 56, - TK_LSHIFTEQ = 57, - TK_RSHIFTEQ = 58, - - TK_PLUS = 59, - TK_MINUS = 60, - TK_STAR = 61, - TK_SLASH = 62, - TK_PERCENT = 63, - TK_AMP = 64, - TK_PIPE = 65, - TK_CARET = 66, - TK_TILDE = 67, - TK_LSHIFT = 68, - TK_RSHIFT = 69, - - TK_EQ = 70, - TK_NEQ = 71, - TK_LT = 72, - TK_LE = 73, - TK_GT = 74, - TK_GE = 75, - - TK_AND = 76, - TK_OR = 77, - TK_NOT = 78, - - TK_LARROW = 79, - TK_ARROW = 80, - TK_FATARROW = 81, - - // Tail-appended values — keeps every prior TK_* numeric value - // stable for the 990_selfhost byte-diff against the C side. - TK_IS = 82, - TK_VOID = 83, - TK_YIELD = 84, - TK_ENUM = 85, - TK_MODULE = 86, // `module foo;` — directory-as-module decl - TK_MODRESET = 87, // `//ww:module-reset` driver bundle boundary: - // reset curmod to "" before a package-less file - // (#16 option-B; cstage TK_MODRESET twin) - TK_MODPATH = 88, // `//ww:module ` driver import - // boundary; decls mangle on the path, not the - // leaf `package` clause (M1 #22; cstage twin) - TK_LAST = 89, -}; - -// ---- Pos / Tok -------------------------------------------------------- +// ---- Nkind ------------------------------------------------------------ // -// `pos` is used at error-reporting boundaries; we always pass it via -// *pos so the value never gets struct-copied (w6c can't yet copy a -// 24-byte struct). -// -// `tok` is flat — file/line/col live directly on the token rather than -// nested inside a `pos` field. Same reason: nested struct field -// assignment isn't supported, and flat primitives are. +// Mirror of cmd/wcc/ww.h Nkind. Values must stay numerically equal so +// the AST diff probe in 990_selfhost works. -type pos = struct { - file: str, - line: i32, - col: i32, +// Mirror of the C `Nkind` enum in cmd/wcc/ww.h. Numeric values are +// explicit and must stay in sync — the 990_selfhost test diffs +// astprint against the C side byte-for-byte. Tail-appended entries +// (TYPETEST onward) preserve every prior N_* value. +type nkind = enum i32 { + N_NONE = 0, + + N_INTLIT = 1, + N_FLOATLIT = 2, + N_STRLIT = 3, + N_RUNELIT = 4, + N_TRUE = 5, + N_FALSE = 6, + N_NIL = 7, + N_IDENT = 8, + + N_BIN = 9, + N_UN = 10, + N_CALL = 11, + N_INDEX = 12, + N_DOT = 13, + N_CAST = 14, + N_STRUCTLIT = 15, + N_ARRLIT = 16, + N_FIELD = 17, + N_ASSIGN = 18, + N_ALLOC = 19, + N_FREE = 20, + N_RECV = 21, + N_SLICE = 22, + N_SPREAD = 23, + + N_BLOCK = 24, + N_EXPRSTMT = 25, + N_LET = 26, + N_RETURN = 27, + N_IF = 28, + N_FOR = 29, + N_FORRANGE = 30, + N_DEFER = 31, + N_BREAK = 32, + N_CONTINUE = 33, + N_SWITCH = 34, + N_CASE = 35, + + N_FILE = 36, + N_USE = 37, + N_DEF = 38, + N_TYPEDECL = 39, + N_FNDECL = 40, + N_PARAM = 41, + + N_TNAME = 42, + N_TPTR = 43, + N_TSLICE = 44, + N_TARRAY = 45, + N_TFN = 46, + N_TSTRUCT = 47, + N_TFIELD = 48, + N_TCHAN = 49, + + N_ATTR = 50, + N_TTUPLE = 51, + N_TTAGGED = 52, + N_TUPLE = 53, + N_MATCH = 54, + N_MCASE = 55, + N_TRYPROP = 56, + N_TRYUNW = 57, + N_MLET = 58, + N_MASSIGN = 59, + + N_TYPETEST = 60, + N_TYPEASSERT = 61, + N_VOIDLIT = 62, + N_TBANG = 63, + N_YIELD = 64, + N_TENUM = 65, + N_TENUMMEMBER = 66, + + // N_TPARAM — chain wrapper for N_TTUPLE.list elements. Mirror of + // cstage's Tparam (cmd/wcc/check.c:1437-1451) lifted to the AST so + // `exprtype` can return shared element-type nodes (sym.decl.lhs, + // struct field's .lhs, another N_TTUPLE's .list element) without + // corrupting source ASTs by reusing their .next. .lhs holds the + // element type AST (possibly shared); .next chains within the + // parent N_TTUPLE.list. Cstage keeps Tparam at the Type-layer; ww + // has no separate type layer for tuple chains, so the wrapper sits + // at the AST layer. Other node fields are unused. Never appears + // outside an N_TTUPLE.list; astprint unwraps transparently to keep + // the 990 -a byte-diff against cstage. + N_TPARAM = 67, + + N_LAST = 68, }; -type tok = struct { - kind: tkind, - file: str, // path of the source the token came from - line: i32, - col: i32, - text: str, // arena-owned token text (tkind.TK_IDENT, tkind.TK_STR, tkind.TK_ERR) - uval: u64, // tkind.TK_INT, tkind.TK_RUNE - fval: f64, // tkind.TK_FLOAT - tsuffix: str, // typed numeric literal suffix or empty +// ---- Node ------------------------------------------------------------- + +type node = struct { + kind: nkind, + file: str, + line: i32, + col: i32, + op: tkind, // for nkind.N_BIN / nkind.N_UN / nkind.N_ASSIGN + str: str, + uval: u64, + fval: f64, + lhs: *node, + rhs: *node, + cond: *node, + body: *node, + els: *node, + list: *node, + next: *node, + attr: *node, + exported: i32, // bool — `export` keyword present + type_: *void, // filled in by checker; type.ww treats it as *tinfo + tsuffix: str, // typed numeric literal suffix ("i32", "u64", ...) + nmod: str, // originating module from `// MODULE: foo`; "" if none + usepath: str, // on an N_USE: full dotted IMPORT path vs leaf alias + // in `str` (M1 #22); "" otherwise + imported: i32, // M1 #22: decl reached via `//ww:module ` import + // boundary (vs root/primary); gates root-only bare main }; -// ---- keyword lookup --------------------------------------------------- +export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = { + // fval cast-init: 990's wwdump TK_FLOAT diff requires this file + // to tokenise identically through C and ww (lex.ww:382 has the + // same workaround for the cstage %g-formats vs ww-skips divergence). + let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, type_=nil, tsuffix="", nmod="", usepath="", imported=0})!; + return n; +}; -// keep alphabetised, so kwlookup is easy to read — mirrors the C twin -// cmd/wcc/tok.c:18-47. Two parallel arrays, not a [N]kwent array-of- -// struct: a str *inside* an aggregate element is the filed #18 follow-up -// (str-in-aggregate). `let`, not `def`: #18's module-level [N]str static -// init is scoped to the DATAW (`let`) directive (A_DATAR needs a DATAW -// holder); `def [N]str` is the same filed follow-up. kwkinds is a plain -// [N]tkind enum byte-array (pre-#18 path). Explicit [30], NOT [_]: -// [_] static-init silently miscompiles to a zero-length array in-tree -// (probe at cc69daf — len() returns 0, no diagnostic); filed bug. -let kwnames: [30]str = [ - "as", "break", "case", "chan", "const", "continue", "def", "defer", - "else", "enum", "export", "false", "fn", "for", "if", "is", - "import", "let", "match", "nil", "package", "proc", "return", - "static", "struct", "switch", "true", "type", "void", "yield", -]; -let kwkinds: [30]tkind = [ - tkind.TK_AS, tkind.TK_BREAK, tkind.TK_CASE, tkind.TK_CHAN, - tkind.TK_CONST, tkind.TK_CONTINUE, tkind.TK_DEF, tkind.TK_DEFER, - tkind.TK_ELSE, tkind.TK_ENUM, tkind.TK_EXPORT, tkind.TK_FALSE, - tkind.TK_FN, tkind.TK_FOR, tkind.TK_IF, tkind.TK_IS, - tkind.TK_USE, tkind.TK_LET, tkind.TK_MATCH, tkind.TK_NIL, - tkind.TK_MODULE, tkind.TK_PROC, tkind.TK_RETURN, tkind.TK_STATIC, - tkind.TK_STRUCT, tkind.TK_SWITCH, tkind.TK_TRUE, tkind.TK_TYPE, - tkind.TK_VOID, tkind.TK_YIELD, -]; +// ---- printer ---------------------------------------------------------- -// kwlookup — returns the matching TK_* keyword kind for a byte run, -// or tkind.TK_NONE if it's an ordinary identifier. Linear scan over the -// table, matching cmd/wcc/tok.c:kwlookup (N=30, no hash). -export fn kwlookup(p: *u8, n: i32) tkind = { - let cand: str; - cand.ptr = p; - cand.len = n; +export fn nkname(k: nkind) str = { + switch (k) { + case nkind.N_NONE: return "none"; + case nkind.N_INTLIT: return "int"; + case nkind.N_FLOATLIT: return "float"; + case nkind.N_STRLIT: return "str"; + case nkind.N_RUNELIT: return "rune"; + case nkind.N_TRUE: return "true"; + case nkind.N_FALSE: return "false"; + case nkind.N_NIL: return "nil"; + case nkind.N_IDENT: return "id"; + + case nkind.N_BIN: return "bin"; + case nkind.N_UN: return "un"; + case nkind.N_CALL: return "call"; + case nkind.N_INDEX: return "index"; + case nkind.N_DOT: return "dot"; + case nkind.N_CAST: return "cast"; + case nkind.N_STRUCTLIT: return "structlit"; + case nkind.N_ARRLIT: return "arrlit"; + case nkind.N_FIELD: return "field"; + case nkind.N_ASSIGN: return "assign"; + case nkind.N_ALLOC: return "alloc"; + case nkind.N_FREE: return "free"; + case nkind.N_RECV: return "recv"; + case nkind.N_SLICE: return "slice"; + case nkind.N_SPREAD: return "spread"; + + case nkind.N_BLOCK: return "block"; + case nkind.N_EXPRSTMT: return "exprstmt"; + case nkind.N_LET: return "let"; + case nkind.N_RETURN: return "return"; + case nkind.N_IF: return "if"; + case nkind.N_FOR: return "for"; + case nkind.N_FORRANGE: return "forrange"; + case nkind.N_DEFER: return "defer"; + case nkind.N_BREAK: return "break"; + case nkind.N_CONTINUE: return "continue"; + case nkind.N_SWITCH: return "switch"; + case nkind.N_CASE: return "case"; + + case nkind.N_FILE: return "file"; + case nkind.N_USE: return "use"; + case nkind.N_DEF: return "def"; + case nkind.N_TYPEDECL: return "typedecl"; + case nkind.N_FNDECL: return "fn"; + case nkind.N_PARAM: return "param"; + + case nkind.N_TNAME: return "tname"; + case nkind.N_TPTR: return "tptr"; + case nkind.N_TSLICE: return "tslice"; + case nkind.N_TARRAY: return "tarray"; + case nkind.N_TFN: return "tfn"; + case nkind.N_TSTRUCT: return "tstruct"; + case nkind.N_TFIELD: return "tfield"; + case nkind.N_TCHAN: return "tchan"; + + case nkind.N_ATTR: return "attr"; + case nkind.N_TTUPLE: return "ttuple"; + case nkind.N_TTAGGED: return "ttagged"; + case nkind.N_TUPLE: return "tuple"; + case nkind.N_MATCH: return "match"; + case nkind.N_MCASE: return "mcase"; + case nkind.N_TRYPROP: return "tryprop"; + case nkind.N_TRYUNW: return "tryunw"; + case nkind.N_MLET: return "mlet"; + case nkind.N_MASSIGN: return "massign"; + + case nkind.N_TYPETEST: return "typetest"; + case nkind.N_TYPEASSERT: return "typeassert"; + case nkind.N_VOIDLIT: return "voidlit"; + case nkind.N_TBANG: return "tbang"; + case nkind.N_YIELD: return "yield"; + case nkind.N_TENUM: return "tenum"; + case nkind.N_TENUMMEMBER: return "tenummember"; + case nkind.N_TPARAM: return "tparam"; + case nkind.N_LAST: return "last"; + }; + return "?"; +}; + +fn ind(fd: i32, d: i32) void = { let i: i32 = 0; - for (i < len(kwnames)) { - if (strings.compare(cand, kwnames[i]) == 0) { - return kwkinds[i]; - }; + for (i < d) { + os.write(fd, " ".ptr, 2u64); i += 1; }; - return tkind.TK_NONE; }; -// ---- tokname ---------------------------------------------------------- -// -// Returns the canonical printable spelling for a token kind. Matches -// the C tokname()'s output exactly so wwdump output diffs cleanly. - -export fn tokname(k: tkind) str = { - switch (k) { - case tkind.TK_NONE: return ""; - case tkind.TK_EOF: return "EOF"; - case tkind.TK_ERR: return "ERR"; - case tkind.TK_IDENT: return "IDENT"; - case tkind.TK_INT: return "INT"; - case tkind.TK_FLOAT: return "FLOAT"; - case tkind.TK_RUNE: return "RUNE"; - case tkind.TK_STR: return "STR"; - - case tkind.TK_FN: return "fn"; - case tkind.TK_LET: return "let"; - case tkind.TK_DEF: return "def"; - case tkind.TK_IF: return "if"; - case tkind.TK_ELSE: return "else"; - case tkind.TK_FOR: return "for"; - case tkind.TK_SWITCH: return "switch"; - case tkind.TK_CASE: return "case"; - case tkind.TK_RETURN: return "return"; - case tkind.TK_USE: return "import"; - case tkind.TK_TYPE: return "type"; - case tkind.TK_STRUCT: return "struct"; - case tkind.TK_DEFER: return "defer"; - case tkind.TK_BREAK: return "break"; - case tkind.TK_CONTINUE: return "continue"; - case tkind.TK_EXPORT: return "export"; - case tkind.TK_PROC: return "proc"; - case tkind.TK_CHAN: return "chan"; - case tkind.TK_NIL: return "nil"; - case tkind.TK_TRUE: return "true"; - case tkind.TK_FALSE: return "false"; - case tkind.TK_AS: return "as"; - case tkind.TK_IS: return "is"; - case tkind.TK_VOID: return "void"; - case tkind.TK_YIELD: return "yield"; - case tkind.TK_STATIC: return "static"; - case tkind.TK_MATCH: return "match"; - case tkind.TK_CONST: return "const"; - case tkind.TK_UNDER: return "_"; - case tkind.TK_ENUM: return "enum"; - case tkind.TK_MODULE: return "package"; - case tkind.TK_MODRESET: return "//ww:module-reset"; - case tkind.TK_MODPATH: return "//ww:module"; - - case tkind.TK_LPAREN: return "("; - case tkind.TK_RPAREN: return ")"; - case tkind.TK_LBRACE: return "{"; - case tkind.TK_RBRACE: return "}"; - case tkind.TK_LBRACK: return "["; - case tkind.TK_RBRACK: return "]"; - case tkind.TK_COMMA: return ","; - case tkind.TK_SEMI: return ";"; - case tkind.TK_COLON: return ":"; - case tkind.TK_DOT: return "."; - case tkind.TK_ELLIPSIS: return "..."; - case tkind.TK_DOTDOT: return ".."; - case tkind.TK_AT: return "@"; - case tkind.TK_QUESTION: return "?"; - - case tkind.TK_ASSIGN: return "="; - case tkind.TK_PLUSEQ: return "+="; - case tkind.TK_MINUSEQ: return "-="; - case tkind.TK_STAREQ: return "*="; - case tkind.TK_SLASHEQ: return "/="; - case tkind.TK_PERCENTEQ: return "%="; - case tkind.TK_AMPEQ: return "&="; - case tkind.TK_PIPEEQ: return "|="; - case tkind.TK_CARETEQ: return "^="; - case tkind.TK_LSHIFTEQ: return "<<="; - case tkind.TK_RSHIFTEQ: return ">>="; - - case tkind.TK_PLUS: return "+"; - case tkind.TK_MINUS: return "-"; - case tkind.TK_STAR: return "*"; - case tkind.TK_SLASH: return "/"; - case tkind.TK_PERCENT: return "%"; - case tkind.TK_AMP: return "&"; - case tkind.TK_PIPE: return "|"; - case tkind.TK_CARET: return "^"; - case tkind.TK_TILDE: return "~"; - case tkind.TK_LSHIFT: return "<<"; - case tkind.TK_RSHIFT: return ">>"; - - case tkind.TK_EQ: return "=="; - case tkind.TK_NEQ: return "!="; - case tkind.TK_LT: return "<"; - case tkind.TK_LE: return "<="; - case tkind.TK_GT: return ">"; - case tkind.TK_GE: return ">="; - - case tkind.TK_AND: return "&&"; - case tkind.TK_OR: return "||"; - case tkind.TK_NOT: return "!"; - - case tkind.TK_LARROW: return "<-"; - case tkind.TK_ARROW: return "->"; - case tkind.TK_FATARROW: return "=>"; - - case tkind.TK_LAST: return ""; - }; - return ""; -}; - -// ---- writer for tokprint ---------------------------------------------- -// -// fputq mirrors cmd/wcc/tok.c:fputq — quote the string with C-style -// escapes for \, ", \n, \t, \r and \xNN for other non-printables. - -fn fputcbyte(fd: i32, b: u8) void = { +fn putc1(fd: i32, b: u8) void = { let buf: [1]u8; buf[0] = b; os.write(fd, buf.ptr, 1u64); }; -fn fputsstr(fd: i32, s: str) void = { - os.write(fd, s.ptr, s.len: u64); -}; - -fn hexchar(n: u8) u8 = { - if (n < 10u8) { return n + 48u8; }; // '0'..'9' - return (n - 10u8) + 97u8; // 'a'..'f' -}; - -fn fputhex2(fd: i32, b: u8) void = { - let out: [4]u8; - out[0] = '\\'; - out[1] = 'x'; - out[2] = hexchar(b >> 4u8); - out[3] = hexchar(b & 15u8); - os.write(fd, out.ptr, 4u64); -}; - -fn fputq(fd: i32, p: *u8, n: i32) void = { - fputcbyte(fd, '"'); +fn putq(fd: i32, s: str) void = { + putc1(fd, '"'); let i: i32 = 0; - for (i < n) { - let c: u8 = p[i]; - switch (c) { - case '\\': fputsstr(fd, "\\\\"); - case '"': fputsstr(fd, "\\\""); - case '\n': fputsstr(fd, "\\n"); - case '\t': fputsstr(fd, "\\t"); - case '\r': fputsstr(fd, "\\r"); - case: - if (c < ' ' || c == 127u8) { - fputhex2(fd, c); - } else { - fputcbyte(fd, c); - }; - }; + for (i < s.len) { + let c: u8 = s[i]; + if (c == '"') { + os.write(fd, "\\\"".ptr, 2u64); + } else { if (c == '\\') { + os.write(fd, "\\\\".ptr, 2u64); + } else { if (c == '\n') { + os.write(fd, "\\n".ptr, 2u64); + } else { if (c == '\t') { + os.write(fd, "\\t".ptr, 2u64); + } else { if (c < 32u8) { + let hi: u8 = c >> 4u8; + let lo: u8 = c & 15u8; + let h: u8 = 0u8; + let l: u8 = 0u8; + if (hi < 10u8) { h = hi + 48u8; } else { h = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { l = lo + 48u8; } else { l = (lo - 10u8) + 97u8; }; + let buf: [4]u8; + buf[0] = 92u8; + buf[1] = 120u8; + buf[2] = h; + buf[3] = l; + os.write(fd, buf.ptr, 4u64); + } else { + putc1(fd, c); + };};};};}; i += 1; }; - fputcbyte(fd, '"'); + putc1(fd, 34u8); }; -// tokprint — write one token line to fd. Format must match -// cmd/wcc/tok.c:tokprint() byte-for-byte: that's the diff anchor. -// ":: [ ]\n" -// -// Takes `t` by pointer because w6c can't yet pass a >16-byte struct -// by value; the C version takes Tok by value. -export fn tokprint(fd: i32, t: *tok) void = { - // Chained-dot field reads (`t.x.y`) on str sub-fields aren't yet - // reduced by w6c — `t.x.y` returns the whole str. Lift the str - // fields into locals so we can use the str pseudo-field path. - let tfile: str = t.file; - let ttext: str = t.text; - if (tfile.len > 0) { - fputsstr(fd, tfile); +fn pr(fd: i32, n: *node, d: i32) void = { + if (n == nil) { + ind(fd, d); + os.write(fd, "()\n".ptr, 3u64); + return; + }; + // N_TPARAM wraps an N_TTUPLE.list element so exprtype can return + // shared element-type nodes without corrupting their .next chain. + // Cstage has no AST-level wrapper, so unwrap here to keep the 990 + // -a byte-diff with cstage's astprint. + if (n.kind == nkind.N_TPARAM) { + pr(fd, n.lhs, d); + return; + }; + ind(fd, d); + putc1(fd, '('); + let nm: str = nkname(n.kind); + os.write(fd, nm.ptr, nm.len: u64); + + if (n.kind == nkind.N_INTLIT) { + putc1(fd, 32u8); + let s: str = strconv.u64tos(n.uval, strconv.base.DEC); + os.write(fd, s.ptr, s.len: u64); + } else { if (n.kind == nkind.N_RUNELIT) { + putc1(fd, 32u8); + let s: str = strconv.u64tos(n.uval, strconv.base.DEC); + os.write(fd, s.ptr, s.len: u64); + } else { if ( + n.kind == nkind.N_STRLIT || + n.kind == nkind.N_IDENT || + n.kind == nkind.N_USE || + n.kind == nkind.N_DOT || + n.kind == nkind.N_DEF || + n.kind == nkind.N_TYPEDECL || + n.kind == nkind.N_FNDECL || + n.kind == nkind.N_PARAM || + n.kind == nkind.N_LET || + n.kind == nkind.N_TNAME || + n.kind == nkind.N_TFIELD || + n.kind == nkind.N_TENUMMEMBER || + n.kind == nkind.N_FIELD || + n.kind == nkind.N_ATTR + ) { + // Match C ast.c: print the str field whenever it's non-nil, + // even if its length is zero (e.g. an empty STRLIT prints + // `(str ""`). + let s: str = n.str; + if (s.ptr != nil) { + putc1(fd, 32u8); + putq(fd, s); + }; + } else { if ( + n.kind == nkind.N_BIN || + n.kind == nkind.N_UN || + n.kind == nkind.N_ASSIGN + ) { + putc1(fd, 32u8); + let on: str = tokname(n.op); + os.write(fd, on.ptr, on.len: u64); + };};};}; + + if (n.kind == nkind.N_FNDECL) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + if (n.kind == nkind.N_DEF) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + if (n.kind == nkind.N_TYPEDECL) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + putc1(fd, '\n'); + + if (n.attr != nil) { + ind(fd, d + 1); + os.write(fd, "(@\n".ptr, 3u64); + let m: *node = n.attr; + for (m != nil) { + pr(fd, m, d + 2); + m = m.next; + }; + ind(fd, d + 1); + os.write(fd, ")\n".ptr, 2u64); + }; + if (n.lhs != nil) { pr(fd, n.lhs, d + 1); }; + if (n.rhs != nil) { pr(fd, n.rhs, d + 1); }; + if (n.cond != nil) { pr(fd, n.cond, d + 1); }; + if (n.body != nil) { pr(fd, n.body, d + 1); }; + if (n.els != nil) { pr(fd, n.els, d + 1); }; + if (n.list != nil) { + ind(fd, d + 1); + os.write(fd, "(list\n".ptr, 6u64); + let m: *node = n.list; + for (m != nil) { + pr(fd, m, d + 2); + m = m.next; + }; + ind(fd, d + 1); + os.write(fd, ")\n".ptr, 2u64); + }; + ind(fd, d); + os.write(fd, ")\n".ptr, 2u64); +}; + +export fn astprint(fd: i32, n: *node) void = { + pr(fd, n, 0); +}; + +//ww:module syntax +// lib/ww/syntax/decl.ww — declaration parsing, split out of parse.ww. + +package syntax; + +import os; +import strings; + +// `import encoding.utf8;` — the driver resolves the dotted path to +// a directory; only the leaf (`utf8`) is needed downstream as the +// module bareword for n_use → decl disambiguation, mirroring Hare's +// `use encoding::utf8;` → `utf8::name` (ref/hare/hare/ast/import.ha:7 +// stores `[]str` but identifier-resolution uses the last component). +fn parseuse(p: *parser) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); // past `use` + let n: *node = newnode(nkind.N_USE, pf, pl, pc); + n.nmod = p.curmod; + // M1 #22: accumulate the full dotted import path (n.usepath) for the + // checker's path-keyed module match; n.str stays the leaf alias the + // user writes (`utf8.x`). + let leaf: str; + expectident(p, &leaf); + let path: str = leaf; + for (p.curkind == tkind.TK_DOT) { + advance(p); // past `.` + expectident(p, &leaf); + path = strings.concat(path, ".", leaf); + }; + n.str = leaf; + n.usepath = path; + expecttok(p, tkind.TK_SEMI, "expected ';' after use"); + return n; +}; + +fn parsedef(p: *parser, exported: i32) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); // past `def` + let n: *node = newnode(nkind.N_DEF, pf, pl, pc); + n.nmod = p.curmod; + let id: str; + expectident(p, &id); + n.str = id; + expecttok(p, tkind.TK_COLON, "expected ':' in def"); + n.lhs = parsetype(p); + // A value-LESS `def X: T;` is an interface prototype (an aggregate- + // init `.wwi` def whose DATA lives in the defining package — BUG-2 / + // task #71), mirroring the fn bodyless-prototype arm. rhs stays nil; + // the checker fold pass and cgen emit_defs/emit_lets already guard + // the rhs==nil shape. + if (accepttok(p, tkind.TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; + expecttok(p, tkind.TK_SEMI, "expected ';' after def"); + n.exported = exported; + return n; +}; + +fn parselet(p: *parser, exported: i32) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + // Accept `let` or `const`. Const-bound bindings are marked via + // n.op = tkind.TK_CONST so the checker can reject reassignment. + let is_const: i32 = 0; + if (p.curkind == tkind.TK_CONST) { is_const = 1; }; + advance(p); + let n: *node = newnode(nkind.N_LET, pf, pl, pc); + n.nmod = p.curmod; + let id: str; + expectbindname(p, &id); + n.str = id; + if (accepttok(p, tkind.TK_COLON)) { + n.lhs = parsetype(p); + }; + if (accepttok(p, tkind.TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; + expecttok(p, tkind.TK_SEMI, "expected ';' after let"); + n.exported = exported; + if (is_const != 0) { n.op = tkind.TK_CONST; }; + return n; +}; + +fn parseattrs(p: *parser) *node = { + let head: *node = nil; + let tail: *node = nil; + for (p.curkind == tkind.TK_AT) { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); + let a: *node = newnode(nkind.N_ATTR, pf, pl, pc); + let id: str; + expectident(p, &id); + a.str = id; + // `@name(args...)` for FFI-style attrs; `@name` for marker- + // only attrs like @test (no parens). + if (accepttok(p, tkind.TK_LPAREN)) { + let arghead: *node = nil; + parsearglist(p, tkind.TK_RPAREN, &arghead); + a.list = arghead; + expecttok(p, tkind.TK_RPAREN, "expected ')' after attribute args"); + }; + if (head == nil) { head = a; tail = a; } + else { tail.next = a; tail = a; }; + }; + return head; +}; + +fn parseparams(p: *parser) *node = { + if (p.curkind == tkind.TK_RPAREN) { return nil; }; + let head: *node = nil; + let tail: *node = nil; + for (true) { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + let n: *node = newnode(nkind.N_PARAM, pf, pl, pc); + // Param form: (IDENT|'_') ':' type. Anonymous-type-only params + // (used in fn type expressions) aren't yet wired here. + let id: str; + expectbindname(p, &id); + n.str = id; + expecttok(p, tkind.TK_COLON, "expected ':' in parameter"); + n.lhs = parsetype(p); + // Hare-style variadic: `name: T...`. Marker on n.op so check + // promotes the param's type to []T and call sites gather / + // forward. Mirrors cmd/wcc/parse.c parseparams. + if (accepttok(p, tkind.TK_ELLIPSIS)) { + n.op = tkind.TK_ELLIPSIS; + }; + if (head == nil) { head = n; tail = n; } + else { tail.next = n; tail = n; }; + if (n.op == tkind.TK_ELLIPSIS) { + break; // variadic must be the last param + }; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + if (p.curkind == tkind.TK_RPAREN) { break; }; + }; + return head; +}; + +fn parsefn(p: *parser, exported: i32, attrs: *node) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); // past `fn` + let n: *node = newnode(nkind.N_FNDECL, pf, pl, pc); + n.nmod = p.curmod; + let id: str; + expectident(p, &id); + n.str = id; + expecttok(p, tkind.TK_LPAREN, "expected '(' after fn name"); + n.list = parseparams(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after params"); + if (p.curkind != tkind.TK_ASSIGN) { + if (p.curkind != tkind.TK_SEMI) { + n.lhs = parsetype(p); + }; + }; + if (accepttok(p, tkind.TK_ASSIGN)) { + n.body = parseblock(p); + expecttok(p, tkind.TK_SEMI, "expected ';' after fn body"); } else { - fputsstr(fd, ""); + // Body-less fn: FFI declaration (`fn name(args) ret;`). + expecttok(p, tkind.TK_SEMI, "expected ';' after fn header"); }; - fputcbyte(fd, ':'); - let ls: str = strconv.i64tos(t.line: i64, strconv.base.DEC); - os.write(fd, ls.ptr, ls.len: u64); - fputcbyte(fd, ':'); - let cs: str = strconv.i64tos(t.col: i64, strconv.base.DEC); - os.write(fd, cs.ptr, cs.len: u64); - fputcbyte(fd, ' '); - fputsstr(fd, tokname(t.kind)); - - switch (t.kind) { - case tkind.TK_IDENT, tkind.TK_STR, tkind.TK_ERR: - fputcbyte(fd, ' '); - fputq(fd, ttext.ptr, ttext.len); - case tkind.TK_INT, tkind.TK_RUNE: - fputcbyte(fd, ' '); - let us: str = strconv.u64tos(t.uval, strconv.base.DEC); - os.write(fd, us.ptr, us.len: u64); - }; - // tkind.TK_FLOAT is intentionally not handled here — %g formatting - // won't byte-match across implementations. Diff fixtures must - // be float-free until we implement a stable float formatter. - - fputcbyte(fd, '\n'); + n.exported = exported; + n.attr = attrs; + return n; }; -//ww:module lex -// lib/ww/lex/lex.ww — port of cmd/wcc/lex.c. +fn parsetypedecl(p: *parser, exported: i32) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); // past `type` + let n: *node = newnode(nkind.N_TYPEDECL, pf, pl, pc); + n.nmod = p.curmod; + let id: str; + expectident(p, &id); + n.str = id; + expecttok(p, tkind.TK_ASSIGN, "expected '=' in type decl"); + n.lhs = parsetype(p); + expecttok(p, tkind.TK_SEMI, "expected ';' after type decl"); + n.exported = exported; + return n; +}; + + +//ww:module syntax +// lib/ww/syntax/expr.ww — expression parsing, split out of parse.ww. + +package syntax; + +import os; + +// streqlocal — str-to-str compare. Inlined here to avoid a cross- +// module `use sym;` for one call site. +fn streqlocal(a: str, b: str) bool = { + if (a.len != b.len) { return false; }; + let i: i32 = 0; + for (i < a.len) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +fn parseprimary(p: *parser) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + + if (p.curkind == tkind.TK_INT) { + let n: *node = newnode(nkind.N_INTLIT, pf, pl, pc); + n.uval = p.curuval; + n.str = p.curtext; + // Plumb the typed-int suffix (`42i64`, `3u8`) through to + // the node. Cgen's rhstargetname reads tsuffix to pick the + // matching tagged-union variant; without this, typed-int + // rhs of `h.e = 42i64;` falls through to the "first non-str + // variant" fallback and writes tag 0. Mirror of cmd/wcc/ + // parse.c parseprimary TK_INT. + n.tsuffix = p.curtsuffix; + advance(p); + return n; + }; + if (p.curkind == tkind.TK_FLOAT) { + let n: *node = newnode(nkind.N_FLOATLIT, pf, pl, pc); + n.fval = p.curfval; + // uval carries the IEEE 754 bit pattern — the lexer sets + // both, and cgen consumers prefer the integer view so they + // don't need a float ABI to materialise the constant. + n.uval = p.curuval; + n.str = p.curtext; + n.tsuffix = p.curtsuffix; + advance(p); + return n; + }; + if (p.curkind == tkind.TK_STR) { + let n: *node = newnode(nkind.N_STRLIT, pf, pl, pc); + n.str = p.curtext; + advance(p); + return n; + }; + if (p.curkind == tkind.TK_RUNE) { + let n: *node = newnode(nkind.N_RUNELIT, pf, pl, pc); + n.uval = p.curuval; + advance(p); + return n; + }; + if (p.curkind == tkind.TK_TRUE) { + advance(p); + return newnode(nkind.N_TRUE, pf, pl, pc); + }; + if (p.curkind == tkind.TK_FALSE) { + advance(p); + return newnode(nkind.N_FALSE, pf, pl, pc); + }; + if (p.curkind == tkind.TK_NIL) { + advance(p); + return newnode(nkind.N_NIL, pf, pl, pc); + }; + if (p.curkind == tkind.TK_VOID) { + advance(p); + return newnode(nkind.N_VOIDLIT, pf, pl, pc); + }; + if (p.curkind == tkind.TK_UNDER) { + // Bare `_` — valid only as a discard lvalue. Emit an N_IDENT + // with empty str (newnode zeroes the node, so str.len is + // already 0); the checker rejects it outside lvalue + // positions. + advance(p); + return newnode(nkind.N_IDENT, pf, pl, pc); + }; + if (p.curkind == tkind.TK_LBRACK) { + // Array literal `[a, b, c]` or `[v, w...]` (repeat suffix). + // The repeat marker is an nkind.N_FIELD node with str = "..." + // appended to the element list so cgen can detect it. + advance(p); + let n: *node = newnode(nkind.N_ARRLIT, pf, pl, pc); + let head: *node = nil; + let tail: *node = nil; + for (p.curkind != tkind.TK_RBRACK) { + if (p.curkind == tkind.TK_EOF) { break; }; + let e: *node = parseexpr(p); + if (head == nil) { head = e; tail = e; } + else { tail.next = e; tail = e; }; + if (accepttok(p, tkind.TK_ELLIPSIS)) { + let rep: *node = newnode(nkind.N_FIELD, + p.curfile, p.curline, p.curcol); + rep.str = "..."; + tail.next = rep; + tail = rep; + break; + }; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + expecttok(p, tkind.TK_RBRACK, "expected ']' after array literal"); + n.list = head; + return n; + }; + if (p.curkind == tkind.TK_LPAREN) { + advance(p); + let e: *node = parseexpr(p); + // Tuple literal: (a, b, ...) + if (accepttok(p, tkind.TK_COMMA)) { + let t: *node = newnode(nkind.N_TUPLE, pf, pl, pc); + t.list = e; + let tail: *node = e; + // Parse each element BEFORE the RPAREN-break so `(a,)` + // (a single elem + trailing comma) is a loud parse error; + // a trailing comma is legal only after >=2 elems. Mirror + // cstage cmd/wcc/parse.c:552-558 loop order. + for (true) { + let en: *node = parseexpr(p); + tail.next = en; + tail = en; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + if (p.curkind == tkind.TK_RPAREN) { break; }; + }; + expecttok(p, tkind.TK_RPAREN, "expected ')' in tuple"); + return t; + }; + expecttok(p, tkind.TK_RPAREN, "expected ')'"); + return e; + }; + if (p.curkind == tkind.TK_IDENT) { + let n: *node = newnode(nkind.N_IDENT, pf, pl, pc); + n.str = p.curtext; + advance(p); + // `IDENT {` — struct literal. Disambiguate: only consume as a + // struct lit when we're not in a context where '{' starts a + // block (e.g. `if (cond) {`). The parser is called from + // expressions, never directly from cond contexts that need a + // block; in stmt parsing, the for/if drivers consume their + // own paren/cond, so this is safe. + if (p.curkind == tkind.TK_LBRACE) { + advance(p); + let s: *node = newnode(nkind.N_STRUCTLIT, pf, pl, pc); + s.lhs = n; + let head: *node = nil; + let tail: *node = nil; + for (p.curkind != tkind.TK_RBRACE) { + if (p.curkind == tkind.TK_EOF) { break; }; + // Trailing `...` autofill marker. Stash on s.op so + // cgen can zero-fill the slot before per-field stores. + if (p.curkind == tkind.TK_ELLIPSIS) { + advance(p); + s.op = tkind.TK_ELLIPSIS; + break; + }; + let fpf: str = p.curfile; + let fpl: i32 = p.curline; + let fpc: i32 = p.curcol; + let id: str; + expectident(p, &id); + expecttok(p, tkind.TK_ASSIGN, "expected '=' in struct lit field"); + let v: *node = parseexpr(p); + let f: *node = newnode(nkind.N_FIELD, fpf, fpl, fpc); + f.str = id; + f.lhs = v; + if (head == nil) { head = f; tail = f; } + else { tail.next = f; tail = f; }; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + expecttok(p, tkind.TK_RBRACE, "expected '}' after struct literal"); + s.list = head; + return s; + }; + return n; + }; + if (p.curkind == tkind.TK_MATCH) { + // match (e) { case let v: T => stmt; case T => stmt; case => stmt; }; + advance(p); + expecttok(p, tkind.TK_LPAREN, "expected '(' after match"); + let m: *node = newnode(nkind.N_MATCH, pf, pl, pc); + m.lhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after match scrutinee"); + expecttok(p, tkind.TK_LBRACE, "expected '{' to open match body"); + let head: *node = nil; + let tail: *node = nil; + for (p.curkind == tkind.TK_CASE) { + let cf: str = p.curfile; + let cl: i32 = p.curline; + let cc: i32 = p.curcol; + advance(p); // past `case` + let mc: *node = newnode(nkind.N_MCASE, cf, cl, cc); + if (p.curkind == tkind.TK_LET) { + advance(p); + let id: str; + expectident(p, &id); + mc.str = id; + expecttok(p, tkind.TK_COLON, "expected ':' after match binding"); + mc.lhs = parsetype(p); + } else { if (p.curkind != tkind.TK_FATARROW) { + mc.lhs = parsetype(p); + };}; + expecttok(p, tkind.TK_FATARROW, "expected '=>' in match arm"); + mc.body = parsestmt(p); + if (head == nil) { head = mc; tail = mc; } + else { tail.next = mc; tail = mc; }; + }; + expecttok(p, tkind.TK_RBRACE, "expected '}' after match body"); + m.list = head; + return m; + }; + errmsg(p, "expected expression"); + advance(p); + return newnode(nkind.N_NONE, pf, pl, pc); +}; + +fn parsearglist(p: *parser, closekind: tkind, headout: **node) void = { + *headout = nil; + if (p.curkind == closekind) { return; }; + let head: *node = nil; + let tail: *node = nil; + for (true) { + let e: *node = parseexpr(p); + // Hare-style spread: `expr...` in an arg slot becomes a + // marker the callee/builtin can iterate over. Mirrors + // cmd/wcc/parse.c. The only consumer today is `append`. + if (accepttok(p, tkind.TK_ELLIPSIS)) { + let sp: *node = newnode(nkind.N_SPREAD, e.file, e.line, e.col); + sp.lhs = e; + e = sp; + }; + if (head == nil) { head = e; tail = e; } + else { tail.next = e; tail = e; }; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + if (p.curkind == closekind) { break; }; + }; + *headout = head; +}; + +fn parsepostfix(p: *parser, lhs: *node) *node = { + let cur: *node = lhs; + for (true) { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + if (p.curkind == tkind.TK_LPAREN) { + advance(p); + let n: *node = newnode(nkind.N_CALL, pf, pl, pc); + n.lhs = cur; + // size(T)/align(T): the single arg is a type expression, + // not a regular expression. Special-case at the parser. + let is_typeop: i32 = 0; + if (cur.kind == nkind.N_IDENT) { + if (streqlocal(cur.str, "size")) { is_typeop = 1; }; + if (streqlocal(cur.str, "align")) { is_typeop = 1; }; + }; + if (is_typeop != 0) { + n.list = parsetype(p); + } else { + let arghead: *node = nil; + parsearglist(p, tkind.TK_RPAREN, &arghead); + n.list = arghead; + }; + expecttok(p, tkind.TK_RPAREN, "expected ')' after args"); + cur = n; + continue; + }; + if (p.curkind == tkind.TK_LBRACK) { + advance(p); + // `[ : hi ]` — slice with implicit lo = 0. + if (p.curkind == tkind.TK_COLON) { + advance(p); + let n: *node = newnode(nkind.N_SLICE, pf, pl, pc); + n.lhs = cur; + if (p.curkind != tkind.TK_RBRACK) { + n.cond = parseexpr(p); + }; + expecttok(p, tkind.TK_RBRACK, "expected ']' in slice"); + cur = n; + continue; + }; + // Suppress cast inside `[...]` so ':' parses as slice + // separator rather than the postfix cast operator. + let prev: i32 = p.nocast; + p.nocast = 1; + let e: *node = parseexpr(p); + p.nocast = prev; + if (p.curkind == tkind.TK_COLON) { + advance(p); + let n: *node = newnode(nkind.N_SLICE, pf, pl, pc); + n.lhs = cur; + n.rhs = e; + if (p.curkind != tkind.TK_RBRACK) { + n.cond = parseexpr(p); + }; + expecttok(p, tkind.TK_RBRACK, "expected ']' in slice"); + cur = n; + continue; + }; + let n: *node = newnode(nkind.N_INDEX, pf, pl, pc); + n.lhs = cur; + n.rhs = e; + expecttok(p, tkind.TK_RBRACK, "expected ']' after index"); + cur = n; + continue; + }; + if (p.curkind == tkind.TK_DOT) { + advance(p); + let n: *node = newnode(nkind.N_DOT, pf, pl, pc); + n.lhs = cur; + // Hare-style tuple field access: `t.0`, `t.1`. The + // numeric literal becomes the field name string so the + // cgen tuple-positional path matches `cmd/wcc/parse.c`. + if (p.curkind == tkind.TK_INT) { + n.str = p.curtext; + advance(p); + } else { + let id: str; + expectident(p, &id); + n.str = id; + }; + cur = n; + continue; + }; + if (p.curkind == tkind.TK_COLON) { + if (p.nocast != 0) { + return cur; + }; + advance(p); + let n: *node = newnode(nkind.N_CAST, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; + // Hare-style postfix: + // `e as T` — assert lhs is variant T (abort otherwise) → T + // `e is T` — bool: does lhs currently hold variant T? + // Same precedence level as the `:` cast. + if (p.curkind == tkind.TK_AS) { + advance(p); + let n: *node = newnode(nkind.N_TYPEASSERT, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; + if (p.curkind == tkind.TK_IS) { + advance(p); + let n: *node = newnode(nkind.N_TYPETEST, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; + // `e?` — propagate error variant up the stack. + // `e!` — abort on error variant. + if (p.curkind == tkind.TK_QUESTION) { + advance(p); + let n: *node = newnode(nkind.N_TRYPROP, pf, pl, pc); + n.lhs = cur; + cur = n; + continue; + }; + if (p.curkind == tkind.TK_NOT) { + advance(p); + let n: *node = newnode(nkind.N_TRYUNW, pf, pl, pc); + n.lhs = cur; + cur = n; + continue; + }; + break; + }; + return cur; +}; + +fn parseunary(p: *parser) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + let k: tkind = p.curkind; + if (k == tkind.TK_MINUS) { + advance(p); + let n: *node = newnode(nkind.N_UN, pf, pl, pc); + n.op = tkind.TK_MINUS; n.lhs = parseunary(p); + return n; + }; + if (k == tkind.TK_PLUS) { + advance(p); + let n: *node = newnode(nkind.N_UN, pf, pl, pc); + n.op = tkind.TK_PLUS; n.lhs = parseunary(p); + return n; + }; + if (k == tkind.TK_NOT) { + advance(p); + let n: *node = newnode(nkind.N_UN, pf, pl, pc); + n.op = tkind.TK_NOT; n.lhs = parseunary(p); + return n; + }; + if (k == tkind.TK_TILDE) { + advance(p); + let n: *node = newnode(nkind.N_UN, pf, pl, pc); + n.op = tkind.TK_TILDE; n.lhs = parseunary(p); + return n; + }; + if (k == tkind.TK_STAR) { + advance(p); + let n: *node = newnode(nkind.N_UN, pf, pl, pc); + n.op = tkind.TK_STAR; n.lhs = parseunary(p); + return n; + }; + if (k == tkind.TK_AMP) { + advance(p); + let n: *node = newnode(nkind.N_UN, pf, pl, pc); + n.op = tkind.TK_AMP; n.lhs = parseunary(p); + return n; + }; + return parsepostfix(p, parseprimary(p)); +}; + +fn parsebin(p: *parser, lhs: *node, minp: i32) *node = { + let cur: *node = lhs; + for (true) { + let op: tkind = p.curkind; + let pr: i32 = bprec(op); + if (pr == 0) { return cur; }; + if (pr < minp) { return cur; }; + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); + let rhs: *node = parseunary(p); + for (true) { + let np: i32 = bprec(p.curkind); + if (np <= pr) { break; }; + rhs = parsebin(p, rhs, np); + }; + let n: *node = newnode(nkind.N_BIN, pf, pl, pc); + n.op = op; n.lhs = cur; n.rhs = rhs; + cur = n; + }; + return cur; +}; + +fn parseexpr(p: *parser) *node = { + let e: *node = parsebin(p, parseunary(p), 1); + if (isassignop(p.curkind)) { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + let op: tkind = p.curkind; + advance(p); + let n: *node = newnode(nkind.N_ASSIGN, pf, pl, pc); + n.op = op; + n.lhs = e; + n.rhs = parseexpr(p); // right-associative + return n; + }; + return e; +}; + + +//ww:module syntax +// lib/ww/syntax/lex.ww — port of cmd/wcc/lex.c. // // The DFA, the helpers, and the order of decisions all mirror the C // version exactly. The 990_selfhost test diffs the resulting token @@ -6836,10 +7486,8 @@ export fn tokprint(fd: i32, t: *tok) void = { // in shape, not in observable behaviour. Token kind values stay // numerically identical. -package lex; +package syntax; -// Sibling import (tok) auto-resolves via task #22 dir-enum when -// callers `import lex;` (which dir-enums lib/ww/lex/). import os; import ascii; import strings; @@ -7704,1066 +8352,8 @@ export fn lexnext(l: *lex, out: *tok) void = { out.text = strings.dup(view); }; -//ww:module-reset -// lib/ww/ast.ww — port of cmd/wcc/ast.c (Node defs + printer). -// -// Status: AST printer is fully ported. Constructor `newnode` is here. -// The parser (parse.ww) is currently minimal — see its file header. -// -// Calling-convention shim: same as tok/lex — `node` is too big to pass -// by value (8 *node pointers + 2 strs + a few ints), so callers always -// hand around `*node`. Only `newnode` allocates and returns a *node. - -package ww; - -import os; -import strconv; -import tok; - -// ---- Nkind ------------------------------------------------------------ -// -// Mirror of cmd/wcc/ww.h Nkind. Values must stay numerically equal so -// the AST diff probe in 990_selfhost works. - -// Mirror of the C `Nkind` enum in cmd/wcc/ww.h. Numeric values are -// explicit and must stay in sync — the 990_selfhost test diffs -// astprint against the C side byte-for-byte. Tail-appended entries -// (TYPETEST onward) preserve every prior N_* value. -type nkind = enum i32 { - N_NONE = 0, - - N_INTLIT = 1, - N_FLOATLIT = 2, - N_STRLIT = 3, - N_RUNELIT = 4, - N_TRUE = 5, - N_FALSE = 6, - N_NIL = 7, - N_IDENT = 8, - - N_BIN = 9, - N_UN = 10, - N_CALL = 11, - N_INDEX = 12, - N_DOT = 13, - N_CAST = 14, - N_STRUCTLIT = 15, - N_ARRLIT = 16, - N_FIELD = 17, - N_ASSIGN = 18, - N_ALLOC = 19, - N_FREE = 20, - N_RECV = 21, - N_SLICE = 22, - N_SPREAD = 23, - - N_BLOCK = 24, - N_EXPRSTMT = 25, - N_LET = 26, - N_RETURN = 27, - N_IF = 28, - N_FOR = 29, - N_FORRANGE = 30, - N_DEFER = 31, - N_BREAK = 32, - N_CONTINUE = 33, - N_SWITCH = 34, - N_CASE = 35, - - N_FILE = 36, - N_USE = 37, - N_DEF = 38, - N_TYPEDECL = 39, - N_FNDECL = 40, - N_PARAM = 41, - - N_TNAME = 42, - N_TPTR = 43, - N_TSLICE = 44, - N_TARRAY = 45, - N_TFN = 46, - N_TSTRUCT = 47, - N_TFIELD = 48, - N_TCHAN = 49, - - N_ATTR = 50, - N_TTUPLE = 51, - N_TTAGGED = 52, - N_TUPLE = 53, - N_MATCH = 54, - N_MCASE = 55, - N_TRYPROP = 56, - N_TRYUNW = 57, - N_MLET = 58, - N_MASSIGN = 59, - - N_TYPETEST = 60, - N_TYPEASSERT = 61, - N_VOIDLIT = 62, - N_TBANG = 63, - N_YIELD = 64, - N_TENUM = 65, - N_TENUMMEMBER = 66, - - // N_TPARAM — chain wrapper for N_TTUPLE.list elements. Mirror of - // cstage's Tparam (cmd/wcc/check.c:1437-1451) lifted to the AST so - // `exprtype` can return shared element-type nodes (sym.decl.lhs, - // struct field's .lhs, another N_TTUPLE's .list element) without - // corrupting source ASTs by reusing their .next. .lhs holds the - // element type AST (possibly shared); .next chains within the - // parent N_TTUPLE.list. Cstage keeps Tparam at the Type-layer; ww - // has no separate type layer for tuple chains, so the wrapper sits - // at the AST layer. Other node fields are unused. Never appears - // outside an N_TTUPLE.list; astprint unwraps transparently to keep - // the 990 -a byte-diff against cstage. - N_TPARAM = 67, - - N_LAST = 68, -}; - -// ---- Node ------------------------------------------------------------- - -type node = struct { - kind: nkind, - file: str, - line: i32, - col: i32, - op: tkind, // for nkind.N_BIN / nkind.N_UN / nkind.N_ASSIGN - str: str, - uval: u64, - fval: f64, - lhs: *node, - rhs: *node, - cond: *node, - body: *node, - els: *node, - list: *node, - next: *node, - attr: *node, - exported: i32, // bool — `export` keyword present - type_: *void, // filled in by checker; type.ww treats it as *tinfo - tsuffix: str, // typed numeric literal suffix ("i32", "u64", ...) - nmod: str, // originating module from `// MODULE: foo`; "" if none - usepath: str, // on an N_USE: full dotted IMPORT path vs leaf alias - // in `str` (M1 #22); "" otherwise - imported: i32, // M1 #22: decl reached via `//ww:module ` import - // boundary (vs root/primary); gates root-only bare main -}; - -export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = { - // fval cast-init: 990's wwdump TK_FLOAT diff requires this file - // to tokenise identically through C and ww (lex.ww:382 has the - // same workaround for the cstage %g-formats vs ww-skips divergence). - let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, type_=nil, tsuffix="", nmod="", usepath="", imported=0})!; - return n; -}; - -// ---- printer ---------------------------------------------------------- - -export fn nkname(k: nkind) str = { - switch (k) { - case nkind.N_NONE: return "none"; - case nkind.N_INTLIT: return "int"; - case nkind.N_FLOATLIT: return "float"; - case nkind.N_STRLIT: return "str"; - case nkind.N_RUNELIT: return "rune"; - case nkind.N_TRUE: return "true"; - case nkind.N_FALSE: return "false"; - case nkind.N_NIL: return "nil"; - case nkind.N_IDENT: return "id"; - - case nkind.N_BIN: return "bin"; - case nkind.N_UN: return "un"; - case nkind.N_CALL: return "call"; - case nkind.N_INDEX: return "index"; - case nkind.N_DOT: return "dot"; - case nkind.N_CAST: return "cast"; - case nkind.N_STRUCTLIT: return "structlit"; - case nkind.N_ARRLIT: return "arrlit"; - case nkind.N_FIELD: return "field"; - case nkind.N_ASSIGN: return "assign"; - case nkind.N_ALLOC: return "alloc"; - case nkind.N_FREE: return "free"; - case nkind.N_RECV: return "recv"; - case nkind.N_SLICE: return "slice"; - case nkind.N_SPREAD: return "spread"; - - case nkind.N_BLOCK: return "block"; - case nkind.N_EXPRSTMT: return "exprstmt"; - case nkind.N_LET: return "let"; - case nkind.N_RETURN: return "return"; - case nkind.N_IF: return "if"; - case nkind.N_FOR: return "for"; - case nkind.N_FORRANGE: return "forrange"; - case nkind.N_DEFER: return "defer"; - case nkind.N_BREAK: return "break"; - case nkind.N_CONTINUE: return "continue"; - case nkind.N_SWITCH: return "switch"; - case nkind.N_CASE: return "case"; - - case nkind.N_FILE: return "file"; - case nkind.N_USE: return "use"; - case nkind.N_DEF: return "def"; - case nkind.N_TYPEDECL: return "typedecl"; - case nkind.N_FNDECL: return "fn"; - case nkind.N_PARAM: return "param"; - - case nkind.N_TNAME: return "tname"; - case nkind.N_TPTR: return "tptr"; - case nkind.N_TSLICE: return "tslice"; - case nkind.N_TARRAY: return "tarray"; - case nkind.N_TFN: return "tfn"; - case nkind.N_TSTRUCT: return "tstruct"; - case nkind.N_TFIELD: return "tfield"; - case nkind.N_TCHAN: return "tchan"; - - case nkind.N_ATTR: return "attr"; - case nkind.N_TTUPLE: return "ttuple"; - case nkind.N_TTAGGED: return "ttagged"; - case nkind.N_TUPLE: return "tuple"; - case nkind.N_MATCH: return "match"; - case nkind.N_MCASE: return "mcase"; - case nkind.N_TRYPROP: return "tryprop"; - case nkind.N_TRYUNW: return "tryunw"; - case nkind.N_MLET: return "mlet"; - case nkind.N_MASSIGN: return "massign"; - - case nkind.N_TYPETEST: return "typetest"; - case nkind.N_TYPEASSERT: return "typeassert"; - case nkind.N_VOIDLIT: return "voidlit"; - case nkind.N_TBANG: return "tbang"; - case nkind.N_YIELD: return "yield"; - case nkind.N_TENUM: return "tenum"; - case nkind.N_TENUMMEMBER: return "tenummember"; - case nkind.N_TPARAM: return "tparam"; - case nkind.N_LAST: return "last"; - }; - return "?"; -}; - -fn ind(fd: i32, d: i32) void = { - let i: i32 = 0; - for (i < d) { - os.write(fd, " ".ptr, 2u64); - i += 1; - }; -}; - -fn putc1(fd: i32, b: u8) void = { - let buf: [1]u8; - buf[0] = b; - os.write(fd, buf.ptr, 1u64); -}; - -fn putq(fd: i32, s: str) void = { - putc1(fd, '"'); - let i: i32 = 0; - for (i < s.len) { - let c: u8 = s[i]; - if (c == '"') { - os.write(fd, "\\\"".ptr, 2u64); - } else { if (c == '\\') { - os.write(fd, "\\\\".ptr, 2u64); - } else { if (c == '\n') { - os.write(fd, "\\n".ptr, 2u64); - } else { if (c == '\t') { - os.write(fd, "\\t".ptr, 2u64); - } else { if (c < 32u8) { - let hi: u8 = c >> 4u8; - let lo: u8 = c & 15u8; - let h: u8 = 0u8; - let l: u8 = 0u8; - if (hi < 10u8) { h = hi + 48u8; } else { h = (hi - 10u8) + 97u8; }; - if (lo < 10u8) { l = lo + 48u8; } else { l = (lo - 10u8) + 97u8; }; - let buf: [4]u8; - buf[0] = 92u8; - buf[1] = 120u8; - buf[2] = h; - buf[3] = l; - os.write(fd, buf.ptr, 4u64); - } else { - putc1(fd, c); - };};};};}; - i += 1; - }; - putc1(fd, 34u8); -}; - -fn pr(fd: i32, n: *node, d: i32) void = { - if (n == nil) { - ind(fd, d); - os.write(fd, "()\n".ptr, 3u64); - return; - }; - // N_TPARAM wraps an N_TTUPLE.list element so exprtype can return - // shared element-type nodes without corrupting their .next chain. - // Cstage has no AST-level wrapper, so unwrap here to keep the 990 - // -a byte-diff with cstage's astprint. - if (n.kind == nkind.N_TPARAM) { - pr(fd, n.lhs, d); - return; - }; - ind(fd, d); - putc1(fd, '('); - let nm: str = nkname(n.kind); - os.write(fd, nm.ptr, nm.len: u64); - - if (n.kind == nkind.N_INTLIT) { - putc1(fd, 32u8); - let s: str = strconv.u64tos(n.uval, strconv.base.DEC); - os.write(fd, s.ptr, s.len: u64); - } else { if (n.kind == nkind.N_RUNELIT) { - putc1(fd, 32u8); - let s: str = strconv.u64tos(n.uval, strconv.base.DEC); - os.write(fd, s.ptr, s.len: u64); - } else { if ( - n.kind == nkind.N_STRLIT || - n.kind == nkind.N_IDENT || - n.kind == nkind.N_USE || - n.kind == nkind.N_DOT || - n.kind == nkind.N_DEF || - n.kind == nkind.N_TYPEDECL || - n.kind == nkind.N_FNDECL || - n.kind == nkind.N_PARAM || - n.kind == nkind.N_LET || - n.kind == nkind.N_TNAME || - n.kind == nkind.N_TFIELD || - n.kind == nkind.N_TENUMMEMBER || - n.kind == nkind.N_FIELD || - n.kind == nkind.N_ATTR - ) { - // Match C ast.c: print the str field whenever it's non-nil, - // even if its length is zero (e.g. an empty STRLIT prints - // `(str ""`). - let s: str = n.str; - if (s.ptr != nil) { - putc1(fd, 32u8); - putq(fd, s); - }; - } else { if ( - n.kind == nkind.N_BIN || - n.kind == nkind.N_UN || - n.kind == nkind.N_ASSIGN - ) { - putc1(fd, 32u8); - let on: str = tokname(n.op); - os.write(fd, on.ptr, on.len: u64); - };};};}; - - if (n.kind == nkind.N_FNDECL) { - if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; - }; - if (n.kind == nkind.N_DEF) { - if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; - }; - if (n.kind == nkind.N_TYPEDECL) { - if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; - }; - putc1(fd, '\n'); - - if (n.attr != nil) { - ind(fd, d + 1); - os.write(fd, "(@\n".ptr, 3u64); - let m: *node = n.attr; - for (m != nil) { - pr(fd, m, d + 2); - m = m.next; - }; - ind(fd, d + 1); - os.write(fd, ")\n".ptr, 2u64); - }; - if (n.lhs != nil) { pr(fd, n.lhs, d + 1); }; - if (n.rhs != nil) { pr(fd, n.rhs, d + 1); }; - if (n.cond != nil) { pr(fd, n.cond, d + 1); }; - if (n.body != nil) { pr(fd, n.body, d + 1); }; - if (n.els != nil) { pr(fd, n.els, d + 1); }; - if (n.list != nil) { - ind(fd, d + 1); - os.write(fd, "(list\n".ptr, 6u64); - let m: *node = n.list; - for (m != nil) { - pr(fd, m, d + 2); - m = m.next; - }; - ind(fd, d + 1); - os.write(fd, ")\n".ptr, 2u64); - }; - ind(fd, d); - os.write(fd, ")\n".ptr, 2u64); -}; - -export fn astprint(fd: i32, n: *node) void = { - pr(fd, n, 0); -}; - -//ww:module parse -// lib/ww/parse/decl.ww — declaration parsing, split out of parse.ww. - -package parse; - -import os; -import strings; -import tok; - -// `import encoding.utf8;` — the driver resolves the dotted path to -// a directory; only the leaf (`utf8`) is needed downstream as the -// module bareword for n_use → decl disambiguation, mirroring Hare's -// `use encoding::utf8;` → `utf8::name` (ref/hare/hare/ast/import.ha:7 -// stores `[]str` but identifier-resolution uses the last component). -fn parseuse(p: *parser) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - advance(p); // past `use` - let n: *node = newnode(nkind.N_USE, pf, pl, pc); - n.nmod = p.curmod; - // M1 #22: accumulate the full dotted import path (n.usepath) for the - // checker's path-keyed module match; n.str stays the leaf alias the - // user writes (`utf8.x`). - let leaf: str; - expectident(p, &leaf); - let path: str = leaf; - for (p.curkind == tkind.TK_DOT) { - advance(p); // past `.` - expectident(p, &leaf); - path = strings.concat(path, ".", leaf); - }; - n.str = leaf; - n.usepath = path; - expecttok(p, tkind.TK_SEMI, "expected ';' after use"); - return n; -}; - -fn parsedef(p: *parser, exported: i32) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - advance(p); // past `def` - let n: *node = newnode(nkind.N_DEF, pf, pl, pc); - n.nmod = p.curmod; - let id: str; - expectident(p, &id); - n.str = id; - expecttok(p, tkind.TK_COLON, "expected ':' in def"); - n.lhs = parsetype(p); - // A value-LESS `def X: T;` is an interface prototype (an aggregate- - // init `.wwi` def whose DATA lives in the defining package — BUG-2 / - // task #71), mirroring the fn bodyless-prototype arm. rhs stays nil; - // the checker fold pass and cgen emit_defs/emit_lets already guard - // the rhs==nil shape. - if (accepttok(p, tkind.TK_ASSIGN)) { - n.rhs = parseexpr(p); - }; - expecttok(p, tkind.TK_SEMI, "expected ';' after def"); - n.exported = exported; - return n; -}; - -fn parselet(p: *parser, exported: i32) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - // Accept `let` or `const`. Const-bound bindings are marked via - // n.op = tkind.TK_CONST so the checker can reject reassignment. - let is_const: i32 = 0; - if (p.curkind == tkind.TK_CONST) { is_const = 1; }; - advance(p); - let n: *node = newnode(nkind.N_LET, pf, pl, pc); - n.nmod = p.curmod; - let id: str; - expectbindname(p, &id); - n.str = id; - if (accepttok(p, tkind.TK_COLON)) { - n.lhs = parsetype(p); - }; - if (accepttok(p, tkind.TK_ASSIGN)) { - n.rhs = parseexpr(p); - }; - expecttok(p, tkind.TK_SEMI, "expected ';' after let"); - n.exported = exported; - if (is_const != 0) { n.op = tkind.TK_CONST; }; - return n; -}; - -fn parseattrs(p: *parser) *node = { - let head: *node = nil; - let tail: *node = nil; - for (p.curkind == tkind.TK_AT) { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - advance(p); - let a: *node = newnode(nkind.N_ATTR, pf, pl, pc); - let id: str; - expectident(p, &id); - a.str = id; - // `@name(args...)` for FFI-style attrs; `@name` for marker- - // only attrs like @test (no parens). - if (accepttok(p, tkind.TK_LPAREN)) { - let arghead: *node = nil; - parsearglist(p, tkind.TK_RPAREN, &arghead); - a.list = arghead; - expecttok(p, tkind.TK_RPAREN, "expected ')' after attribute args"); - }; - if (head == nil) { head = a; tail = a; } - else { tail.next = a; tail = a; }; - }; - return head; -}; - -fn parseparams(p: *parser) *node = { - if (p.curkind == tkind.TK_RPAREN) { return nil; }; - let head: *node = nil; - let tail: *node = nil; - for (true) { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - let n: *node = newnode(nkind.N_PARAM, pf, pl, pc); - // Param form: (IDENT|'_') ':' type. Anonymous-type-only params - // (used in fn type expressions) aren't yet wired here. - let id: str; - expectbindname(p, &id); - n.str = id; - expecttok(p, tkind.TK_COLON, "expected ':' in parameter"); - n.lhs = parsetype(p); - // Hare-style variadic: `name: T...`. Marker on n.op so check - // promotes the param's type to []T and call sites gather / - // forward. Mirrors cmd/wcc/parse.c parseparams. - if (accepttok(p, tkind.TK_ELLIPSIS)) { - n.op = tkind.TK_ELLIPSIS; - }; - if (head == nil) { head = n; tail = n; } - else { tail.next = n; tail = n; }; - if (n.op == tkind.TK_ELLIPSIS) { - break; // variadic must be the last param - }; - if (!accepttok(p, tkind.TK_COMMA)) { break; }; - if (p.curkind == tkind.TK_RPAREN) { break; }; - }; - return head; -}; - -fn parsefn(p: *parser, exported: i32, attrs: *node) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - advance(p); // past `fn` - let n: *node = newnode(nkind.N_FNDECL, pf, pl, pc); - n.nmod = p.curmod; - let id: str; - expectident(p, &id); - n.str = id; - expecttok(p, tkind.TK_LPAREN, "expected '(' after fn name"); - n.list = parseparams(p); - expecttok(p, tkind.TK_RPAREN, "expected ')' after params"); - if (p.curkind != tkind.TK_ASSIGN) { - if (p.curkind != tkind.TK_SEMI) { - n.lhs = parsetype(p); - }; - }; - if (accepttok(p, tkind.TK_ASSIGN)) { - n.body = parseblock(p); - expecttok(p, tkind.TK_SEMI, "expected ';' after fn body"); - } else { - // Body-less fn: FFI declaration (`fn name(args) ret;`). - expecttok(p, tkind.TK_SEMI, "expected ';' after fn header"); - }; - n.exported = exported; - n.attr = attrs; - return n; -}; - -fn parsetypedecl(p: *parser, exported: i32) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - advance(p); // past `type` - let n: *node = newnode(nkind.N_TYPEDECL, pf, pl, pc); - n.nmod = p.curmod; - let id: str; - expectident(p, &id); - n.str = id; - expecttok(p, tkind.TK_ASSIGN, "expected '=' in type decl"); - n.lhs = parsetype(p); - expecttok(p, tkind.TK_SEMI, "expected ';' after type decl"); - n.exported = exported; - return n; -}; - - -//ww:module parse -// lib/ww/parse/expr.ww — expression parsing, split out of parse.ww. - -package parse; - -import os; -import tok; - -// streqlocal — str-to-str compare. Inlined here to avoid a cross- -// module `use sym;` for one call site. -fn streqlocal(a: str, b: str) bool = { - if (a.len != b.len) { return false; }; - let i: i32 = 0; - for (i < a.len) { - if (a[i] != b[i]) { return false; }; - i += 1; - }; - return true; -}; - -fn parseprimary(p: *parser) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - - if (p.curkind == tkind.TK_INT) { - let n: *node = newnode(nkind.N_INTLIT, pf, pl, pc); - n.uval = p.curuval; - n.str = p.curtext; - // Plumb the typed-int suffix (`42i64`, `3u8`) through to - // the node. Cgen's rhstargetname reads tsuffix to pick the - // matching tagged-union variant; without this, typed-int - // rhs of `h.e = 42i64;` falls through to the "first non-str - // variant" fallback and writes tag 0. Mirror of cmd/wcc/ - // parse.c parseprimary TK_INT. - n.tsuffix = p.curtsuffix; - advance(p); - return n; - }; - if (p.curkind == tkind.TK_FLOAT) { - let n: *node = newnode(nkind.N_FLOATLIT, pf, pl, pc); - n.fval = p.curfval; - // uval carries the IEEE 754 bit pattern — the lexer sets - // both, and cgen consumers prefer the integer view so they - // don't need a float ABI to materialise the constant. - n.uval = p.curuval; - n.str = p.curtext; - n.tsuffix = p.curtsuffix; - advance(p); - return n; - }; - if (p.curkind == tkind.TK_STR) { - let n: *node = newnode(nkind.N_STRLIT, pf, pl, pc); - n.str = p.curtext; - advance(p); - return n; - }; - if (p.curkind == tkind.TK_RUNE) { - let n: *node = newnode(nkind.N_RUNELIT, pf, pl, pc); - n.uval = p.curuval; - advance(p); - return n; - }; - if (p.curkind == tkind.TK_TRUE) { - advance(p); - return newnode(nkind.N_TRUE, pf, pl, pc); - }; - if (p.curkind == tkind.TK_FALSE) { - advance(p); - return newnode(nkind.N_FALSE, pf, pl, pc); - }; - if (p.curkind == tkind.TK_NIL) { - advance(p); - return newnode(nkind.N_NIL, pf, pl, pc); - }; - if (p.curkind == tkind.TK_VOID) { - advance(p); - return newnode(nkind.N_VOIDLIT, pf, pl, pc); - }; - if (p.curkind == tkind.TK_UNDER) { - // Bare `_` — valid only as a discard lvalue. Emit an N_IDENT - // with empty str (newnode zeroes the node, so str.len is - // already 0); the checker rejects it outside lvalue - // positions. - advance(p); - return newnode(nkind.N_IDENT, pf, pl, pc); - }; - if (p.curkind == tkind.TK_LBRACK) { - // Array literal `[a, b, c]` or `[v, w...]` (repeat suffix). - // The repeat marker is an nkind.N_FIELD node with str = "..." - // appended to the element list so cgen can detect it. - advance(p); - let n: *node = newnode(nkind.N_ARRLIT, pf, pl, pc); - let head: *node = nil; - let tail: *node = nil; - for (p.curkind != tkind.TK_RBRACK) { - if (p.curkind == tkind.TK_EOF) { break; }; - let e: *node = parseexpr(p); - if (head == nil) { head = e; tail = e; } - else { tail.next = e; tail = e; }; - if (accepttok(p, tkind.TK_ELLIPSIS)) { - let rep: *node = newnode(nkind.N_FIELD, - p.curfile, p.curline, p.curcol); - rep.str = "..."; - tail.next = rep; - tail = rep; - break; - }; - if (!accepttok(p, tkind.TK_COMMA)) { break; }; - }; - expecttok(p, tkind.TK_RBRACK, "expected ']' after array literal"); - n.list = head; - return n; - }; - if (p.curkind == tkind.TK_LPAREN) { - advance(p); - let e: *node = parseexpr(p); - // Tuple literal: (a, b, ...) - if (accepttok(p, tkind.TK_COMMA)) { - let t: *node = newnode(nkind.N_TUPLE, pf, pl, pc); - t.list = e; - let tail: *node = e; - // Parse each element BEFORE the RPAREN-break so `(a,)` - // (a single elem + trailing comma) is a loud parse error; - // a trailing comma is legal only after >=2 elems. Mirror - // cstage cmd/wcc/parse.c:552-558 loop order. - for (true) { - let en: *node = parseexpr(p); - tail.next = en; - tail = en; - if (!accepttok(p, tkind.TK_COMMA)) { break; }; - if (p.curkind == tkind.TK_RPAREN) { break; }; - }; - expecttok(p, tkind.TK_RPAREN, "expected ')' in tuple"); - return t; - }; - expecttok(p, tkind.TK_RPAREN, "expected ')'"); - return e; - }; - if (p.curkind == tkind.TK_IDENT) { - let n: *node = newnode(nkind.N_IDENT, pf, pl, pc); - n.str = p.curtext; - advance(p); - // `IDENT {` — struct literal. Disambiguate: only consume as a - // struct lit when we're not in a context where '{' starts a - // block (e.g. `if (cond) {`). The parser is called from - // expressions, never directly from cond contexts that need a - // block; in stmt parsing, the for/if drivers consume their - // own paren/cond, so this is safe. - if (p.curkind == tkind.TK_LBRACE) { - advance(p); - let s: *node = newnode(nkind.N_STRUCTLIT, pf, pl, pc); - s.lhs = n; - let head: *node = nil; - let tail: *node = nil; - for (p.curkind != tkind.TK_RBRACE) { - if (p.curkind == tkind.TK_EOF) { break; }; - // Trailing `...` autofill marker. Stash on s.op so - // cgen can zero-fill the slot before per-field stores. - if (p.curkind == tkind.TK_ELLIPSIS) { - advance(p); - s.op = tkind.TK_ELLIPSIS; - break; - }; - let fpf: str = p.curfile; - let fpl: i32 = p.curline; - let fpc: i32 = p.curcol; - let id: str; - expectident(p, &id); - expecttok(p, tkind.TK_ASSIGN, "expected '=' in struct lit field"); - let v: *node = parseexpr(p); - let f: *node = newnode(nkind.N_FIELD, fpf, fpl, fpc); - f.str = id; - f.lhs = v; - if (head == nil) { head = f; tail = f; } - else { tail.next = f; tail = f; }; - if (!accepttok(p, tkind.TK_COMMA)) { break; }; - }; - expecttok(p, tkind.TK_RBRACE, "expected '}' after struct literal"); - s.list = head; - return s; - }; - return n; - }; - if (p.curkind == tkind.TK_MATCH) { - // match (e) { case let v: T => stmt; case T => stmt; case => stmt; }; - advance(p); - expecttok(p, tkind.TK_LPAREN, "expected '(' after match"); - let m: *node = newnode(nkind.N_MATCH, pf, pl, pc); - m.lhs = parseexpr(p); - expecttok(p, tkind.TK_RPAREN, "expected ')' after match scrutinee"); - expecttok(p, tkind.TK_LBRACE, "expected '{' to open match body"); - let head: *node = nil; - let tail: *node = nil; - for (p.curkind == tkind.TK_CASE) { - let cf: str = p.curfile; - let cl: i32 = p.curline; - let cc: i32 = p.curcol; - advance(p); // past `case` - let mc: *node = newnode(nkind.N_MCASE, cf, cl, cc); - if (p.curkind == tkind.TK_LET) { - advance(p); - let id: str; - expectident(p, &id); - mc.str = id; - expecttok(p, tkind.TK_COLON, "expected ':' after match binding"); - mc.lhs = parsetype(p); - } else { if (p.curkind != tkind.TK_FATARROW) { - mc.lhs = parsetype(p); - };}; - expecttok(p, tkind.TK_FATARROW, "expected '=>' in match arm"); - mc.body = parsestmt(p); - if (head == nil) { head = mc; tail = mc; } - else { tail.next = mc; tail = mc; }; - }; - expecttok(p, tkind.TK_RBRACE, "expected '}' after match body"); - m.list = head; - return m; - }; - errmsg(p, "expected expression"); - advance(p); - return newnode(nkind.N_NONE, pf, pl, pc); -}; - -fn parsearglist(p: *parser, closekind: tkind, headout: **node) void = { - *headout = nil; - if (p.curkind == closekind) { return; }; - let head: *node = nil; - let tail: *node = nil; - for (true) { - let e: *node = parseexpr(p); - // Hare-style spread: `expr...` in an arg slot becomes a - // marker the callee/builtin can iterate over. Mirrors - // cmd/wcc/parse.c. The only consumer today is `append`. - if (accepttok(p, tkind.TK_ELLIPSIS)) { - let sp: *node = newnode(nkind.N_SPREAD, e.file, e.line, e.col); - sp.lhs = e; - e = sp; - }; - if (head == nil) { head = e; tail = e; } - else { tail.next = e; tail = e; }; - if (!accepttok(p, tkind.TK_COMMA)) { break; }; - if (p.curkind == closekind) { break; }; - }; - *headout = head; -}; - -fn parsepostfix(p: *parser, lhs: *node) *node = { - let cur: *node = lhs; - for (true) { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - if (p.curkind == tkind.TK_LPAREN) { - advance(p); - let n: *node = newnode(nkind.N_CALL, pf, pl, pc); - n.lhs = cur; - // size(T)/align(T): the single arg is a type expression, - // not a regular expression. Special-case at the parser. - let is_typeop: i32 = 0; - if (cur.kind == nkind.N_IDENT) { - if (streqlocal(cur.str, "size")) { is_typeop = 1; }; - if (streqlocal(cur.str, "align")) { is_typeop = 1; }; - }; - if (is_typeop != 0) { - n.list = parsetype(p); - } else { - let arghead: *node = nil; - parsearglist(p, tkind.TK_RPAREN, &arghead); - n.list = arghead; - }; - expecttok(p, tkind.TK_RPAREN, "expected ')' after args"); - cur = n; - continue; - }; - if (p.curkind == tkind.TK_LBRACK) { - advance(p); - // `[ : hi ]` — slice with implicit lo = 0. - if (p.curkind == tkind.TK_COLON) { - advance(p); - let n: *node = newnode(nkind.N_SLICE, pf, pl, pc); - n.lhs = cur; - if (p.curkind != tkind.TK_RBRACK) { - n.cond = parseexpr(p); - }; - expecttok(p, tkind.TK_RBRACK, "expected ']' in slice"); - cur = n; - continue; - }; - // Suppress cast inside `[...]` so ':' parses as slice - // separator rather than the postfix cast operator. - let prev: i32 = p.nocast; - p.nocast = 1; - let e: *node = parseexpr(p); - p.nocast = prev; - if (p.curkind == tkind.TK_COLON) { - advance(p); - let n: *node = newnode(nkind.N_SLICE, pf, pl, pc); - n.lhs = cur; - n.rhs = e; - if (p.curkind != tkind.TK_RBRACK) { - n.cond = parseexpr(p); - }; - expecttok(p, tkind.TK_RBRACK, "expected ']' in slice"); - cur = n; - continue; - }; - let n: *node = newnode(nkind.N_INDEX, pf, pl, pc); - n.lhs = cur; - n.rhs = e; - expecttok(p, tkind.TK_RBRACK, "expected ']' after index"); - cur = n; - continue; - }; - if (p.curkind == tkind.TK_DOT) { - advance(p); - let n: *node = newnode(nkind.N_DOT, pf, pl, pc); - n.lhs = cur; - // Hare-style tuple field access: `t.0`, `t.1`. The - // numeric literal becomes the field name string so the - // cgen tuple-positional path matches `cmd/wcc/parse.c`. - if (p.curkind == tkind.TK_INT) { - n.str = p.curtext; - advance(p); - } else { - let id: str; - expectident(p, &id); - n.str = id; - }; - cur = n; - continue; - }; - if (p.curkind == tkind.TK_COLON) { - if (p.nocast != 0) { - return cur; - }; - advance(p); - let n: *node = newnode(nkind.N_CAST, pf, pl, pc); - n.lhs = cur; - n.rhs = parsetype(p); - cur = n; - continue; - }; - // Hare-style postfix: - // `e as T` — assert lhs is variant T (abort otherwise) → T - // `e is T` — bool: does lhs currently hold variant T? - // Same precedence level as the `:` cast. - if (p.curkind == tkind.TK_AS) { - advance(p); - let n: *node = newnode(nkind.N_TYPEASSERT, pf, pl, pc); - n.lhs = cur; - n.rhs = parsetype(p); - cur = n; - continue; - }; - if (p.curkind == tkind.TK_IS) { - advance(p); - let n: *node = newnode(nkind.N_TYPETEST, pf, pl, pc); - n.lhs = cur; - n.rhs = parsetype(p); - cur = n; - continue; - }; - // `e?` — propagate error variant up the stack. - // `e!` — abort on error variant. - if (p.curkind == tkind.TK_QUESTION) { - advance(p); - let n: *node = newnode(nkind.N_TRYPROP, pf, pl, pc); - n.lhs = cur; - cur = n; - continue; - }; - if (p.curkind == tkind.TK_NOT) { - advance(p); - let n: *node = newnode(nkind.N_TRYUNW, pf, pl, pc); - n.lhs = cur; - cur = n; - continue; - }; - break; - }; - return cur; -}; - -fn parseunary(p: *parser) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - let k: tkind = p.curkind; - if (k == tkind.TK_MINUS) { - advance(p); - let n: *node = newnode(nkind.N_UN, pf, pl, pc); - n.op = tkind.TK_MINUS; n.lhs = parseunary(p); - return n; - }; - if (k == tkind.TK_PLUS) { - advance(p); - let n: *node = newnode(nkind.N_UN, pf, pl, pc); - n.op = tkind.TK_PLUS; n.lhs = parseunary(p); - return n; - }; - if (k == tkind.TK_NOT) { - advance(p); - let n: *node = newnode(nkind.N_UN, pf, pl, pc); - n.op = tkind.TK_NOT; n.lhs = parseunary(p); - return n; - }; - if (k == tkind.TK_TILDE) { - advance(p); - let n: *node = newnode(nkind.N_UN, pf, pl, pc); - n.op = tkind.TK_TILDE; n.lhs = parseunary(p); - return n; - }; - if (k == tkind.TK_STAR) { - advance(p); - let n: *node = newnode(nkind.N_UN, pf, pl, pc); - n.op = tkind.TK_STAR; n.lhs = parseunary(p); - return n; - }; - if (k == tkind.TK_AMP) { - advance(p); - let n: *node = newnode(nkind.N_UN, pf, pl, pc); - n.op = tkind.TK_AMP; n.lhs = parseunary(p); - return n; - }; - return parsepostfix(p, parseprimary(p)); -}; - -fn parsebin(p: *parser, lhs: *node, minp: i32) *node = { - let cur: *node = lhs; - for (true) { - let op: tkind = p.curkind; - let pr: i32 = bprec(op); - if (pr == 0) { return cur; }; - if (pr < minp) { return cur; }; - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - advance(p); - let rhs: *node = parseunary(p); - for (true) { - let np: i32 = bprec(p.curkind); - if (np <= pr) { break; }; - rhs = parsebin(p, rhs, np); - }; - let n: *node = newnode(nkind.N_BIN, pf, pl, pc); - n.op = op; n.lhs = cur; n.rhs = rhs; - cur = n; - }; - return cur; -}; - -fn parseexpr(p: *parser) *node = { - let e: *node = parsebin(p, parseunary(p), 1); - if (isassignop(p.curkind)) { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; - let op: tkind = p.curkind; - advance(p); - let n: *node = newnode(nkind.N_ASSIGN, pf, pl, pc); - n.op = op; - n.lhs = e; - n.rhs = parseexpr(p); // right-associative - return n; - }; - return e; -}; - - -//ww:module parse -// lib/ww/parse/parse.ww — port of cmd/wcc/parse.c (entry + plumbing). +//ww:module syntax +// lib/ww/syntax/parse.ww — port of cmd/wcc/parse.c (entry + plumbing). // // Split into Hare-style submodule: parse.ww (here) holds the parser // struct, lexer plumbing, parsetype, parsefile (entry). Expression, @@ -8775,14 +8365,12 @@ fn parseexpr(p: *parser) *node = { // stores the current token as flat primitive fields rather than a // nested `tok` struct; `refill` copies a freshly lexed token in. -package parse; +package syntax; -// Sibling imports (expr, stmt, decl) auto-resolve via task #22 -// dir-enum when callers `import parse;` (which dir-enums -// lib/ww/parse/). +// Sibling files (expr, stmt, decl) are the same `package syntax`, +// auto-resolved via task #22 dir-enum of lib/ww/syntax/. import os; import strings; -import tok; type parser = struct { l: *lex, @@ -9311,13 +8899,12 @@ export fn parsefile(p: *parser) *node = { return f; }; -//ww:module parse -// lib/ww/parse/stmt.ww — statement parsing, split out of parse.ww. +//ww:module syntax +// lib/ww/syntax/stmt.ww — statement parsing, split out of parse.ww. -package parse; +package syntax; import os; -import tok; fn parseletlocal(p: *parser) *node = { let pf = p.curfile; @@ -9726,8 +9313,748 @@ fn parsestmt(p: *parser) *node = { }; -//ww:module-reset -// lib/ww/typ.ww — port of cmd/wcc/type.c. +//ww:module syntax +// lib/ww/syntax/sym.ww — port of cmd/wcc/sym.c. +// +// Per-scope hashtable, chained to the parent. Lookup walks up. +// Plan 9 / Hare flavoured. Duplicate definitions in the same scope +// return nil; the caller flags the error. + +package syntax; + +// Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind. +type skind = enum i32 { + SK_NONE = 0, + SK_VAR = 1, + SK_PARAM = 2, + SK_DEF = 3, + SK_TYPE = 4, + SK_FN = 5, + SK_USE = 6, + SK_FIELD = 7, +}; + +type sym = struct { + name: str, + skind: skind, + type_: *tinfo, + decl: *node, + exported: i32, + is_const: i32, // const-bound (assignment rejected) + use_alias: i32, // #30: this value/type decl ALSO names an imported + // module (the fnmatch.fnmatch / random.random shape). + // Set when installtop promotes a same-leaf SK_USE in + // place; the N_DOT guards treat such a sym as a module + // for `name.member`. Mirror cstage Sym.use_alias + // (cmd/wcc/check.c:2831-2951 promote + 87/1337 guards). + mod: str, // importing module's bareword for symbols + // from a `use`-imported module; "" for primary + // (root) compilation unit symbols. Used by + // scopelookupinmodule to disambiguate same-leaf- + // name types coming from different imports. + snext: *sym, // iteration order + hashnext: *sym, // hash bucket chain + scope: *scope, +}; + +def NBUCKETS: i32 = 16; + +type scope = struct { + parent: *scope, + first: *sym, + last: *sym, + buckets: **sym, // length = NBUCKETS + nbuckets: i32, +}; + +// FNV-1a 64 — same hash the C side uses, so bucket distribution is +// identical when both walk a scope in declaration order. +fn hashstr(s: str) u64 = { + let h: u64 = 14695981039346656037u64; + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + h = h ^ (c: u64); + h = h * 1099511628211u64; + i += 1; + }; + return h; +}; + +export fn newscope(parent: *scope) *scope = { + let buckets_sl: []*sym = alloc([], NBUCKETS: u64)!; + let s: *scope = alloc(scope{parent=parent, first=nil, last=nil, buckets=buckets_sl.ptr, nbuckets=NBUCKETS})!; + return s; +}; + +export fn streq(a: str, b: str) bool = { + if (a.len != b.len) { return false; }; + let i: i32 = 0; + for (i < a.len) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +export fn scopelookuplocal(s: *scope, name: str) *sym = { + if (s == nil) { return nil; }; + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + let bn: str = b.name; + if (streq(bn, name)) { return b; }; + b = b.hashnext; + }; + return nil; +}; + +export fn scopelookup(s: *scope, name: str) *sym = { + for (s != nil) { + let r: *sym = scopelookuplocal(s, name); + if (r != nil) { return r; }; + s = s.parent; + }; + return nil; +}; + +// scopelookuptype — find an SK_TYPE entry by name, same-module preferred. +// +// Same FNV bucket + hashnext chain + parent walk as scopelookup, with +// an `skind == SK_TYPE` filter. Used to disambiguate the bare-TNAME +// vs imported-module-bareword collision: when scopelookup returns the +// SK_USE sym for a leaf that ALSO names a type (a same-name `import X;` +// SK_USE shadowing a struct X declared in another module), the resolver +// needs the type entry — the struct's mod may differ from the leaf so +// scopelookupinmodule(c, leaf, leaf) won't find it. +// +// #58/#50: within each scope, Pass-1 prefers an SK_TYPE whose `sym.mod` +// matches `mod`; Pass-2 falls back to the first SK_TYPE regardless of +// mod (chain-first, the prior behavior). scopedefineinmodule PREPENDS, +// so chain-first = last-registered — when two modules export the same +// type leaf the bare walk silently picked the newest-installed one, +// install-order-dependent, while cstage is deterministic on cur_mod. +// Mirrors cstage cmd/wcc/sym.c scope_lookup_type(s, mod, name) (the +// kind-filtered + mod-preferring single walk); sole caller passes +// c.curmod. i32-correct: streq throughout, only `.len > 0` guards (the +// reverted attempt compared str.len as u64 — str.len is i32). +export fn scopelookuptype(s: *scope, mod: str, name: str) *sym = { + let p: *scope = s; + for (p != nil) { + let h: u64 = hashstr(name); + let bi: i32 = (h % (p.nbuckets: u64)): i32; + let b: *sym = p.buckets[bi]; + let fallback: *sym = nil; + for (b != nil) { + if (streq(b.name, name)) { + if (b.skind == skind.SK_TYPE) { + if (mod.len > 0) { + if (b.mod.len > 0) { + if (streq(b.mod, mod)) { + return b; + }; + }; + }; + if (fallback == nil) { fallback = b; }; + }; + }; + b = b.hashnext; + }; + if (fallback != nil) { return fallback; }; + p = p.parent; + }; + return nil; +}; + +// scopelookupuselocal — find a same-leaf SK_USE entry within ONE scope. +// +// Same FNV bucket + hashnext chain as scopelookuplocal, with a +// `skind == SK_USE` filter and NO parent walk. The dot-lhs twin of +// scopelookuptype: when a `use mod;` and a colliding top-level +// `fn mod` / `type mod` of the same leaf coexist (random.random, +// fnmatch.fnmatch), the mod-preferring scopelookupprefer returns the +// SK_FN/SK_TYPE whose mod matches the importing unit's package, masking +// the SK_USE. A dot-lhs `mod.x` must resolve `mod` to the SK_USE for the +// module-qualified arm to fire, so the resolver re-resolves through this +// filter — keyed on the scope where scopelookupprefer LANDED — when it +// lands on a non-USE same-leaf entry. +// +// Single-scope (not a parent walk) so a local binding that shares a leaf +// with a top-level `use` keeps value semantics: scopelookupprefer +// resolves the local in its inner scope, whose bucket holds no SK_USE, +// so this returns nil and the dot stays field access. Only a genuine +// same-scope coexistence (top-level use + top-level type/fn) re-resolves. +// +// #30 (design reversal): this serves the DISTINCT-mod two-sym case only — +// a `fn fnmatch` (mod="fnmatch") coexisting with `import fnmatch`'s SK_USE +// (mod=""), where scopelookupprefer lands on the value and the dot re- +// resolves to the SK_USE here. The SAME-mod collision (a primary-package +// decl whose leaf also names a bundled module — `type sym` vs `import sym`, +// `@test fn ascii` vs the fnmatch->ascii bundle floor) is NO LONGER left to +// two coexisting syms: that ripples into every bare-ref resolver (a missed +// site is a byte-id-consistent-but-wrong cat-A risk the gate can't prove +// away). Instead installtop now PROMOTES the SK_USE in place to the value +// kind with use_alias=1 (selfhost/cmd/wcc/check.ww installtop), mirroring +// cstage's Sym.use_alias promote exactly (cmd/wcc/check.c:2831-2951); the +// N_DOT guards honor `skind == SK_USE || use_alias` directly. ONE +// correctly-kinded sym → all resolvers correct by construction. Cite: +// task #30; project memory module_type_name_collision (cstage 2026-05-13). +export fn scopelookupuselocal(s: *scope, name: str) *sym = { + if (s == nil) { return nil; }; + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + if (streq(b.name, name)) { + if (b.skind == skind.SK_USE) { return b; }; + }; + b = b.hashnext; + }; + return nil; +}; + +// scopelookupinmodule — module-filtered chain walk. +// +// Same FNV bucket + hashnext chain + parent walk as scopelookup, plus +// a `b.mod.len > 0 && streq(b.mod, mod)` filter. When `mod` is empty +// we fall back to unfiltered scopelookup semantics, so callers that +// don't care about disambiguation get the default. +// +// Used by the dot-prefixed type-name lookup in selfhost/cmd/wcc/ +// check.ww to pick the right same-leaf-name type when two imports +// each export it (`bufio.stream` vs `io.stream`). +export fn scopelookupinmodule(s: *scope, mod: str, name: str) *sym = { + if (mod.len == 0) { return scopelookup(s, name); }; + for (s != nil) { + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + if (streq(b.name, name)) { + if (b.mod.len > 0) { + if (streq(b.mod, mod)) { + return b; + }; + }; + }; + b = b.hashnext; + }; + s = s.parent; + }; + return nil; +}; + +// scopelookupprefer — bare-leaf lookup with same-module preference. +// +// Walks the same FNV bucket + hashnext chain + parent walk scopelookup +// uses. Within each scope's bucket: Pass 1 prefers entries whose +// `sym.mod` matches `mod`; Pass 2 falls back to the first match +// regardless of mod (same semantics as scopelookup). We only descend +// to the parent scope when the current scope has no matching entry at +// all — so a local binding in a closer scope still shadows a same-name +// fn from a parent scope, even when the parent entry mod-matches. +// +// When `mod` is empty we just call scopelookup — there's no module +// identity to prefer. +// +// Used at bare-leaf lookup sites inside a known current module so that +// a bare `read` inside lib/os resolves to os.read rather than the +// io.read that happens to hash earlier into the flat scope. Mirrors +// cmd/wcc/sym.c scope_lookup_prefer. +export fn scopelookupprefer(s: *scope, mod: str, name: str) *sym = { + if (mod.len == 0) { return scopelookup(s, name); }; + let p: *scope = s; + for (p != nil) { + let h: u64 = hashstr(name); + let bi: i32 = (h % (p.nbuckets: u64)): i32; + let b: *sym = p.buckets[bi]; + let fallback: *sym = nil; + for (b != nil) { + if (streq(b.name, name)) { + if (b.mod.len > 0) { + if (streq(b.mod, mod)) { + return b; + }; + }; + if (fallback == nil) { fallback = b; }; + }; + b = b.hashnext; + }; + if (fallback != nil) { return fallback; }; + p = p.parent; + }; + return nil; +}; + +export fn scopedefine(s: *scope, name: str, k: skind, t: *tinfo, decl: *node) *sym = { + let empty: str; + return scopedefineinmodule(s, name, empty, k, t, decl); +}; + +// scopedefineinmodule — bucket insert with per-mod dedup. +// +// Same insertion as scopedefine, but the duplicate-rejection key is +// (name, mod) rather than name alone. This lets two imports each +// register their own `stream` SK_TYPE in the flat scope, and lets the +// primary register `stream` (mod="") alongside imported `stream`s. +// +// Within a single (name, mod) pair the first registration wins; later +// attempts return nil and the caller can flag the error. +export fn scopedefineinmodule(s: *scope, name: str, mod: str, k: skind, t: *tinfo, decl: *node) *sym = { + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + if (streq(b.name, name)) { + if (b.mod.len == 0) { + if (mod.len == 0) { return nil; }; + } else { + if (mod.len > 0) { + if (streq(b.mod, mod)) { return nil; }; + }; + }; + }; + b = b.hashnext; + }; + let sy: *sym = alloc(sym{name=name, skind=k, type_=t, decl=decl, exported=0, is_const=0, use_alias=0, mod=mod, snext=nil, hashnext=s.buckets[bi], scope=s})!; + s.buckets[bi] = sy; + if (s.first == nil) { s.first = sy; } else { s.last.snext = sy; }; + s.last = sy; + return sy; +}; + +// scopesamekeysym — the entry scopedefineinmodule(name, mod) treats as a +// duplicate (same name, same mod-key), or nil if the key is free. Lets a +// caller that got a nil from scopedefineinmodule learn WHAT it collided +// with (e.g. a pre-seeded builtin vs a genuine user redeclaration). The +// match logic mirrors scopedefineinmodule's reject branch exactly. +export fn scopesamekeysym(s: *scope, name: str, mod: str) *sym = { + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + if (streq(b.name, name)) { + if (b.mod.len == 0) { + if (mod.len == 0) { return b; }; + } else { + if (mod.len > 0) { + if (streq(b.mod, mod)) { return b; }; + }; + }; + }; + b = b.hashnext; + }; + return nil; +}; + +//ww:module syntax +// lib/ww/syntax/tok.ww — port of cmd/wcc/tok.c plus the Tkind / +// Tok / Pos shapes from cmd/wcc/ww.h. +// +// Token kind values must stay numerically equal to the C side: the +// 990_selfhost test diffs ww-side wwdump output against C-side +// wwdump output, byte-for-byte. Reordering this list shifts the +// integers and breaks the diff. +// +// Bottom of file: tokprint, which emits one token per line in a +// format identical to cmd/wcc/tok.c:tokprint(). + +package syntax; + +import os; +import strconv; +import strings; + +// ---- tkind ------------------------------------------------------------ +// Mirror of the C `Tkind` enum in cmd/wcc/ww.h. Numeric values are +// explicit and must stay in sync — the 990_selfhost test diffs wwdump +// output against the C side, byte for byte. + +type tkind = enum i32 { + TK_NONE = 0, + TK_EOF = 1, + TK_ERR = 2, + TK_IDENT = 3, + TK_INT = 4, + TK_FLOAT = 5, + TK_RUNE = 6, + TK_STR = 7, + + TK_FN = 8, + TK_LET = 9, + TK_DEF = 10, + TK_IF = 11, + TK_ELSE = 12, + TK_FOR = 13, + TK_SWITCH = 14, + TK_CASE = 15, + TK_RETURN = 16, + TK_USE = 17, + TK_TYPE = 18, + TK_STRUCT = 19, + TK_DEFER = 20, + TK_BREAK = 21, + TK_CONTINUE = 22, + TK_EXPORT = 23, + TK_PROC = 24, + TK_CHAN = 25, + TK_NIL = 26, + TK_TRUE = 27, + TK_FALSE = 28, + TK_AS = 29, + TK_STATIC = 30, + TK_MATCH = 31, + TK_CONST = 32, + TK_UNDER = 33, + + TK_LPAREN = 34, + TK_RPAREN = 35, + TK_LBRACE = 36, + TK_RBRACE = 37, + TK_LBRACK = 38, + TK_RBRACK = 39, + TK_COMMA = 40, + TK_SEMI = 41, + TK_COLON = 42, + TK_DOT = 43, + TK_ELLIPSIS = 44, + TK_DOTDOT = 45, + TK_AT = 46, + TK_QUESTION = 47, + + TK_ASSIGN = 48, + TK_PLUSEQ = 49, + TK_MINUSEQ = 50, + TK_STAREQ = 51, + TK_SLASHEQ = 52, + TK_PERCENTEQ = 53, + TK_AMPEQ = 54, + TK_PIPEEQ = 55, + TK_CARETEQ = 56, + TK_LSHIFTEQ = 57, + TK_RSHIFTEQ = 58, + + TK_PLUS = 59, + TK_MINUS = 60, + TK_STAR = 61, + TK_SLASH = 62, + TK_PERCENT = 63, + TK_AMP = 64, + TK_PIPE = 65, + TK_CARET = 66, + TK_TILDE = 67, + TK_LSHIFT = 68, + TK_RSHIFT = 69, + + TK_EQ = 70, + TK_NEQ = 71, + TK_LT = 72, + TK_LE = 73, + TK_GT = 74, + TK_GE = 75, + + TK_AND = 76, + TK_OR = 77, + TK_NOT = 78, + + TK_LARROW = 79, + TK_ARROW = 80, + TK_FATARROW = 81, + + // Tail-appended values — keeps every prior TK_* numeric value + // stable for the 990_selfhost byte-diff against the C side. + TK_IS = 82, + TK_VOID = 83, + TK_YIELD = 84, + TK_ENUM = 85, + TK_MODULE = 86, // `module foo;` — directory-as-module decl + TK_MODRESET = 87, // `//ww:module-reset` driver bundle boundary: + // reset curmod to "" before a package-less file + // (#16 option-B; cstage TK_MODRESET twin) + TK_MODPATH = 88, // `//ww:module ` driver import + // boundary; decls mangle on the path, not the + // leaf `package` clause (M1 #22; cstage twin) + TK_LAST = 89, +}; + +// ---- Pos / Tok -------------------------------------------------------- +// +// `pos` is used at error-reporting boundaries; we always pass it via +// *pos so the value never gets struct-copied (w6c can't yet copy a +// 24-byte struct). +// +// `tok` is flat — file/line/col live directly on the token rather than +// nested inside a `pos` field. Same reason: nested struct field +// assignment isn't supported, and flat primitives are. + +type pos = struct { + file: str, + line: i32, + col: i32, +}; + +type tok = struct { + kind: tkind, + file: str, // path of the source the token came from + line: i32, + col: i32, + text: str, // arena-owned token text (tkind.TK_IDENT, tkind.TK_STR, tkind.TK_ERR) + uval: u64, // tkind.TK_INT, tkind.TK_RUNE + fval: f64, // tkind.TK_FLOAT + tsuffix: str, // typed numeric literal suffix or empty +}; + +// ---- keyword lookup --------------------------------------------------- + +// keep alphabetised, so kwlookup is easy to read — mirrors the C twin +// cmd/wcc/tok.c:18-47. Two parallel arrays, not a [N]kwent array-of- +// struct: a str *inside* an aggregate element is the filed #18 follow-up +// (str-in-aggregate). `let`, not `def`: #18's module-level [N]str static +// init is scoped to the DATAW (`let`) directive (A_DATAR needs a DATAW +// holder); `def [N]str` is the same filed follow-up. kwkinds is a plain +// [N]tkind enum byte-array (pre-#18 path). Explicit [30], NOT [_]: +// [_] static-init silently miscompiles to a zero-length array in-tree +// (probe at cc69daf — len() returns 0, no diagnostic); filed bug. +let kwnames: [30]str = [ + "as", "break", "case", "chan", "const", "continue", "def", "defer", + "else", "enum", "export", "false", "fn", "for", "if", "is", + "import", "let", "match", "nil", "package", "proc", "return", + "static", "struct", "switch", "true", "type", "void", "yield", +]; +let kwkinds: [30]tkind = [ + tkind.TK_AS, tkind.TK_BREAK, tkind.TK_CASE, tkind.TK_CHAN, + tkind.TK_CONST, tkind.TK_CONTINUE, tkind.TK_DEF, tkind.TK_DEFER, + tkind.TK_ELSE, tkind.TK_ENUM, tkind.TK_EXPORT, tkind.TK_FALSE, + tkind.TK_FN, tkind.TK_FOR, tkind.TK_IF, tkind.TK_IS, + tkind.TK_USE, tkind.TK_LET, tkind.TK_MATCH, tkind.TK_NIL, + tkind.TK_MODULE, tkind.TK_PROC, tkind.TK_RETURN, tkind.TK_STATIC, + tkind.TK_STRUCT, tkind.TK_SWITCH, tkind.TK_TRUE, tkind.TK_TYPE, + tkind.TK_VOID, tkind.TK_YIELD, +]; + +// kwlookup — returns the matching TK_* keyword kind for a byte run, +// or tkind.TK_NONE if it's an ordinary identifier. Linear scan over the +// table, matching cmd/wcc/tok.c:kwlookup (N=30, no hash). +export fn kwlookup(p: *u8, n: i32) tkind = { + let cand: str; + cand.ptr = p; + cand.len = n; + let i: i32 = 0; + for (i < len(kwnames)) { + if (strings.compare(cand, kwnames[i]) == 0) { + return kwkinds[i]; + }; + i += 1; + }; + return tkind.TK_NONE; +}; + +// ---- tokname ---------------------------------------------------------- +// +// Returns the canonical printable spelling for a token kind. Matches +// the C tokname()'s output exactly so wwdump output diffs cleanly. + +export fn tokname(k: tkind) str = { + switch (k) { + case tkind.TK_NONE: return ""; + case tkind.TK_EOF: return "EOF"; + case tkind.TK_ERR: return "ERR"; + case tkind.TK_IDENT: return "IDENT"; + case tkind.TK_INT: return "INT"; + case tkind.TK_FLOAT: return "FLOAT"; + case tkind.TK_RUNE: return "RUNE"; + case tkind.TK_STR: return "STR"; + + case tkind.TK_FN: return "fn"; + case tkind.TK_LET: return "let"; + case tkind.TK_DEF: return "def"; + case tkind.TK_IF: return "if"; + case tkind.TK_ELSE: return "else"; + case tkind.TK_FOR: return "for"; + case tkind.TK_SWITCH: return "switch"; + case tkind.TK_CASE: return "case"; + case tkind.TK_RETURN: return "return"; + case tkind.TK_USE: return "import"; + case tkind.TK_TYPE: return "type"; + case tkind.TK_STRUCT: return "struct"; + case tkind.TK_DEFER: return "defer"; + case tkind.TK_BREAK: return "break"; + case tkind.TK_CONTINUE: return "continue"; + case tkind.TK_EXPORT: return "export"; + case tkind.TK_PROC: return "proc"; + case tkind.TK_CHAN: return "chan"; + case tkind.TK_NIL: return "nil"; + case tkind.TK_TRUE: return "true"; + case tkind.TK_FALSE: return "false"; + case tkind.TK_AS: return "as"; + case tkind.TK_IS: return "is"; + case tkind.TK_VOID: return "void"; + case tkind.TK_YIELD: return "yield"; + case tkind.TK_STATIC: return "static"; + case tkind.TK_MATCH: return "match"; + case tkind.TK_CONST: return "const"; + case tkind.TK_UNDER: return "_"; + case tkind.TK_ENUM: return "enum"; + case tkind.TK_MODULE: return "package"; + case tkind.TK_MODRESET: return "//ww:module-reset"; + case tkind.TK_MODPATH: return "//ww:module"; + + case tkind.TK_LPAREN: return "("; + case tkind.TK_RPAREN: return ")"; + case tkind.TK_LBRACE: return "{"; + case tkind.TK_RBRACE: return "}"; + case tkind.TK_LBRACK: return "["; + case tkind.TK_RBRACK: return "]"; + case tkind.TK_COMMA: return ","; + case tkind.TK_SEMI: return ";"; + case tkind.TK_COLON: return ":"; + case tkind.TK_DOT: return "."; + case tkind.TK_ELLIPSIS: return "..."; + case tkind.TK_DOTDOT: return ".."; + case tkind.TK_AT: return "@"; + case tkind.TK_QUESTION: return "?"; + + case tkind.TK_ASSIGN: return "="; + case tkind.TK_PLUSEQ: return "+="; + case tkind.TK_MINUSEQ: return "-="; + case tkind.TK_STAREQ: return "*="; + case tkind.TK_SLASHEQ: return "/="; + case tkind.TK_PERCENTEQ: return "%="; + case tkind.TK_AMPEQ: return "&="; + case tkind.TK_PIPEEQ: return "|="; + case tkind.TK_CARETEQ: return "^="; + case tkind.TK_LSHIFTEQ: return "<<="; + case tkind.TK_RSHIFTEQ: return ">>="; + + case tkind.TK_PLUS: return "+"; + case tkind.TK_MINUS: return "-"; + case tkind.TK_STAR: return "*"; + case tkind.TK_SLASH: return "/"; + case tkind.TK_PERCENT: return "%"; + case tkind.TK_AMP: return "&"; + case tkind.TK_PIPE: return "|"; + case tkind.TK_CARET: return "^"; + case tkind.TK_TILDE: return "~"; + case tkind.TK_LSHIFT: return "<<"; + case tkind.TK_RSHIFT: return ">>"; + + case tkind.TK_EQ: return "=="; + case tkind.TK_NEQ: return "!="; + case tkind.TK_LT: return "<"; + case tkind.TK_LE: return "<="; + case tkind.TK_GT: return ">"; + case tkind.TK_GE: return ">="; + + case tkind.TK_AND: return "&&"; + case tkind.TK_OR: return "||"; + case tkind.TK_NOT: return "!"; + + case tkind.TK_LARROW: return "<-"; + case tkind.TK_ARROW: return "->"; + case tkind.TK_FATARROW: return "=>"; + + case tkind.TK_LAST: return ""; + }; + return ""; +}; + +// ---- writer for tokprint ---------------------------------------------- +// +// fputq mirrors cmd/wcc/tok.c:fputq — quote the string with C-style +// escapes for \, ", \n, \t, \r and \xNN for other non-printables. + +fn fputcbyte(fd: i32, b: u8) void = { + let buf: [1]u8; + buf[0] = b; + os.write(fd, buf.ptr, 1u64); +}; + +fn fputsstr(fd: i32, s: str) void = { + os.write(fd, s.ptr, s.len: u64); +}; + +fn hexchar(n: u8) u8 = { + if (n < 10u8) { return n + 48u8; }; // '0'..'9' + return (n - 10u8) + 97u8; // 'a'..'f' +}; + +fn fputhex2(fd: i32, b: u8) void = { + let out: [4]u8; + out[0] = '\\'; + out[1] = 'x'; + out[2] = hexchar(b >> 4u8); + out[3] = hexchar(b & 15u8); + os.write(fd, out.ptr, 4u64); +}; + +fn fputq(fd: i32, p: *u8, n: i32) void = { + fputcbyte(fd, '"'); + let i: i32 = 0; + for (i < n) { + let c: u8 = p[i]; + switch (c) { + case '\\': fputsstr(fd, "\\\\"); + case '"': fputsstr(fd, "\\\""); + case '\n': fputsstr(fd, "\\n"); + case '\t': fputsstr(fd, "\\t"); + case '\r': fputsstr(fd, "\\r"); + case: + if (c < ' ' || c == 127u8) { + fputhex2(fd, c); + } else { + fputcbyte(fd, c); + }; + }; + i += 1; + }; + fputcbyte(fd, '"'); +}; + +// tokprint — write one token line to fd. Format must match +// cmd/wcc/tok.c:tokprint() byte-for-byte: that's the diff anchor. +// ":: [ ]\n" +// +// Takes `t` by pointer because w6c can't yet pass a >16-byte struct +// by value; the C version takes Tok by value. +export fn tokprint(fd: i32, t: *tok) void = { + // Chained-dot field reads (`t.x.y`) on str sub-fields aren't yet + // reduced by w6c — `t.x.y` returns the whole str. Lift the str + // fields into locals so we can use the str pseudo-field path. + let tfile: str = t.file; + let ttext: str = t.text; + if (tfile.len > 0) { + fputsstr(fd, tfile); + } else { + fputsstr(fd, ""); + }; + fputcbyte(fd, ':'); + let ls: str = strconv.i64tos(t.line: i64, strconv.base.DEC); + os.write(fd, ls.ptr, ls.len: u64); + fputcbyte(fd, ':'); + let cs: str = strconv.i64tos(t.col: i64, strconv.base.DEC); + os.write(fd, cs.ptr, cs.len: u64); + fputcbyte(fd, ' '); + fputsstr(fd, tokname(t.kind)); + + switch (t.kind) { + case tkind.TK_IDENT, tkind.TK_STR, tkind.TK_ERR: + fputcbyte(fd, ' '); + fputq(fd, ttext.ptr, ttext.len); + case tkind.TK_INT, tkind.TK_RUNE: + fputcbyte(fd, ' '); + let us: str = strconv.u64tos(t.uval, strconv.base.DEC); + os.write(fd, us.ptr, us.len: u64); + }; + // tkind.TK_FLOAT is intentionally not handled here — %g formatting + // won't byte-match across implementations. Diff fixtures must + // be float-free until we implement a stable float formatter. + + fputcbyte(fd, '\n'); +}; + +//ww:module syntax +// lib/ww/syntax/typ.ww — port of cmd/wcc/type.c. // // Status: full structural port. The C version uses module-globals for // the primitive types (tyvoid, tyi32, …); ww doesn't have writable @@ -9735,7 +10062,7 @@ fn parsestmt(p: *parser) *node = { // the checker passes around explicitly. typesinit fills the tctx // once per program. -package ww; +package syntax; import os; @@ -10343,342 +10670,6 @@ export fn typeeq(a: *tinfo, b: *tinfo) bool = { return true; // primitives match by kind alone }; -//ww:module-reset -// lib/ww/sym.ww — port of cmd/wcc/sym.c. -// -// Per-scope hashtable, chained to the parent. Lookup walks up. -// Plan 9 / Hare flavoured. Duplicate definitions in the same scope -// return nil; the caller flags the error. - -package ww; - -// Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind. -type skind = enum i32 { - SK_NONE = 0, - SK_VAR = 1, - SK_PARAM = 2, - SK_DEF = 3, - SK_TYPE = 4, - SK_FN = 5, - SK_USE = 6, - SK_FIELD = 7, -}; - -type sym = struct { - name: str, - skind: skind, - type_: *tinfo, - decl: *node, - exported: i32, - is_const: i32, // const-bound (assignment rejected) - use_alias: i32, // #30: this value/type decl ALSO names an imported - // module (the fnmatch.fnmatch / random.random shape). - // Set when installtop promotes a same-leaf SK_USE in - // place; the N_DOT guards treat such a sym as a module - // for `name.member`. Mirror cstage Sym.use_alias - // (cmd/wcc/check.c:2831-2951 promote + 87/1337 guards). - mod: str, // importing module's bareword for symbols - // from a `use`-imported module; "" for primary - // (root) compilation unit symbols. Used by - // scopelookupinmodule to disambiguate same-leaf- - // name types coming from different imports. - snext: *sym, // iteration order - hashnext: *sym, // hash bucket chain - scope: *scope, -}; - -def NBUCKETS: i32 = 16; - -type scope = struct { - parent: *scope, - first: *sym, - last: *sym, - buckets: **sym, // length = NBUCKETS - nbuckets: i32, -}; - -// FNV-1a 64 — same hash the C side uses, so bucket distribution is -// identical when both walk a scope in declaration order. -fn hashstr(s: str) u64 = { - let h: u64 = 14695981039346656037u64; - let i: i32 = 0; - for (i < s.len) { - let c: u8 = s[i]; - h = h ^ (c: u64); - h = h * 1099511628211u64; - i += 1; - }; - return h; -}; - -export fn newscope(parent: *scope) *scope = { - let buckets_sl: []*sym = alloc([], NBUCKETS: u64)!; - let s: *scope = alloc(scope{parent=parent, first=nil, last=nil, buckets=buckets_sl.ptr, nbuckets=NBUCKETS})!; - return s; -}; - -export fn streq(a: str, b: str) bool = { - if (a.len != b.len) { return false; }; - let i: i32 = 0; - for (i < a.len) { - if (a[i] != b[i]) { return false; }; - i += 1; - }; - return true; -}; - -export fn scopelookuplocal(s: *scope, name: str) *sym = { - if (s == nil) { return nil; }; - let h: u64 = hashstr(name); - let bi: i32 = (h % (s.nbuckets: u64)): i32; - let b: *sym = s.buckets[bi]; - for (b != nil) { - let bn: str = b.name; - if (streq(bn, name)) { return b; }; - b = b.hashnext; - }; - return nil; -}; - -export fn scopelookup(s: *scope, name: str) *sym = { - for (s != nil) { - let r: *sym = scopelookuplocal(s, name); - if (r != nil) { return r; }; - s = s.parent; - }; - return nil; -}; - -// scopelookuptype — find an SK_TYPE entry by name, same-module preferred. -// -// Same FNV bucket + hashnext chain + parent walk as scopelookup, with -// an `skind == SK_TYPE` filter. Used to disambiguate the bare-TNAME -// vs imported-module-bareword collision: when scopelookup returns the -// SK_USE sym for a leaf that ALSO names a type (e.g. `tok` struct -// declared in lib/ww/lex/tok.ww with `package lex;` while -// `import tok;` registers a same-name SK_USE), the resolver needs -// the type entry — the struct's mod may differ from the leaf (lex/tok -// pair) so scopelookupinmodule(c, leaf, leaf) won't find it. -// -// #58/#50: within each scope, Pass-1 prefers an SK_TYPE whose `sym.mod` -// matches `mod`; Pass-2 falls back to the first SK_TYPE regardless of -// mod (chain-first, the prior behavior). scopedefineinmodule PREPENDS, -// so chain-first = last-registered — when two modules export the same -// type leaf the bare walk silently picked the newest-installed one, -// install-order-dependent, while cstage is deterministic on cur_mod. -// Mirrors cstage cmd/wcc/sym.c scope_lookup_type(s, mod, name) (the -// kind-filtered + mod-preferring single walk); sole caller passes -// c.curmod. i32-correct: streq throughout, only `.len > 0` guards (the -// reverted attempt compared str.len as u64 — str.len is i32). -export fn scopelookuptype(s: *scope, mod: str, name: str) *sym = { - let p: *scope = s; - for (p != nil) { - let h: u64 = hashstr(name); - let bi: i32 = (h % (p.nbuckets: u64)): i32; - let b: *sym = p.buckets[bi]; - let fallback: *sym = nil; - for (b != nil) { - if (streq(b.name, name)) { - if (b.skind == skind.SK_TYPE) { - if (mod.len > 0) { - if (b.mod.len > 0) { - if (streq(b.mod, mod)) { - return b; - }; - }; - }; - if (fallback == nil) { fallback = b; }; - }; - }; - b = b.hashnext; - }; - if (fallback != nil) { return fallback; }; - p = p.parent; - }; - return nil; -}; - -// scopelookupuselocal — find a same-leaf SK_USE entry within ONE scope. -// -// Same FNV bucket + hashnext chain as scopelookuplocal, with a -// `skind == SK_USE` filter and NO parent walk. The dot-lhs twin of -// scopelookuptype: when a `use mod;` and a colliding top-level -// `fn mod` / `type mod` of the same leaf coexist (random.random, -// fnmatch.fnmatch), the mod-preferring scopelookupprefer returns the -// SK_FN/SK_TYPE whose mod matches the importing unit's package, masking -// the SK_USE. A dot-lhs `mod.x` must resolve `mod` to the SK_USE for the -// module-qualified arm to fire, so the resolver re-resolves through this -// filter — keyed on the scope where scopelookupprefer LANDED — when it -// lands on a non-USE same-leaf entry. -// -// Single-scope (not a parent walk) so a local binding that shares a leaf -// with a top-level `use` keeps value semantics: scopelookupprefer -// resolves the local in its inner scope, whose bucket holds no SK_USE, -// so this returns nil and the dot stays field access. Only a genuine -// same-scope coexistence (top-level use + top-level type/fn) re-resolves. -// -// #30 (design reversal): this serves the DISTINCT-mod two-sym case only — -// a `fn fnmatch` (mod="fnmatch") coexisting with `import fnmatch`'s SK_USE -// (mod=""), where scopelookupprefer lands on the value and the dot re- -// resolves to the SK_USE here. The SAME-mod collision (a primary-package -// decl whose leaf also names a bundled module — `type sym` vs `import sym`, -// `@test fn ascii` vs the fnmatch->ascii bundle floor) is NO LONGER left to -// two coexisting syms: that ripples into every bare-ref resolver (a missed -// site is a byte-id-consistent-but-wrong cat-A risk the gate can't prove -// away). Instead installtop now PROMOTES the SK_USE in place to the value -// kind with use_alias=1 (selfhost/cmd/wcc/check.ww installtop), mirroring -// cstage's Sym.use_alias promote exactly (cmd/wcc/check.c:2831-2951); the -// N_DOT guards honor `skind == SK_USE || use_alias` directly. ONE -// correctly-kinded sym → all resolvers correct by construction. Cite: -// task #30; project memory module_type_name_collision (cstage 2026-05-13). -export fn scopelookupuselocal(s: *scope, name: str) *sym = { - if (s == nil) { return nil; }; - let h: u64 = hashstr(name); - let bi: i32 = (h % (s.nbuckets: u64)): i32; - let b: *sym = s.buckets[bi]; - for (b != nil) { - if (streq(b.name, name)) { - if (b.skind == skind.SK_USE) { return b; }; - }; - b = b.hashnext; - }; - return nil; -}; - -// scopelookupinmodule — module-filtered chain walk. -// -// Same FNV bucket + hashnext chain + parent walk as scopelookup, plus -// a `b.mod.len > 0 && streq(b.mod, mod)` filter. When `mod` is empty -// we fall back to unfiltered scopelookup semantics, so callers that -// don't care about disambiguation get the default. -// -// Used by the dot-prefixed type-name lookup in selfhost/cmd/wcc/ -// check.ww to pick the right same-leaf-name type when two imports -// each export it (`bufio.stream` vs `io.stream`). -export fn scopelookupinmodule(s: *scope, mod: str, name: str) *sym = { - if (mod.len == 0) { return scopelookup(s, name); }; - for (s != nil) { - let h: u64 = hashstr(name); - let bi: i32 = (h % (s.nbuckets: u64)): i32; - let b: *sym = s.buckets[bi]; - for (b != nil) { - if (streq(b.name, name)) { - if (b.mod.len > 0) { - if (streq(b.mod, mod)) { - return b; - }; - }; - }; - b = b.hashnext; - }; - s = s.parent; - }; - return nil; -}; - -// scopelookupprefer — bare-leaf lookup with same-module preference. -// -// Walks the same FNV bucket + hashnext chain + parent walk scopelookup -// uses. Within each scope's bucket: Pass 1 prefers entries whose -// `sym.mod` matches `mod`; Pass 2 falls back to the first match -// regardless of mod (same semantics as scopelookup). We only descend -// to the parent scope when the current scope has no matching entry at -// all — so a local binding in a closer scope still shadows a same-name -// fn from a parent scope, even when the parent entry mod-matches. -// -// When `mod` is empty we just call scopelookup — there's no module -// identity to prefer. -// -// Used at bare-leaf lookup sites inside a known current module so that -// a bare `read` inside lib/os resolves to os.read rather than the -// io.read that happens to hash earlier into the flat scope. Mirrors -// cmd/wcc/sym.c scope_lookup_prefer. -export fn scopelookupprefer(s: *scope, mod: str, name: str) *sym = { - if (mod.len == 0) { return scopelookup(s, name); }; - let p: *scope = s; - for (p != nil) { - let h: u64 = hashstr(name); - let bi: i32 = (h % (p.nbuckets: u64)): i32; - let b: *sym = p.buckets[bi]; - let fallback: *sym = nil; - for (b != nil) { - if (streq(b.name, name)) { - if (b.mod.len > 0) { - if (streq(b.mod, mod)) { - return b; - }; - }; - if (fallback == nil) { fallback = b; }; - }; - b = b.hashnext; - }; - if (fallback != nil) { return fallback; }; - p = p.parent; - }; - return nil; -}; - -export fn scopedefine(s: *scope, name: str, k: skind, t: *tinfo, decl: *node) *sym = { - let empty: str; - return scopedefineinmodule(s, name, empty, k, t, decl); -}; - -// scopedefineinmodule — bucket insert with per-mod dedup. -// -// Same insertion as scopedefine, but the duplicate-rejection key is -// (name, mod) rather than name alone. This lets two imports each -// register their own `stream` SK_TYPE in the flat scope, and lets the -// primary register `stream` (mod="") alongside imported `stream`s. -// -// Within a single (name, mod) pair the first registration wins; later -// attempts return nil and the caller can flag the error. -export fn scopedefineinmodule(s: *scope, name: str, mod: str, k: skind, t: *tinfo, decl: *node) *sym = { - let h: u64 = hashstr(name); - let bi: i32 = (h % (s.nbuckets: u64)): i32; - let b: *sym = s.buckets[bi]; - for (b != nil) { - if (streq(b.name, name)) { - if (b.mod.len == 0) { - if (mod.len == 0) { return nil; }; - } else { - if (mod.len > 0) { - if (streq(b.mod, mod)) { return nil; }; - }; - }; - }; - b = b.hashnext; - }; - let sy: *sym = alloc(sym{name=name, skind=k, type_=t, decl=decl, exported=0, is_const=0, use_alias=0, mod=mod, snext=nil, hashnext=s.buckets[bi], scope=s})!; - s.buckets[bi] = sy; - if (s.first == nil) { s.first = sy; } else { s.last.snext = sy; }; - s.last = sy; - return sy; -}; - -// scopesamekeysym — the entry scopedefineinmodule(name, mod) treats as a -// duplicate (same name, same mod-key), or nil if the key is free. Lets a -// caller that got a nil from scopedefineinmodule learn WHAT it collided -// with (e.g. a pre-seeded builtin vs a genuine user redeclaration). The -// match logic mirrors scopedefineinmodule's reject branch exactly. -export fn scopesamekeysym(s: *scope, name: str, mod: str) *sym = { - let h: u64 = hashstr(name); - let bi: i32 = (h % (s.nbuckets: u64)): i32; - let b: *sym = s.buckets[bi]; - for (b != nil) { - if (streq(b.name, name)) { - if (b.mod.len == 0) { - if (mod.len == 0) { return b; }; - } else { - if (mod.len > 0) { - if (streq(b.mod, mod)) { return b; }; - }; - }; - }; - b = b.hashnext; - }; - return nil; -}; - //ww:module-reset // selfhost/cmd/wcc/check.ww — minimal port of cmd/wcc/check.c. // @@ -10702,7 +10693,7 @@ export fn scopesamekeysym(s: *scope, name: str, mod: str) *sym = { package wcc; import os; -import tok; +import syntax; import strconv; type checker = struct { @@ -18185,10 +18176,7 @@ export fn borrowedread(s: *stream, amt: i32) ([]u8 | io.eof) = { package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; // ---- variadic-call helpers (Hare-style `T...` param) ----------------- @@ -23742,10 +23730,7 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; // cgfloatbits — materialise a float constant in X0: MOVQ the IEEE bits @@ -36278,10 +36263,7 @@ fn cgassign(c: *cgen, n: *node) void = { package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; // ---- statement cgen -------------------------------------------------- @@ -40630,10 +40612,7 @@ fn cgcontinue(c: *cgen, n: *node) void = { package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; @@ -41311,10 +41290,7 @@ export fn cgfile(c: *cgen, file: *node) void = { package wcc; import os; -import ast; -import tok; -import typ; -import sym; +import syntax; import strconv; import strings; import io; @@ -45374,12 +45350,7 @@ export fn fargregname(i: i32) str = { package main; import os; -import tok; -import lex; -import ast; -import parse; -import typ; -import sym; +import syntax; import check; import cgen; import strconv; diff --git a/selfhost/cmd/wwdump/main.ww b/selfhost/cmd/wwdump/main.ww index 77b04271..e1050ccc 100644 --- a/selfhost/cmd/wwdump/main.ww +++ b/selfhost/cmd/wwdump/main.ww @@ -12,12 +12,7 @@ package main; import os; -import tok; -import lex; -import ast; -import parse; -import typ; -import sym; +import syntax; import check; import cgen; import strconv; diff --git a/selfhost/test/sym_link.ww b/selfhost/test/sym_link.ww index c1507388..d7509e59 100644 --- a/selfhost/test/sym_link.ww +++ b/selfhost/test/sym_link.ww @@ -5,9 +5,7 @@ package test; -import typ; -import ast; -import sym; +import syntax; export fn main() i32 = { let s: *scope = newscope(nil); diff --git a/test/wcc/904_tok_run.c b/test/wcc/904_tok_run.c index b636045c..4b5f1181 100644 --- a/test/wcc/904_tok_run.c +++ b/test/wcc/904_tok_run.c @@ -1,5 +1,5 @@ /* - * 904_tok_run — execute the lib/ww/lex tokname/kwlookup @test fixture + * 904_tok_run — execute the lib/ww/syntax tokname/kwlookup @test fixture * under the C-side `ww run` driver and assert exit 0. * * Same thin-wrapper shape as 904_ascii_run / 967_bytes_run: toktest.ww @@ -38,10 +38,11 @@ main(void) char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return 1; - const char *src = "lib/ww/lex/toktest.ww"; - char path[1024], cmd[2048]; + const char *src = "lib/ww/syntax/toktest.ww"; + char path[1024], inc[1024], cmd[3072]; snprintf(path, sizeof path, "%s/%s", cwd, src); - snprintf(cmd, sizeof cmd, "%s/ww test %s", bin, path); + snprintf(inc, sizeof inc, "%s/lib/ww", cwd); + snprintf(cmd, sizeof cmd, "%s/ww test -I %s %s", bin, inc, path); int rc = runwait(cmd); if (rc != 0) { fprintf(stderr, "tok_run FAIL: %s exited %d\n", src, rc); diff --git a/test/wcc/905_nkname_run.c b/test/wcc/905_nkname_run.c index 05c25d0a..2b3001d1 100644 --- a/test/wcc/905_nkname_run.c +++ b/test/wcc/905_nkname_run.c @@ -9,9 +9,9 @@ * which the 990_selfhost wwdump diff only covers for kinds that appear * in its corpus. * - * Unlike 904_tok_run, the fixture imports `ast` (package ww), whose - * printer references tok's tkind/tokname, so the run needs - * `-I lib/ww/lex` to resolve that transitive import. + * Unlike 904_tok_run, the fixture imports the `syntax` package, whose + * printer references the token tkind/tokname, so the run needs + * `-I lib/ww` to resolve the `import syntax` directory package. */ #include #include @@ -42,10 +42,10 @@ main(void) char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return 1; - const char *src = "lib/ww/asttest.ww"; + const char *src = "lib/ww/syntax/asttest.ww"; char path[1024], inc[1024], cmd[3072]; snprintf(path, sizeof path, "%s/%s", cwd, src); - snprintf(inc, sizeof inc, "%s/lib/ww/lex", cwd); + snprintf(inc, sizeof inc, "%s/lib/ww", cwd); snprintf(cmd, sizeof cmd, "%s/ww test -I %s %s", bin, inc, path); int rc = runwait(cmd); if (rc != 0) { diff --git a/test/wcc/989_lib_byteid.c b/test/wcc/989_lib_byteid.c index 7b74604e..8e75f998 100644 --- a/test/wcc/989_lib_byteid.c +++ b/test/wcc/989_lib_byteid.c @@ -150,10 +150,10 @@ static const struct ent ents[] = { { .fixture = "lib/os/stattest.ww", .mode = M_DIVERGE, .cite = "#59.9" }, /* #59.10 stoftest graduated to M_ID above (#62 fix) */ - { .fixture = "lib/ww/lex/toktest.ww", + { .fixture = "lib/ww/syntax/toktest.ww", .inc = "lib/ww", .mode = M_ID, .cite = "#59.11 graduated by #146 str==" }, - /* asttest resolves `import tok` via -I lib/ww/lex, cf 905 */ - { .fixture = "lib/ww/asttest.ww", .inc = "lib/ww/lex", + /* toktest/asttest resolve `import syntax` via -I lib/ww, cf 905 */ + { .fixture = "lib/ww/syntax/asttest.ww", .inc = "lib/ww", .mode = M_ID, .cite = "#59.12 graduated by #146 str==" }, /* -------- wwstage front-end gaps (task #59) -------------- */ { .fixture = "lib/crypto/sha256/sha256_test.ww", @@ -373,7 +373,7 @@ out: * and the corpus rots silently as lib/ grows. */ static const char *covered[] = { /* selfhost-embedded: byte-id'd by the 990-997 gates */ - "lib/io", "lib/math", "lib/rt", "lib/types", "lib/ww/parse", + "lib/io", "lib/math", "lib/rt", "lib/types", /* module body dragged into the lib/strconv/test fixtures */ "lib/strconv", /* #17: the @test runner is AUTO-BUNDLED into every -T combined, so diff --git a/test/wcc/990_selfhost.c b/test/wcc/990_selfhost.c index 3708ee53..65f4333a 100644 --- a/test/wcc/990_selfhost.c +++ b/test/wcc/990_selfhost.c @@ -170,10 +170,10 @@ probe_dump_diff(const char *bin) { const char *tok_inputs[] = { "selfhost/cmd/wcc/err.ww", - "lib/ww/lex/lex.ww", - "lib/ww/lex/tok.ww", - "lib/ww/ast.ww", - "lib/ww/parse/parse.ww", "lib/ww/parse/expr.ww", "lib/ww/parse/stmt.ww", "lib/ww/parse/decl.ww", + "lib/ww/syntax/lex.ww", + "lib/ww/syntax/tok.ww", + "lib/ww/syntax/ast.ww", + "lib/ww/syntax/parse.ww", "lib/ww/syntax/expr.ww", "lib/ww/syntax/stmt.ww", "lib/ww/syntax/decl.ww", "selfhost/cmd/wwdump/main.ww", "selfhost/test/smoke.ww", NULL, @@ -186,12 +186,12 @@ probe_dump_diff(const char *bin) const char *ast_inputs[] = { "selfhost/test/uses.ww", "selfhost/cmd/wcc/err.ww", - "lib/ww/lex/lex.ww", - "lib/ww/lex/tok.ww", - "lib/ww/ast.ww", - "lib/ww/parse/parse.ww", "lib/ww/parse/expr.ww", "lib/ww/parse/stmt.ww", "lib/ww/parse/decl.ww", - "lib/ww/typ.ww", - "lib/ww/sym.ww", + "lib/ww/syntax/lex.ww", + "lib/ww/syntax/tok.ww", + "lib/ww/syntax/ast.ww", + "lib/ww/syntax/parse.ww", "lib/ww/syntax/expr.ww", "lib/ww/syntax/stmt.ww", "lib/ww/syntax/decl.ww", + "lib/ww/syntax/typ.ww", + "lib/ww/syntax/sym.ww", "selfhost/cmd/wcc/check.ww", "selfhost/cmd/wcc/cgen.ww", "selfhost/cmd/wwdump/main.ww", @@ -572,7 +572,7 @@ probe_resolve(const char *bin) if (getcwd(cwd, sizeof cwd) == NULL) return -1; const char *fixtures[] = { "selfhost/cmd/wcc/err.ww", - "lib/ww/lex/tok.ww", + "lib/ww/syntax/tok.ww", "selfhost/test/smoke.ww", NULL, }; @@ -747,8 +747,8 @@ probe_ww_links(const char *bin) runwait(cmd); /* ww build to get the .combined.ww as a side effect. */ snprintf(cmd, sizeof cmd, - "cd %s && timeout 180 %s/ww build -I %s/lib/ww -I %s/lib/ww/lex -I %s/lib/ww/parse -I %s/selfhost/cmd/wcc %s >/dev/null 2>&1", - tmpdir, bin, cwd, cwd, cwd, cwd, tmpsrc); + "cd %s && timeout 180 %s/ww build -I %s/lib/ww -I %s/selfhost/cmd/wcc %s >/dev/null 2>&1", + tmpdir, bin, cwd, cwd, tmpsrc); if (runwait(cmd) != 0) { fprintf(stderr, "ww-links FAIL: ww build %s\n", fix); fail++; @@ -817,7 +817,7 @@ static int probe_cgen_match(const char *bin) { const char *files[] = { - "lib/ww/lex/tok.ww", + "lib/ww/syntax/tok.ww", "selfhost/test/smoke.ww", NULL, }; diff --git a/test/wcc/993_ww_ww.c b/test/wcc/993_ww_ww.c index 257c444f..5b17ea0b 100644 --- a/test/wcc/993_ww_ww.c +++ b/test/wcc/993_ww_ww.c @@ -242,8 +242,8 @@ main(void) static char wwdump_src[2048], wwdump_incs[4096]; snprintf(wwdump_src, sizeof wwdump_src, "%s/selfhost/cmd/wwdump/main.ww", cwd); snprintf(wwdump_incs, sizeof wwdump_incs, - "%s/lib/ww:%s/lib/ww/lex:%s/lib/ww/parse:%s/lib/encoding/utf8:%s/selfhost/cmd/wcc", - cwd, cwd, cwd, cwd, cwd); + "%s/lib/ww:%s/lib/encoding/utf8:%s/selfhost/cmd/wcc", + cwd, cwd, cwd); cases[1].src = wwdump_src; cases[1].incs = wwdump_incs; diff --git a/test/wcc/995_self_rebuild.c b/test/wcc/995_self_rebuild.c index d4156ea0..2e072c8b 100644 --- a/test/wcc/995_self_rebuild.c +++ b/test/wcc/995_self_rebuild.c @@ -102,16 +102,16 @@ spawn_build(const char *bin, const char *cwd, struct buildjob *j) if (j->inc_local && j->inc_local[0]) { snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s/ww_ww build -o %s/main -I %s/%s " - "-I %s/lib/ww -I %s/lib/ww/lex -I %s/lib/ww/parse " + "-I %s/lib/ww " "-I %s/selfhost/cmd/wcc %s/%s >/dev/null 2>%s/build.err", j->workdir, bin, j->workdir, cwd, j->inc_local, - cwd, cwd, cwd, cwd, cwd, j->src_rel, j->workdir); + cwd, cwd, cwd, j->src_rel, j->workdir); } else { snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s/ww_ww build -o %s/main " - "-I %s/lib/ww -I %s/lib/ww/lex -I %s/lib/ww/parse " + "-I %s/lib/ww " "-I %s/selfhost/cmd/wcc %s/%s >/dev/null 2>%s/build.err", - j->workdir, bin, j->workdir, cwd, cwd, cwd, cwd, cwd, j->src_rel, j->workdir); + j->workdir, bin, j->workdir, cwd, cwd, cwd, j->src_rel, j->workdir); } pid_t p = fork();