Files
ww/lib/ww/syntax/parse.ww
Hojun-Cho 5abb1e6069 wwstage: port struct embedding; graduate #59.13
The last frontend-gap pin: wwstage had no Hare struct embedding
(struct { hash.hash, ... }), rejecting lib/crypto/sha256 at parse.

- parse.ww: the three member forms (named / anonymous struct / bare
  dotted-ident embed), consume-then-branch since this parser has no
  peek; embeds carry f.str == "" and the type in f.lhs.
- check.ww N_TSTRUCT flatten: promote the inner struct's flattened
  fields at base+src.offset (check.c:961-990); the embed is one
  nested-struct unit in the slot ladder; the resolved inner AST is
  planted on the TFIELD rhs for cgen.
- check.ww walkers: astoffset / exprtype N_DOT / #251 struct-lit
  field lookups descend embeds through shared helpers; the collision
  and non-struct-embed rejects live in validatestructfields (the
  once-per-decl diagnostic site).
- cgenutil.ww registerstruct: regfieldrun walks the AST against the
  flattened tfield cursor, descending embeds via the planted inner
  AST so promoted fieldinfo entries keep the inner field's own name
  and type node.
- wwi printers unchanged (both stages already emit nameless fields).

sha256_test compiles byte-identically end to end and its 6 tests
pass; 989_lib_byteid is now 44 id / 0 divergent / 0 wwreject.
Fixtures r5913_* (promoted rw, offset shift, anonymous embed,
two-level embed + promoted fn-ptr callee, three rejects); corpus pin
1485/2970.
2026-08-08 02:37:50 +09:00

606 lines
20 KiB
Plaintext

// 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,
// statement, and declaration parsers live in expr.ww, stmt.ww,
// decl.ww respectively — all in the same `parse` module.
//
// Calling-convention shim: w6c can't yet pass a sub-struct field
// (e.g. p.cur.line where p.cur is a `tok` of size 76). The parser
// stores the current token as flat primitive fields rather than a
// nested `tok` struct; `refill` copies a freshly lexed token in.
package syntax;
// 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;
export type parser = struct {
l: *lex,
errs: i32,
// nocast: while inside `[...]` we treat ':' as the slice
// separator, not the cast operator. Mirrors parse.c's flag.
nocast: i32,
curkind: tkind,
curfile: str,
curline: i32,
curcol: i32,
curtext: str,
curuval: u64,
curfval: f64,
// curtsuffix: typed numeric literal suffix ("i32", "u64", ...) on
// the current TK_INT / TK_FLOAT token, or empty. Parseprimary
// copies this onto the N_INTLIT / N_FLOATLIT node so cgen's
// rhstargetname can map `42i64` to the i64 variant of a tagged
// union without falling back to "first non-str variant" (which
// silently picked tag 0 for typed-int literals; see #10).
curtsuffix: str,
// curmod: the most-recent `module foo;` declaration. Each
// top-level decl is stamped with this value; on concatenated
// multi-file streams successive `module` decls mark per-file
// section boundaries. Mirrors cstage Parser.curmod.
curmod: str,
// M1 #22: active `//ww:module <path>` dotted import path. While set,
// decls stamp nmod=pathmod and imported=1, and the in-file `package`
// clause is an assertion. "" means inactive (root/primary).
pathmod: str,
// #57: active `//ww:module-reset <path>` dotted path. Mangles decls on
// the path WITHOUT imported=1 (primary-ness for -c and the #32 bare-main
// rule stay intact), and the in-file `package` clause asserts (leaf ==
// last component) instead of overwriting curmod. "" means inactive.
resetmod: str,
};
fn refill(p: *parser) void = {
let t: tok;
lexnext(p.l, &t);
p.curkind = t.kind;
p.curfile = t.file;
p.curline = t.line;
p.curcol = t.col;
p.curtext = t.text;
p.curuval = t.uval;
p.curfval = t.fval;
p.curtsuffix = t.tsuffix;
};
export fn parserinit(p: *parser, l: *lex) void = {
p.l = l;
p.errs = 0;
p.nocast = 0;
p.pathmod = "";
p.resetmod = "";
refill(p);
};
fn advance(p: *parser) void = { refill(p); };
fn accepttok(p: *parser, k: tkind) bool = {
if (p.curkind == k) { advance(p); return true; };
return false;
};
fn errmsg(p: *parser, msg: str) void = {
let pre = "parse: ";
os.write(2, pre.ptr, pre.len: u64);
os.write(2, msg.ptr, msg.len: u64);
os.write(2, "\n".ptr, 1u64);
p.errs += 1;
};
fn expecttok(p: *parser, k: tkind, what: str) bool = {
if (p.curkind == k) { advance(p); return true; };
errmsg(p, what);
return false;
};
// expectident — consume the current tkind.TK_IDENT and return its text.
// Returns the empty str on error (and advances to make progress).
fn expectident(p: *parser, into: *str) bool = {
if (p.curkind != tkind.TK_IDENT) {
errmsg(p, "expected identifier");
advance(p);
return false;
};
*into = p.curtext;
advance(p);
return true;
};
// expectbindname — like expectident but also accepts a bare `_`
// discard marker. On `_`, returns "" so the checker skips
// scope_define for the binding.
fn expectbindname(p: *parser, into: *str) bool = {
if (p.curkind == tkind.TK_UNDER) {
*into = "";
advance(p);
return true;
};
return expectident(p, into);
};
// ---- type expressions ------------------------------------------------
//
// Currently: TNAME (single ident, no dotted path yet) and TPTR (`*T`).
// Other forms (slice, array, struct, fn, chan, tuple, tagged) will
// land in subsequent commits.
// joindotted — build "head.tail" for dotted type-name path
// collapse. Mirrors aprintf in C parser; pulled local to avoid a
// cross-module dependency.
fn joindotted(head: str, tail: str) str = {
let n: u64 = head.len: u64 + 1u64 + tail.len: u64;
let buf: []u8 = alloc([], n + 1u64)!;
let i: u64 = 0u64;
let j: i32 = 0;
for (j < head.len) { buf[i] = head[j]; i += 1u64; j += 1; };
buf[i] = '.';
i += 1u64;
j = 0;
for (j < tail.len) { buf[i] = tail[j]; i += 1u64; j += 1; };
buf[i] = 0u8;
let r: str;
r.ptr = buf.ptr;
r.len = n: i32;
return r;
};
fn parsetype(p: *parser) *node = {
let pf = p.curfile;
let pl = p.curline;
let pc = p.curcol;
if (p.curkind == tkind.TK_NOT) {
// `!T` — Hare error-flagged type wrapper.
advance(p);
let n = newnode(nkind.N_TBANG, pf, pl, pc);
n.lhs = parsetype(p);
return n;
};
if (p.curkind == tkind.TK_STAR) {
advance(p);
let n = newnode(nkind.N_TPTR, pf, pl, pc);
n.lhs = parsetype(p);
return n;
};
if (p.curkind == tkind.TK_LBRACK) {
advance(p);
if (p.curkind == tkind.TK_RBRACK) {
advance(p);
let n = newnode(nkind.N_TSLICE, pf, pl, pc);
n.lhs = parsetype(p);
return n;
};
let n = newnode(nkind.N_TARRAY, pf, pl, pc);
// `[_]T` — length inferred from initialiser. n.rhs stays nil
// as the sentinel; the cgen path for nkind.N_LET fills it from the
// array literal's element count.
if (p.curkind == tkind.TK_UNDER) {
advance(p);
} else {
n.rhs = parseexpr(p);
};
expecttok(p, tkind.TK_RBRACK, "expected ']' in array type");
n.lhs = parsetype(p);
return n;
};
if (p.curkind == tkind.TK_STRUCT) {
advance(p);
let n = newnode(nkind.N_TSTRUCT, pf, pl, pc);
// `@packed` is an inline struct TYPE attribute (harec ast.h:95),
// after `struct` and before `{` — NOT a fn-decl attr, so it does
// not route through parseattrs.
if (p.curkind == tkind.TK_AT) {
advance(p);
let an: str;
expectident(p, &an);
if (!streq(an, "packed")) {
errmsg(p, "unknown struct attribute");
} else {
n.packed = 1;
};
};
expecttok(p, tkind.TK_LBRACE, "expected '{' after struct");
let fhead: *node = nil;
let ftail: *node = nil;
for (p.curkind != tkind.TK_RBRACE) {
if (p.curkind == tkind.TK_EOF) { break; };
let fpf = p.curfile;
let fpl = p.curline;
let fpc = p.curcol;
let f = newnode(nkind.N_TFIELD, fpf, fpl, fpc);
// Three member forms (cstage parse.c:230-244):
// name: type — regular field
// struct { ... } — anonymous embedded struct
// Identifier — bare dotted-ident embedded type
// Embeds carry f.str == "" and the type in f.lhs. The
// cstage branches on peek≠':'; this parser has no peek,
// so consume the leading ident first and branch on ':'.
if (p.curkind == tkind.TK_STRUCT) {
f.lhs = parsetype(p);
} else {
let fid: str;
expectident(p, &fid);
if (p.curkind == tkind.TK_COLON) {
advance(p);
f.str = fid;
f.lhs = parsetype(p);
} else {
// Rebuild the dotted TNAME the consumed ident
// began (parsetype's TK_IDENT collapse, L280-285).
let tn = newnode(nkind.N_TNAME, fpf, fpl, fpc);
let acc = fid;
for (p.curkind == tkind.TK_DOT) {
advance(p);
if (p.curkind != tkind.TK_IDENT) { break; };
acc = joindotted(acc, p.curtext);
advance(p);
};
tn.str = acc;
f.lhs = tn;
};
};
if (fhead == nil) { fhead = f; ftail = f; }
else { ftail.next = f; ftail = f; };
if (!accepttok(p, tkind.TK_COMMA)) { break; };
};
expecttok(p, tkind.TK_RBRACE, "expected '}' after struct fields");
n.list = fhead;
return n;
};
if (p.curkind == tkind.TK_ENUM) {
// `enum [storage] { NAME [= expr], ... }`
// Storage defaults to i32 (lhs == nil). Each member is an
// nkind.N_TENUMMEMBER with str=name and lhs = value expr or nil
// (auto-increment when omitted).
advance(p);
let n = newnode(nkind.N_TENUM, pf, pl, pc);
if (p.curkind != tkind.TK_LBRACE) {
n.lhs = parsetype(p);
};
expecttok(p, tkind.TK_LBRACE, "expected '{' after enum");
let mhead: *node = nil;
let mtail: *node = nil;
for (p.curkind != tkind.TK_RBRACE) {
if (p.curkind == tkind.TK_EOF) { break; };
let mpf = p.curfile;
let mpl = p.curline;
let mpc = p.curcol;
let m = newnode(nkind.N_TENUMMEMBER, mpf, mpl, mpc);
let mid: str;
expectident(p, &mid);
m.str = mid;
if (accepttok(p, tkind.TK_ASSIGN)) {
m.lhs = parseexpr(p);
};
if (mhead == nil) { mhead = m; mtail = m; }
else { mtail.next = m; mtail = m; };
if (!accepttok(p, tkind.TK_COMMA)) { break; };
};
expecttok(p, tkind.TK_RBRACE, "expected '}' after enum members");
n.list = mhead;
return n;
};
if (p.curkind == tkind.TK_VOID) {
// `void` keyword in type-expr context — emit as nkind.N_TNAME so
// resolution treats it like any other primitive name.
let n = newnode(nkind.N_TNAME, pf, pl, pc);
n.str = "void";
advance(p);
return n;
};
if (p.curkind == tkind.TK_IDENT) {
let n = newnode(nkind.N_TNAME, pf, pl, pc);
let acc = p.curtext;
advance(p);
// Dotted path collapse: pkg.Type → single TNAME with the
// joined string. Mirrors C parsetype's loop.
for (p.curkind == tkind.TK_DOT) {
advance(p);
if (p.curkind != tkind.TK_IDENT) { break; };
acc = joindotted(acc, p.curtext);
advance(p);
};
n.str = acc;
return n;
};
if (p.curkind == tkind.TK_LPAREN) {
// (T) or (T, T, ...) or (T | T | ...)
//
// Each tagged variant may be prefixed with `...` to mark a
// spread — when the variant resolves to another tagged union
// its variants are flattened into the enclosing union. We
// tag the spread on node.op = TK_ELLIPSIS so resolve_type
// can distinguish intent. Mirrors C parsetype.
advance(p);
let firstspread = accepttok(p, tkind.TK_ELLIPSIS);
let first = parsetype(p);
if (firstspread) { first.op = tkind.TK_ELLIPSIS; };
if (accepttok(p, tkind.TK_PIPE)) {
let n = newnode(nkind.N_TTAGGED, pf, pl, pc);
let head = first;
let tail = first;
for (true) {
let spread = accepttok(p, tkind.TK_ELLIPSIS);
let e = parsetype(p);
if (spread) { e.op = tkind.TK_ELLIPSIS; };
tail.next = e;
tail = e;
if (!accepttok(p, tkind.TK_PIPE)) { break; };
};
expecttok(p, tkind.TK_RPAREN, "expected ')' in tagged-union type");
n.list = head;
return n;
};
if (firstspread) {
errmsg(p, "spread '...' only valid before tagged-union variants");
};
if (!accepttok(p, tkind.TK_COMMA)) {
expecttok(p, tkind.TK_RPAREN, "expected ')' after parenthesised type");
return first;
};
// Wrap each element in N_TPARAM so the chain owns its .next.
// Mirrors cstage's Tparam (cmd/wcc/check.c:1437-1451); lifted
// to the AST layer here because wwstage has no separate type
// layer, and exprtype must return shared element-type nodes
// (sym.decl.lhs, struct field's .lhs, another N_TTUPLE element)
// without corrupting source ASTs.
let n = newnode(nkind.N_TTUPLE, pf, pl, pc);
let firstwrap = newnode(nkind.N_TPARAM, pf, pl, pc);
firstwrap.lhs = first;
let head = firstwrap;
let tail = firstwrap;
for (true) {
let e = parsetype(p);
let w = newnode(nkind.N_TPARAM, e.file, e.line, e.col);
w.lhs = e;
tail.next = w;
tail = w;
if (!accepttok(p, tkind.TK_COMMA)) { break; };
if (p.curkind == tkind.TK_RPAREN) { break; };
};
expecttok(p, tkind.TK_RPAREN, "expected ')' in tuple type");
n.list = head;
return n;
};
if (p.curkind == tkind.TK_FN) {
advance(p);
expecttok(p, tkind.TK_LPAREN, "expected '(' after fn in type");
let n = newnode(nkind.N_TFN, pf, pl, pc);
// Anonymous-or-named params: parseparams handles named only;
// for fn-type expressions the C parser allows IDENT-less
// (anonymous) params. Stub: only named params for now.
n.list = parseparams(p);
expecttok(p, tkind.TK_RPAREN, "expected ')' after fn type params");
n.lhs = parsetype(p);
return n;
};
errmsg(p, "expected type");
advance(p);
return newnode(nkind.N_TNAME, pf, pl, pc);
};
// ---- expressions (Pratt) ---------------------------------------------
//
// Forwards: parseexpr → parsebin → parseunary → parsepostfix(parseprimary).
// Tuple literals, match expressions, struct literals, slice [lo:hi],
// and the ?/! try operators are not yet wired — they'll arrive as the
// AST diff fixture grows to need them.
fn bprec(k: tkind) i32 = {
if (k == tkind.TK_OR) { return 1; };
if (k == tkind.TK_AND) { return 2; };
if (k == tkind.TK_EQ) { return 3; };
if (k == tkind.TK_NEQ) { return 3; };
if (k == tkind.TK_LT) { return 4; };
if (k == tkind.TK_LE) { return 4; };
if (k == tkind.TK_GT) { return 4; };
if (k == tkind.TK_GE) { return 4; };
if (k == tkind.TK_PIPE) { return 5; };
if (k == tkind.TK_CARET) { return 6; };
if (k == tkind.TK_AMP) { return 7; };
if (k == tkind.TK_LSHIFT) { return 8; };
if (k == tkind.TK_RSHIFT) { return 8; };
if (k == tkind.TK_PLUS) { return 9; };
if (k == tkind.TK_MINUS) { return 9; };
if (k == tkind.TK_STAR) { return 10; };
if (k == tkind.TK_SLASH) { return 10; };
if (k == tkind.TK_PERCENT) { return 10; };
return 0;
};
fn isassignop(k: tkind) bool = {
if (k == tkind.TK_ASSIGN) { return true; };
if (k == tkind.TK_PLUSEQ) { return true; };
if (k == tkind.TK_MINUSEQ) { return true; };
if (k == tkind.TK_STAREQ) { return true; };
if (k == tkind.TK_SLASHEQ) { return true; };
if (k == tkind.TK_PERCENTEQ) { return true; };
if (k == tkind.TK_AMPEQ) { return true; };
if (k == tkind.TK_PIPEEQ) { return true; };
if (k == tkind.TK_CARETEQ) { return true; };
if (k == tkind.TK_LSHIFTEQ) { return true; };
if (k == tkind.TK_RSHIFTEQ) { return true; };
return false;
};
// Forward references between parseunary/parseexpr/parsebin/parsepostfix
// are resolved by the two-pass checker — no body-less prototypes needed.
export fn parsefile(p: *parser) *node = {
let f = newnode(nkind.N_FILE, p.curfile, p.curline, p.curcol);
let head: *node = nil;
let tail: *node = nil;
let sawpackage: i32 = 0;
for (p.curkind != tkind.TK_EOF) {
// `package foo;` — directory-as-module declaration. Every
// primary section opens with one (`package main;` for an
// executable); a missing clause on the first real decl is a
// hard error (strict-package, #24a). Imported (pathmod) and
// sep primary-reset (resetmod) regions carry identity
// out-of-band and are exempt.
if (p.curkind == tkind.TK_MODULE) {
sawpackage = 1;
advance(p);
let name: str;
expectident(p, &name);
expecttok(p, tkind.TK_SEMI, "expected ';' after module name");
if (p.pathmod.len != 0 || p.resetmod.len != 0) {
// M1 #22: while an import path is active the in-file
// `package` clause is an ASSERTION — its leaf must
// equal the path's last component; it does NOT
// overwrite the path-derived module. #57 extends this
// to the sep primary-reset path (resetmod): the dotted
// reset path is authoritative, the clause asserts.
let active: str = p.pathmod;
if (p.pathmod.len == 0) { active = p.resetmod; };
let (pre, post) = strings.rcut(active, ".");
let last: str = post;
if (post.len == 0) { last = active; };
if (strings.compare(name, last) != 0) {
errmsg(p, "package does not match import path");
};
} else {
p.curmod = name;
// #11: stamp the primary module identity on the
// N_FILE node so wwi_emit can derive the `package`
// leaf even when the body carries zero module-tagged
// decls. Primary identity only; never the imported
// boundary (TK_MODPATH).
if (f.nmod.len == 0) { f.nmod = name; };
};
continue;
};
// `//ww:module <path>` — M1 #22 import boundary. The following
// file's decls mangle on the full dotted import path, not the
// leaf `package` clause, and are flagged imported (gates the
// root-only bare-`main` rule, #32).
if (p.curkind == tkind.TK_MODPATH) {
sawpackage = 0;
p.pathmod = p.curtext;
p.curmod = p.curtext;
p.resetmod = "";
advance(p);
continue;
};
// `//ww:module-reset` — bundle boundary before a package-less
// file. Reset curmod to "" so the file's decls (and its own
// `import os;`) are attributed to the primary module, not the
// preceding bundled package. Codegen-neutral: "" curmod keeps
// bare symbols. (#16 option-B; closes task #11.)
// Driver-emitted ONLY before package-less files; a hand-placed
// directive after a mid-file `package` would strip subsequent
// decls to bare — that usage is deliberate-only.
if (p.curkind == tkind.TK_MODRESET) {
sawpackage = 0;
// #57: a path-carrying reset (sep primary body) mangles decls
// on the dotted path so definer == importer, but leaves
// imported==0 (curmod set, pathmod "") so -c primary-ness and
// the #32 bare-main rule are intact; the body's `package`
// clause then asserts (resetmod). A bare reset is the
// root/package-less boundary: curmod "" → bare, today's path.
let rp: str = p.curtext;
advance(p);
p.pathmod = "";
if (rp.len != 0) {
p.curmod = rp;
p.resetmod = rp;
// #11: path-carrying reset is a primary body identity
// (sep); stamp it for the wwi leaf fallback. A bare
// reset (rp empty) is the root/package-less boundary
// and MUST keep the "main" default — so don't stamp.
if (f.nmod.len == 0) { f.nmod = rp; };
} else {
let empty: str;
empty.ptr = nil;
empty.len = 0;
p.curmod = empty;
p.resetmod = "";
};
continue;
};
// strict-package: a primary section's first real decl must be
// preceded by a `package` clause. Imported (pathmod) and sep
// primary-reset (resetmod) regions carry identity out-of-band,
// so they are exempt. Wording mirrors Go's missing-`package`
// diagnostic (rule 5; Hare has no clause to port).
if (p.pathmod.len == 0 && p.resetmod.len == 0 && sawpackage == 0) {
errmsg(p, "missing package clause");
sawpackage = 1;
};
let attrs = parseattrs(p);
let exported: i32 = 0;
if (p.curkind == tkind.TK_EXPORT) { exported = 1; advance(p); };
let d: *node = nil;
if (p.curkind == tkind.TK_USE) {
d = parseuse(p);
} else { if (p.curkind == tkind.TK_DEF) {
d = parsedef(p, exported);
} else { if (p.curkind == tkind.TK_TYPE) {
d = parsetypedecl(p, exported);
} else { if (p.curkind == tkind.TK_LET) {
d = parselet(p, exported);
} else { if (p.curkind == tkind.TK_CONST) {
d = parselet(p, exported);
} else { if (p.curkind == tkind.TK_FN) {
d = parsefn(p, exported, attrs);
} else {
// cstage parse.c:1395-1400 errorf+p->errs++ on the
// default arm: an unknown top-level construct is a loud
// reject, not a silent skip. ww chews the whole decl at
// once (below), so one error per fallback entry.
errmsg(p, "expected top-level decl");
// Recovery: chew tokens until next ';' or EOF, balancing
// '{' '}' pairs so internal ';'s in unfamiliar forms don't
// derail us.
for (p.curkind != tkind.TK_SEMI) {
if (p.curkind == tkind.TK_EOF) { break; };
if (p.curkind == tkind.TK_LBRACE) {
let depth: i32 = 0;
for (true) {
if (p.curkind == tkind.TK_EOF) { break; };
if (p.curkind == tkind.TK_LBRACE) { depth += 1; advance(p); continue; };
if (p.curkind == tkind.TK_RBRACE) {
depth -= 1;
advance(p);
if (depth == 0) { break; };
continue;
};
advance(p);
};
continue;
};
advance(p);
};
if (p.curkind == tkind.TK_SEMI) { advance(p); };
};};};};};};
if (d != nil) {
// M1 #22: flag decls reached via an import-path boundary
// (gates the root-only bare-`main` rule, #32).
if (p.pathmod.len != 0) { d.imported = 1; };
if (head == nil) {
head = d;
tail = d;
} else {
tail.next = d;
tail = d;
};
};
};
f.list = head;
return f;
};