import { lex, Tok, type Token, type Span, LexError } from "./lexer.js"; import type * as A from "./ast.js"; export class ParseError extends Error { constructor( message: string, public span: Span, ) { super(message); } } export function parse(source: string, file: string): A.Program { let tokens: Token[]; try { tokens = lex(source, file); } catch (e) { if (e instanceof LexError) throw new ParseError(e.message, e.span); throw e; } const p = new Parser(tokens, file); return p.parseProgram(); } class Parser { private pos = 0; private src: string; /** when set, an Ident followed by '{' is NOT a struct literal (used before blocks) */ private noStructLiteral = 0; /** nesting of ( [ { — inside them, newlines are ignored */ private depth = 0; constructor( private tokens: Token[], private file: string, ) { this.src = this.file; } private noStruct(fn: () => T): T { this.noStructLiteral++; try { return fn(); } finally { this.noStructLiteral--; } } /** skip newline tokens (callers decide when newlines are ignorable) */ private skipNls(): void { while (this.pos < this.tokens.length && this.tokens[this.pos]!.kind === Tok.Newline) this.pos++; } private peek(offset = 0): Token { if (this.depth > 0) this.skipNls(); return this.tokens[Math.min(this.pos + offset, this.tokens.length - 1)]!; } private next(): Token { const t = this.tokens[this.pos]!; if (t.kind === Tok.LParen || t.kind === Tok.LBracket) this.depth++; else if (t.kind === Tok.RParen || t.kind === Tok.RBracket) { if (this.depth > 0) this.depth--; } if (t.kind !== Tok.Eof) this.pos++; return t; } private is(kind: Tok): boolean { return this.peek().kind === kind; } private eat(kind: Tok): Token | null { if (this.is(kind)) return this.next(); return null; } private expect(kind: Tok, what: string): Token { const t = this.peek(); if (t.kind !== kind) { throw new ParseError(`expected ${what} but found '${t.text || t.kind}'`, t.span); } return this.next(); } private expectIdent(what: string): Token { const t = this.peek(); if (t.kind !== Tok.Ident) { throw new ParseError(`expected ${what} but found '${t.text || t.kind}'`, t.span); } return this.next(); } /** module segments may collide with keywords (e.g. 'test' in std.test) */ private expectSegmentName(what: string): string { const t = this.peek(); if (t.kind === Tok.Ident) { this.next(); return t.text; } if (t.kind === Tok.KwTest) { this.next(); return "test"; } throw new ParseError(`expected ${what} but found '${t.text || t.kind}'`, t.span); } /** member names may collide with keywords (e.g. Schema.from, 'is') */ private expectMemberName(what: string): string { const t = this.peek(); if (t.kind === Tok.Ident) { this.next(); return t.text; } if (t.kind.startsWith("kw")) { this.next(); return t.text; } throw new ParseError(`expected ${what} but found '${t.text || t.kind}'`, t.span); } // ---------- program ---------- parseProgram(): A.Program { const span = this.peek().span; const imports: A.ImportDecl[] = []; const decls: A.Decl[] = []; const tests: A.TestDecl[] = []; this.skipNls(); while (!this.is(Tok.Eof)) { if (this.is(Tok.KwImport)) { imports.push(this.parseImport()); this.skipNls(); continue; } if (this.is(Tok.KwFrom)) { imports.push(this.parseFromImport()); this.skipNls(); continue; } if (this.is(Tok.KwExport)) { const start = this.next().span; const d = this.parseDecl(true, start); if (d) decls.push(d); this.skipNls(); continue; } if (this.is(Tok.KwTest)) { tests.push(this.parseTest()); this.skipNls(); continue; } const d = this.parseDecl(false, this.peek().span); if (d) decls.push(d); this.skipNls(); } return { kind: "program", imports, decls, tests, span: { ...span, end: this.peek().span.end }, file: this.file, }; } private parseImport(): A.ImportDecl { const start = this.expect(Tok.KwImport, "'import'").span; this.skipNls(); const module = this.parseModuleRef(); this.skipNls(); let alias: string | null = null; if (this.eat(Tok.KwAs)) alias = this.expectIdent("alias name").text; return { kind: "import", module, alias, names: [], span: { ...start, end: this.peek().span.start }, file: this.file }; } private parseFromImport(): A.ImportDecl { const start = this.expect(Tok.KwFrom, "'from'").span; this.skipNls(); const module = this.parseModuleRef(); this.skipNls(); this.expect(Tok.KwImport, "'import'"); this.skipNls(); const names: A.ImportDecl["names"] = []; do { const name = this.expectIdent("imported name").text; let alias: string | null = null; if (this.eat(Tok.KwAs)) alias = this.expectIdent("alias name").text; names.push({ name, alias }); } while (this.eat(Tok.Comma)); return { kind: "import", module, alias: null, names, span: { ...start, end: this.peek().span.start }, file: this.file }; } private parseModuleRef(): A.ModuleRef { const start = this.peek().span; const t = this.peek(); if (t.kind === Tok.Ident && t.text === "std") { this.next(); const segments: string[] = []; do { this.expect(Tok.Dot, "'.'"); this.skipNls(); segments.push(this.expectSegmentName("module segment")); this.skipNls(); } while (this.is(Tok.Dot)); return { kind: "std", segments, span: { ...start, end: this.peek().span.start } }; } if (t.kind === Tok.Ident && t.text === "pkg") { this.next(); this.expect(Tok.Colon, "':'"); const name = this.expectIdent("package name").text; return { kind: "pkg", name, span: { ...start, end: this.peek().span.start } }; } if (t.kind === Tok.Ident && t.text === "js") { this.next(); this.expect(Tok.Colon, "':'"); let name = this.expectIdent("js module name").text; while (this.is(Tok.Slash)) { this.next(); name += "/" + this.expectIdent("js module segment").text; } return { kind: "js", name, span: { ...start, end: this.peek().span.start } }; } if (this.is(Tok.StrStart)) { this.next(); const parts = this.parseStringParts(t); const text = parts.map((p) => (p.kind === "text" ? p.text : "")).join(""); return { kind: "rel", path: text, span: { ...start, end: this.peek().span.start } }; } throw new ParseError("expected a module path ('std.*', 'pkg:name', 'js:name', or a quoted path)", start); } private parseStringParts(startTok: Token): A.InterpPart[] { const parts: A.InterpPart[] = []; while (true) { const t = this.peek(); if (t.kind === Tok.StrPart) { this.next(); parts.push({ kind: "text", text: t.text! }); continue; } if (t.kind === Tok.StrEnd) { this.next(); break; } throw new ParseError("unexpected token inside string", t.span); } void startTok; return parts; } private parseTest(): A.TestDecl { const start = this.expect(Tok.KwTest, "'test'").span; const nameTok = this.peek(); if (nameTok.kind !== Tok.StrStart) throw new ParseError("expected test name string", nameTok.span); this.next(); const parts = this.parseStringParts(nameTok); const name = parts.map((p) => (p.kind === "text" ? p.text : "")).join(""); const body = this.parseBlock(); return { kind: "test", name, body, span: { ...start, end: body.span.end }, file: this.file }; } private parseDecl(exported: boolean, start: Span): A.Decl | null { const t = this.peek(); switch (t.kind) { case Tok.KwLet: return this.parseTopLet(exported, start); case Tok.KwFun: case Tok.KwAsync: { const isAsync = this.is(Tok.KwAsync); if (isAsync) this.next(); return this.parseFun(exported, start, isAsync); } case Tok.KwStruct: return this.parseStruct(exported, start); case Tok.KwEnum: return this.parseEnum(exported, start); case Tok.KwType: return this.parseTypeDecl(exported, start); case Tok.KwContract: return this.parseContract(exported, start); case Tok.KwSchema: return this.parseSchema(exported, start); case Tok.KwClass: return this.parseClass(exported, start); default: throw new ParseError(`expected a declaration ('let', 'fun', 'struct', ...) but found '${t.text || t.kind}'`, t.span); } } private parseTopLet(exported: boolean, start: Span): A.LetDecl { this.expect(Tok.KwLet, "'let'"); const mut = !!this.eat(Tok.KwMut); const name = this.expectIdent("variable name").text; let type: A.TypeExpr | null = null; if (this.eat(Tok.Colon)) type = this.parseTypeExpr(); this.expect(Tok.Assign, "'='"); const init = this.parseExpr(); return { kind: "let", name, mut, type, init, exported, span: { ...start, end: init.span.end }, file: this.file }; } private parseFun(exported: boolean, start: Span, isAsync: boolean): A.FunDecl { this.expect(Tok.KwFun, "'fun'"); this.skipNls(); const name = this.expectIdent("function name").text; const params = this.parseParams(false); this.skipNls(); let retType: A.TypeExpr | null = null; if (this.eat(Tok.Arrow)) { this.skipNls(); retType = this.parseTypeExpr(); this.skipNls(); } const body = this.parseFunBody(); return { kind: "fun", name, async: isAsync, params, retType, body, exported, span: { ...start, end: body.span.end }, file: this.file, }; } private parseParams(lambda: boolean): A.Param[] { this.expect(Tok.LParen, "'('"); const params: A.Param[] = []; while (!this.is(Tok.RParen)) { if (this.is(Tok.Eof)) throw new ParseError("unterminated parameter list", this.peek().span); const name = this.expectIdent("parameter name").text; let type: A.TypeExpr; if (this.eat(Tok.Colon)) { type = this.parseTypeExpr(); } else if (lambda) { type = { kind: "named", name: "_", args: [], span: this.peek().span }; } else { throw new ParseError(`parameter '${name}' needs a type annotation`, this.peek().span); } params.push({ name, type, span: this.peek().span }); if (!this.eat(Tok.Comma)) break; } this.expect(Tok.RParen, "')'"); return params; } private parseFunBody(): A.FunBody { if (this.eat(Tok.Assign)) { const e = this.parseExpr(); return { kind: "expr", expr: e, span: e.span }; } this.skipNls(); return this.parseBlock(); } private parseStruct(exported: boolean, start: Span): A.StructDecl { this.expect(Tok.KwStruct, "'struct'"); this.skipNls(); const name = this.expectIdent("struct name").text; this.skipNls(); this.expect(Tok.LBrace, "'{'"); const fields: A.StructField[] = []; this.skipNls(); while (!this.is(Tok.RBrace)) { if (this.is(Tok.Eof)) throw new ParseError("unterminated struct body", this.peek().span); const fname = this.expectIdent("field name").text; this.expect(Tok.Colon, "':'"); const type = this.parseTypeExpr(); fields.push({ name: fname, type, span: this.peek().span }); this.eat(Tok.Comma); this.skipNls(); } this.expect(Tok.RBrace, "'}'"); return { kind: "struct", name, fields, exported, span: { ...start, end: this.peek().span.start }, file: this.file }; } private parseEnum(exported: boolean, start: Span): A.EnumDecl { this.expect(Tok.KwEnum, "'enum'"); this.skipNls(); const name = this.expectIdent("enum name").text; this.skipNls(); this.expect(Tok.LBrace, "'{'"); const variants: string[] = []; this.skipNls(); while (!this.is(Tok.RBrace)) { if (this.is(Tok.Eof)) throw new ParseError("unterminated enum body", this.peek().span); variants.push(this.expectIdent("variant name").text); this.eat(Tok.Comma); this.skipNls(); } this.expect(Tok.RBrace, "'}'"); return { kind: "enum", name, variants, exported, span: { ...start, end: this.peek().span.start }, file: this.file }; } private parseTypeDecl(exported: boolean, start: Span): A.TypeDecl { this.expect(Tok.KwType, "'type'"); this.skipNls(); const name = this.expectIdent("type name").text; this.skipNls(); this.expect(Tok.Assign, "'='"); this.skipNls(); this.eat(Tok.Pipe); const variants: A.VariantDef[] = []; do { const vstart = this.peek().span; const vname = this.expectIdent("variant name").text; let params: A.Param[] | null = null; if (this.is(Tok.LParen)) params = this.parseParams(false); variants.push({ name: vname, params, span: vstart }); this.skipNls(); } while (this.eat(Tok.Pipe)); return { kind: "type", name, variants, exported, span: { ...start, end: this.peek().span.start }, file: this.file }; } private parseContract(exported: boolean, start: Span): A.ContractDecl { this.expect(Tok.KwContract, "'contract'"); this.skipNls(); const name = this.expectIdent("contract name").text; this.skipNls(); const typeParams: string[] = []; if (this.is(Tok.Lt)) { this.next(); do { typeParams.push(this.expectIdent("type parameter").text); } while (this.eat(Tok.Comma)); this.expect(Tok.Gt, "'>'"); } this.skipNls(); this.expect(Tok.LBrace, "'{'"); const methods: A.ContractMethod[] = []; this.skipNls(); while (!this.is(Tok.RBrace)) { if (this.is(Tok.Eof)) throw new ParseError("unterminated contract body", this.peek().span); const mstart = this.peek().span; const mname = this.expectIdent("method name").text; const params = this.parseParams(false); this.skipNls(); this.expect(Tok.Arrow, "'->'"); this.skipNls(); const retType = this.parseTypeExpr(); methods.push({ name: mname, params, retType, span: mstart }); this.eat(Tok.Comma); this.skipNls(); } this.expect(Tok.RBrace, "'}'"); return { kind: "contract", name, typeParams, methods, exported, span: { ...start, end: this.peek().span.start }, file: this.file }; } private parseSchema(exported: boolean, start: Span): A.SchemaDecl { this.expect(Tok.KwSchema, "'schema'"); this.skipNls(); const name = this.expectIdent("schema name").text; this.skipNls(); this.expect(Tok.LBrace, "'{'"); const fields: A.SchemaField[] = []; this.skipNls(); while (!this.is(Tok.RBrace)) { if (this.is(Tok.Eof)) throw new ParseError("unterminated schema body", this.peek().span); const fstart = this.peek().span; const fname = this.expectIdent("field name").text; this.expect(Tok.Colon, "':'"); const type = this.parseTypeExpr(); const constraints: A.SchemaField["constraints"] = []; if (this.eat(Tok.LParen)) { while (!this.is(Tok.RParen)) { if (this.is(Tok.Eof)) throw new ParseError("unterminated constraints", this.peek().span); const cname = this.expectIdent("constraint name").text; this.expect(Tok.Colon, "':'"); const value = this.parseExpr(); constraints.push({ name: cname, value, span: this.peek().span }); if (!this.eat(Tok.Comma)) break; } this.expect(Tok.RParen, "')'"); } if (this.eat(Tok.Assign)) { constraints.push({ name: "default", value: this.parseExpr(), span: this.peek().span }); } fields.push({ name: fname, type, constraints, span: fstart }); this.eat(Tok.Comma); this.skipNls(); } this.expect(Tok.RBrace, "'}'"); return { kind: "schema", name, fields, exported, span: { ...start, end: this.peek().span.start }, file: this.file }; } private parseClass(exported: boolean, start: Span): A.ClassDecl { this.expect(Tok.KwClass, "'class'"); this.skipNls(); const name = this.expectIdent("class name").text; this.skipNls(); this.expect(Tok.LBrace, "'{'"); const fields: A.ClassField[] = []; const methods: A.FunDecl[] = []; this.skipNls(); while (!this.is(Tok.RBrace)) { if (this.is(Tok.Eof)) throw new ParseError("unterminated class body", this.peek().span); const mstart = this.peek().span; let memberExported = false; if (this.eat(Tok.KwExport)) memberExported = true; this.skipNls(); if (this.is(Tok.KwFun) || this.is(Tok.KwAsync)) { const isAsync = this.is(Tok.KwAsync); if (isAsync) this.next(); methods.push(this.parseFun(memberExported, mstart, isAsync)); } else { const mut = !!this.eat(Tok.KwMut); const fname = this.expectIdent("field name").text; this.expect(Tok.Colon, "':'"); const type = this.parseTypeExpr(); let init: A.Expr | null = null; if (this.eat(Tok.Assign)) init = this.parseExpr(); fields.push({ name: fname, mut, type, init, exported: memberExported, span: mstart }); this.eat(Tok.Comma); } this.skipNls(); } this.expect(Tok.RBrace, "'}'"); return { kind: "class", name, fields, methods, exported, span: { ...start, end: this.peek().span.start }, file: this.file }; } // ---------- statements ---------- private parseBlock(): A.Block { const start = this.expect(Tok.LBrace, "'{'").span; const stmts: A.Stmt[] = []; this.skipNls(); while (!this.is(Tok.RBrace)) { if (this.is(Tok.Eof)) throw new ParseError("unterminated block", this.peek().span); stmts.push(this.parseStmt()); this.skipNls(); } const end = this.next().span; return { kind: "block", stmts, span: { ...start, end: end.end } }; } private parseStmt(): A.Stmt { const t = this.peek(); const start = t.span; switch (t.kind) { case Tok.KwLet: { this.next(); this.skipNls(); const mut = !!this.eat(Tok.KwMut); this.skipNls(); const name = this.expectIdent("variable name").text; let type: A.TypeExpr | null = null; if (this.eat(Tok.Colon)) type = this.parseTypeExpr(); this.expect(Tok.Assign, "'='"); const init = this.parseExpr(); return { kind: "let", name, mut, type, init, span: { ...start, end: init.span.end } }; } case Tok.KwReturn: { this.next(); let value: A.Expr | null = null; if (!this.is(Tok.RBrace) && !this.is(Tok.Newline) && !this.is(Tok.Eof)) value = this.parseExpr(); return { kind: "return", value, span: { ...start, end: this.peek().span.start } }; } case Tok.KwRaise: { this.next(); const value = this.parseExpr(); return { kind: "raise", value, span: { ...start, end: value.span.end } }; } case Tok.KwIf: { this.next(); this.skipNls(); if (this.eat(Tok.KwLet)) { const pattern = this.parsePattern(); this.skipNls(); this.expect(Tok.Assign, "'='"); const value = this.noStruct(() => this.parseExpr()); this.skipNls(); const then = this.parseBlock(); this.skipNls(); let elseBlock: A.Block | null = null; if (this.is(Tok.KwElse)) { this.next(); elseBlock = this.parseBlock(); } return { kind: "iflet", pattern, value, then, else: elseBlock, span: { ...start, end: (elseBlock ?? then).span.end } }; } const cond = this.noStruct(() => this.parseExpr()); this.skipNls(); const then = this.parseBlock(); this.skipNls(); let elseBlock: A.Block | null = null; if (this.is(Tok.KwElse)) { this.next(); elseBlock = this.parseBlock(); } return { kind: "if", cond, then, else: elseBlock, span: { ...start, end: (elseBlock ?? then).span.end } }; } case Tok.KwMatch: { this.next(); const value = this.noStruct(() => this.parseExpr()); this.skipNls(); const arms = this.parseMatchArms(); return { kind: "match", value, arms, span: { ...start, end: arms.length ? arms[arms.length - 1]!.span.end : this.peek().span.start } }; } case Tok.KwFor: { this.next(); this.skipNls(); const pattern = this.parsePattern(); this.skipNls(); this.expect(Tok.KwIn, "'in'"); this.skipNls(); const iterable = this.noStruct(() => this.parseExpr()); this.skipNls(); const body = this.parseBlock(); return { kind: "for", pattern, iterable, body, span: { ...start, end: body.span.end } }; } case Tok.KwWhile: { this.next(); const cond = this.noStruct(() => this.parseExpr()); this.skipNls(); const body = this.parseBlock(); return { kind: "while", cond, body, span: { ...start, end: body.span.end } }; } case Tok.KwTry: { this.next(); const body = this.parseBlock(); this.skipNls(); this.expect(Tok.KwCatch, "'catch'"); const catchVar = this.expectIdent("catch variable").text; this.skipNls(); const handler = this.parseBlock(); return { kind: "try", body, catchVar, handler, span: { ...start, end: handler.span.end } }; } case Tok.KwWith: { this.next(); this.expect(Tok.LParen, "'('"); this.skipNls(); this.expect(Tok.KwLet, "'let'"); const name = this.expectIdent("resource name").text; this.expect(Tok.Assign, "'='"); const init = this.parseExpr(); this.skipNls(); this.expect(Tok.RParen, "')'"); this.skipNls(); const body = this.parseBlock(); return { kind: "with", name, init, body, span: { ...start, end: body.span.end } }; } default: { const expr = this.parseExpr(); if (!this.is(Tok.Newline) && !this.is(Tok.RBrace) && !this.is(Tok.Eof)) { const t2 = this.peek(); throw new ParseError(`expected end of line but found '${t2.text || t2.kind}'`, t2.span); } return { kind: "expr", expr, span: { ...start, end: expr.span.end } }; } } } private parseMatchArms(): A.MatchArm[] { this.expect(Tok.LBrace, "'{'"); const arms: A.MatchArm[] = []; this.skipNls(); while (!this.is(Tok.RBrace)) { if (this.is(Tok.Eof)) throw new ParseError("unterminated match", this.peek().span); const start = this.peek().span; const pattern = this.parsePattern(); this.skipNls(); this.expect(Tok.Arrow, "'->'"); this.skipNls(); let body: A.Expr | A.Block; if (this.is(Tok.LBrace)) { body = this.parseBlock(); } else { body = this.parseExpr(); } arms.push({ pattern, body, span: { ...start, end: body.span.end } }); this.eat(Tok.Comma); this.skipNls(); } this.expect(Tok.RBrace, "'}'"); return arms; } // ---------- patterns ---------- private parsePattern(): A.Pattern { const left = this.parsePatternAtom(); if (this.is(Tok.KwOr)) { const items: A.Pattern[] = [left]; while (this.eat(Tok.KwOr)) items.push(this.parsePatternAtom()); return { kind: "alt", items, span: { ...items[0]!.span, end: items[items.length - 1]!.span.end } }; } return left; } private parsePatternAtom(): A.Pattern { const t = this.peek(); const start = t.span; switch (t.kind) { case Tok.KwSome: { this.next(); const inner = this.parsePattern(); return { kind: "some", inner, span: { ...start, end: inner.span.end } }; } case Tok.KwNone: this.next(); return { kind: "none", span: start }; case Tok.KwOk: { this.next(); const inner = this.parsePattern(); return { kind: "ok", inner, span: { ...start, end: inner.span.end } }; } case Tok.KwErr: { this.next(); let inner: A.Pattern | null = null; if (this.is(Tok.LParen)) { this.next(); if (!this.is(Tok.RParen)) inner = this.parsePattern(); this.expect(Tok.RParen, "')'"); } return { kind: "err", inner, span: { ...start, end: this.peek().span.start } }; } case Tok.KwIs: { this.next(); const type = this.parseTypeExpr(); return { kind: "is", type, span: { ...start, end: type.span.end } }; } case Tok.LParen: { this.next(); const items: A.Pattern[] = []; if (!this.is(Tok.RParen)) { items.push(this.parsePattern()); while (this.eat(Tok.Comma)) items.push(this.parsePattern()); } this.expect(Tok.RParen, "')'"); if (items.length === 1) return items[0]!; return { kind: "tuple", items, span: { ...start, end: this.peek().span.start } }; } case Tok.Int: case Tok.Float: case Tok.StrStart: case Tok.Char: case Tok.KwTrue: case Tok.KwFalse: { const lit = this.parseLiteralExpr(); if (lit.kind === "string") { const text = lit.parts.map((p) => (p.kind === "text" ? p.text : "")).join(""); return { kind: "literal", value: text, lit: "string", span: lit.span }; } if (lit.kind === "bool") return { kind: "literal", value: lit.value, lit: "bool", span: lit.span }; if (lit.kind === "char") return { kind: "literal", value: lit.value, lit: "char", span: lit.span }; if (lit.kind === "int" || lit.kind === "float") { return { kind: "literal", value: lit.value, lit: lit.kind, span: lit.span }; } throw new ParseError("this literal cannot be used as a pattern", lit.span); } case Tok.Ident: { this.next(); if (t.text === "_") return { kind: "wild", span: t.span }; if (this.is(Tok.LParen)) { this.next(); const args: A.Pattern[] = []; if (!this.is(Tok.RParen)) { args.push(this.parsePattern()); while (this.eat(Tok.Comma)) args.push(this.parsePattern()); } this.expect(Tok.RParen, "')'"); return { kind: "variant", name: t.text, args: args.length ? args : null, span: { ...start, end: this.peek().span.start } }; } return { kind: "ident", name: t.text, span: t.span }; } default: throw new ParseError(`expected a pattern but found '${t.text || t.kind}'`, t.span); } } // ---------- expressions ---------- parseExpr(): A.Expr { return this.parseAssignment(); } private parseAssignment(): A.Expr { const left = this.parseOr(); const t = this.peek(); const isAssign = t.kind === Tok.Assign || t.kind === Tok.PlusEq || t.kind === Tok.MinusEq || t.kind === Tok.StarEq || t.kind === Tok.SlashEq || t.kind === Tok.PercentEq; if (isAssign) { this.next(); const value = this.parseAssignment(); return { kind: "assign", op: t.text, target: left, value, span: { ...left.span, end: value.span.end } }; } return left; } private parseOr(): A.Expr { const left = this.parseAnd(); if (this.is(Tok.KwOr)) { this.next(); const right = this.parseOr(); return { kind: "binary", op: "or", left, right, span: { ...left.span, end: right.span.end } }; } return left; } private parseAnd(): A.Expr { const left = this.parseComparison(); const t = this.peek(); if (t.kind === Tok.AndAnd || t.kind === Tok.KwAnd) { this.next(); const right = this.parseAnd(); return { kind: "binary", op: "&&", left, right, span: { ...left.span, end: right.span.end } }; } return left; } private parseComparison(): A.Expr { const left = this.parseRangeLevel(); const t = this.peek(); if (t.kind === Tok.KwIs) { this.next(); const type = this.parseTypeExpr(); return { kind: "is", left, type, span: { ...left.span, end: type.span.end } }; } if (t.kind === Tok.EqEq || t.kind === Tok.NotEq || t.kind === Tok.Lt || t.kind === Tok.LtEq || t.kind === Tok.Gt || t.kind === Tok.GtEq || t.kind === Tok.OrOr) { this.next(); const right = this.parseComparison(); return { kind: "binary", op: t.kind === Tok.OrOr ? "||" : t.text, left, right, span: { ...left.span, end: right.span.end } }; } return left; } private parseRangeLevel(): A.Expr { const left = this.parseAdditive(); const t = this.peek(); if (t.kind === Tok.DotDot || t.kind === Tok.DotDotEq) { this.next(); const end = this.parseRangeLevel(); return { kind: "range", start: left, end, inclusive: t.kind === Tok.DotDotEq, span: { ...left.span, end: end.span.end } }; } return left; } private parseAdditive(): A.Expr { const left = this.parseMultiplicative(); const t = this.peek(); if (t.kind === Tok.Plus || t.kind === Tok.Minus) { this.next(); const right = this.parseAdditive(); return { kind: "binary", op: t.text, left, right, span: { ...left.span, end: right.span.end } }; } return left; } private parseMultiplicative(): A.Expr { const left = this.parseUnary(); const t = this.peek(); if (t.kind === Tok.Star || t.kind === Tok.Slash || t.kind === Tok.Percent) { this.next(); const right = this.parseMultiplicative(); return { kind: "binary", op: t.text, left, right, span: { ...left.span, end: right.span.end } }; } return left; } private parseUnary(): A.Expr { const t = this.peek(); if (t.kind === Tok.Minus) { this.next(); const operand = this.parseUnary(); return { kind: "unary", op: "-", operand, span: { ...t.span, end: operand.span.end } }; } if (t.kind === Tok.Bang || t.kind === Tok.KwNot) { this.next(); const operand = this.parseUnary(); return { kind: "unary", op: "not", operand, span: { ...t.span, end: operand.span.end } }; } return this.parsePostfix(); } private parsePostfix(): A.Expr { let expr = this.parseAwaitLevel(); while (true) { const t = this.peek(); if (t.kind === Tok.LParen) { this.next(); const args: A.Expr[] = []; if (!this.is(Tok.RParen)) { args.push(this.parseExpr()); while (this.eat(Tok.Comma)) args.push(this.parseExpr()); } this.expect(Tok.RParen, "')'"); expr = { kind: "call", callee: expr, args, span: { ...expr.span, end: this.peek().span.start } }; continue; } if (t.kind === Tok.LBracket) { this.next(); const index = this.parseExpr(); this.expect(Tok.RBracket, "']'"); expr = { kind: "index", target: expr, index, span: { ...expr.span, end: this.peek().span.start } }; continue; } if (t.kind === Tok.Dot) { this.next(); const name = this.expectMemberName("member name"); expr = { kind: "member", target: expr, name, span: { ...expr.span, end: this.peek().span.start } }; continue; } if (t.kind === Tok.QDot) { this.next(); const name = this.expectMemberName("member name"); expr = { kind: "optaccess", target: expr, name, span: { ...expr.span, end: this.peek().span.start } }; continue; } if (t.kind === Tok.Question) { this.next(); expr = { kind: "propagate", target: expr, span: { ...expr.span, end: t.span.end } }; continue; } break; } return expr; } private parseAwaitLevel(): A.Expr { if (this.is(Tok.KwAwait)) { const start = this.next().span; const target = this.parsePostfix(); return { kind: "await", target, span: { ...start, end: target.span.end } }; } return this.parsePrimary(); } private parsePrimary(): A.Expr { const t = this.peek(); const start = t.span; switch (t.kind) { case Tok.Int: case Tok.Float: case Tok.KwTrue: case Tok.KwFalse: case Tok.Char: case Tok.StrStart: return this.parseLiteralExpr(); case Tok.KwUndefined: this.next(); return { kind: "undefined", span: start }; case Tok.KwOk: { this.next(); let value: A.Expr | null = null; if (this.is(Tok.LParen)) { this.next(); if (!this.is(Tok.RParen)) value = this.parseExpr(); this.expect(Tok.RParen, "')'"); } return { kind: "ok", value, span: { ...start, end: this.peek().span.start } }; } case Tok.KwErr: { this.next(); this.expect(Tok.LParen, "'('"); const value = this.parseExpr(); this.expect(Tok.RParen, "')'"); return { kind: "err", value, span: { ...start, end: this.peek().span.start } }; } case Tok.KwSome: { this.next(); this.expect(Tok.LParen, "'('"); const value = this.parseExpr(); this.expect(Tok.RParen, "')'"); return { kind: "some", value, span: { ...start, end: this.peek().span.start } }; } case Tok.KwNone: this.next(); return { kind: "none", span: start }; case Tok.KwRaise: { this.next(); const value = this.parseExpr(); return { kind: "raise", value, span: { ...start, end: value.span.end } }; } case Tok.KwIf: { this.next(); this.skipNls(); const cond = this.noStruct(() => this.parseExpr()); this.skipNls(); const then = this.parseBlock(); this.skipNls(); let elseExpr: A.Expr | A.Block | null = null; if (this.is(Tok.KwElse)) { this.next(); if (this.is(Tok.KwIf)) { elseExpr = this.parsePrimary(); } else { elseExpr = this.parseBlock(); } } return { kind: "if", cond, then, else: elseExpr, span: { ...start, end: (elseExpr ?? then).span.end } }; } case Tok.KwMatch: { this.next(); const value = this.noStruct(() => this.parseExpr()); this.skipNls(); const arms = this.parseMatchArms(); return { kind: "match", value, arms, span: { ...start, end: arms.length ? arms[arms.length - 1]!.span.end : this.peek().span.start } }; } case Tok.KwFun: case Tok.KwAsync: { const isAsync = this.is(Tok.KwAsync); if (isAsync) this.next(); this.expect(Tok.KwFun, "'fun'"); const params = this.parseParams(true); let retType: A.TypeExpr | null = null; if (this.eat(Tok.Arrow)) retType = this.parseTypeExpr(); const body = this.parseFunBody(); return { kind: "lambda", params, retType, body, async: isAsync, span: { ...start, end: body.span.end } }; } case Tok.LParen: { this.next(); const items: A.Expr[] = []; if (!this.is(Tok.RParen)) { items.push(this.parseExpr()); while (this.eat(Tok.Comma)) items.push(this.parseExpr()); } this.expect(Tok.RParen, "')'"); if (items.length === 1) return items[0]!; return { kind: "tuple", items, span: { ...start, end: this.peek().span.start } }; } case Tok.LBracket: { this.next(); const items: A.Expr[] = []; if (!this.is(Tok.RBracket)) { items.push(this.parseExpr()); while (this.eat(Tok.Comma)) items.push(this.parseExpr()); } this.expect(Tok.RBracket, "']'"); return { kind: "list", items, span: { ...start, end: this.peek().span.start } }; } case Tok.LBrace: { this.next(); const entries: A.MapEntry[] = []; this.skipNls(); while (!this.is(Tok.RBrace)) { if (this.is(Tok.Eof)) throw new ParseError("unterminated map literal", this.peek().span); const kstart = this.peek().span; if (this.is(Tok.StrStart)) { this.next(); const parts = this.parseStringParts(this.peek()); const text = parts.map((p) => (p.kind === "text" ? p.text : "")).join(""); this.expect(Tok.Colon, "':'"); const value = this.parseExpr(); entries.push({ key: text, value, span: kstart }); } else { const key = this.expectIdent("map key").text; this.expect(Tok.Colon, "':'"); const value = this.parseExpr(); entries.push({ key, value, span: kstart }); } if (!this.eat(Tok.Comma)) break; this.skipNls(); } this.skipNls(); this.expect(Tok.RBrace, "'}'"); return { kind: "map", entries, span: { ...start, end: this.peek().span.start } }; } case Tok.Ident: { this.next(); if (this.noStructLiteral === 0 && this.is(Tok.LBrace)) { // struct literal by name: Point { x: 1, y: 2 } this.next(); const entries: A.StructEntry[] = []; this.skipNls(); while (!this.is(Tok.RBrace)) { if (this.is(Tok.Eof)) throw new ParseError("unterminated struct literal", this.peek().span); const kstart = this.peek().span; const key = this.expectIdent("field name").text; this.expect(Tok.Colon, "':'"); const value = this.parseExpr(); entries.push({ name: key, value, span: kstart }); if (!this.eat(Tok.Comma)) break; this.skipNls(); } this.skipNls(); this.expect(Tok.RBrace, "'}'"); return { kind: "struct", name: t.text, entries, positional: false, span: { ...start, end: this.peek().span.start } }; } return { kind: "ident", name: t.text, span: t.span }; } default: throw new ParseError(`expected an expression but found '${t.text || t.kind}'`, t.span); } } private parseLiteralExpr(): A.Expr { const t = this.peek(); const start = t.span; switch (t.kind) { case Tok.Int: this.next(); return { kind: "int", value: t.value as number, text: t.text, span: start }; case Tok.Float: this.next(); return { kind: "float", value: t.value as number, text: t.text, span: start }; case Tok.KwTrue: this.next(); return { kind: "bool", value: true, span: start }; case Tok.KwFalse: this.next(); return { kind: "bool", value: false, span: start }; case Tok.Char: this.next(); return { kind: "char", value: t.value as string, span: start }; case Tok.StrStart: { this.next(); const parts: A.InterpPart[] = []; while (true) { const s = this.peek(); if (s.kind === Tok.StrPart) { this.next(); parts.push({ kind: "text", text: s.text! }); continue; } if (s.kind === Tok.StrEnd) { this.next(); break; } if (s.kind === Tok.InterpEnd) { this.next(); continue; } if (s.kind === Tok.Eof) throw new ParseError("unterminated string", start); const expr = this.parseExpr(); parts.push({ kind: "expr", expr }); } return { kind: "string", parts, span: { ...start, end: this.peek().span.start } }; } default: throw new ParseError("expected a literal", t.span); } } // ---------- types ---------- parseTypeExpr(): A.TypeExpr { let base = this.parseBaseType(); while (true) { const t = this.peek(); if (t.kind === Tok.Question) { this.next(); base = { kind: "optional", inner: base, span: { ...base.span, end: t.span.end } }; continue; } if (t.kind === Tok.Bang) { this.next(); base = { kind: "result", inner: base, span: { ...base.span, end: t.span.end } }; continue; } break; } return base; } private parseBaseType(): A.TypeExpr { const t = this.peek(); const start = t.span; if (t.kind === Tok.LParen) { this.next(); if (this.is(Tok.RParen)) { // () -> R : zero-param function type or unit... treat as function with no params const close = this.next(); if (this.is(Tok.Arrow)) { this.next(); const ret = this.parseTypeExpr(); return { kind: "fun", params: [], ret, span: { ...start, end: ret.span.end } }; } throw new ParseError("expected '->' after () in type", close.span); } // could be tuple or function type: lookahead — parse first, check for ',' or '->' const items: { name: string; type: A.TypeExpr; span: Span }[] = []; const first = this.parseTypeExpr(); if (this.is(Tok.Arrow)) { this.next(); const ret = this.parseTypeExpr(); this.expect(Tok.RParen, "')'"); return { kind: "fun", params: [{ name: "_", type: first, span: first.span }], ret, span: { ...start, end: ret.span.end } }; } items.push({ name: "_", type: first, span: first.span }); while (this.eat(Tok.Comma)) { items.push({ name: "_", type: this.parseTypeExpr(), span: this.peek().span }); } this.expect(Tok.RParen, "')'"); if (this.is(Tok.Arrow)) { this.next(); const ret = this.parseTypeExpr(); return { kind: "fun", params: items, ret, span: { ...start, end: ret.span.end } }; } return { kind: "tuple", items: items.map((i) => i.type), span: { ...start, end: this.peek().span.start } }; } if (t.kind === Tok.Ident) { this.next(); const name = t.text; const args: A.TypeExpr[] = []; if (this.is(Tok.Lt)) { this.next(); do { args.push(this.parseTypeExpr()); } while (this.eat(Tok.Comma)); this.expect(Tok.Gt, "'>'"); } return { kind: "named", name, args, span: { ...start, end: this.peek().span.start } }; } throw new ParseError(`expected a type but found '${t.text || t.kind}'`, t.span); } }