The ww compiler frontend was split across packages lex (lex+tok), ww
(ast+sym+typ), and parse — mirroring Hare's ref/hare/hare/{ast,lex,parse}.
That split's only payoff is third-party reuse, which ww has zero of: the
frontend is consumed by exactly one client, the wcc backend. The split's
cost is a wide cross-package export surface — every fn over a sibling
package's type must export it, and under separate compilation that
re-triggers check_exported_type, plus a phantom `import tok;` (tok lives
in package lex). Consolidate into ONE package lib/ww/syntax/, modelled on
Go's cmd/compile/internal/syntax. The 9 files move in (package syntax);
the intra-frontend mutual references become same-package; wcc and the
tool mains import syntax. No cstage C change (the C frontend mangles from
the source package clause). Internal data shapes (AST kinds, token model,
lexer/parser state) still mirror ref/hare/hare per rule 6/12 — only the
module decomposition collapses; the stdlib is untouched.
USER-approved (#74); spec .ai/rob-frontend-reorg.md (drew2 fidelity-
confirmed). Rule-6 carve-out documented in CLAUDE.md. Dissolves the tok
phantom import; collapses the intra-frontend export sprawl. Byte-id
rebaseline (lex.X/parse.X/ww.X -> syntax.X); cs==ww held. The residual
syntax->wcc export surface (10 types) + the unqualified-ref question are
separate follow-ups (#72/#75).
468 lines
13 KiB
Plaintext
468 lines
13 KiB
Plaintext
// 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;
|
|
};
|
|
|