@ascent-lang/dev 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +68 -0
  2. package/dist/errors/index.d.ts +4 -0
  3. package/dist/errors/index.d.ts.map +1 -0
  4. package/dist/errors/index.js +35 -0
  5. package/dist/errors/index.js.map +1 -0
  6. package/dist/errors/types.d.ts +9 -0
  7. package/dist/errors/types.d.ts.map +1 -0
  8. package/dist/errors/types.js +5 -0
  9. package/dist/errors/types.js.map +1 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +200 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/interpreter.d.ts +35 -0
  15. package/dist/interpreter.d.ts.map +1 -0
  16. package/dist/interpreter.js +305 -0
  17. package/dist/interpreter.js.map +1 -0
  18. package/dist/lexer/chars.d.ts +6 -0
  19. package/dist/lexer/chars.d.ts.map +1 -0
  20. package/dist/lexer/chars.js +6 -0
  21. package/dist/lexer/chars.js.map +1 -0
  22. package/dist/lexer/cursor.d.ts +16 -0
  23. package/dist/lexer/cursor.d.ts.map +1 -0
  24. package/dist/lexer/cursor.js +43 -0
  25. package/dist/lexer/cursor.js.map +1 -0
  26. package/dist/lexer/index.d.ts +21 -0
  27. package/dist/lexer/index.d.ts.map +1 -0
  28. package/dist/lexer/index.js +163 -0
  29. package/dist/lexer/index.js.map +1 -0
  30. package/dist/lexer/keywords.d.ts +5 -0
  31. package/dist/lexer/keywords.d.ts.map +1 -0
  32. package/dist/lexer/keywords.js +39 -0
  33. package/dist/lexer/keywords.js.map +1 -0
  34. package/dist/lexer/token.d.ts +20 -0
  35. package/dist/lexer/token.d.ts.map +1 -0
  36. package/dist/lexer/token.js +2 -0
  37. package/dist/lexer/token.js.map +1 -0
  38. package/dist/lib.d.ts +14 -0
  39. package/dist/lib.d.ts.map +1 -0
  40. package/dist/lib.js +17 -0
  41. package/dist/lib.js.map +1 -0
  42. package/dist/parser/ast.d.ts +128 -0
  43. package/dist/parser/ast.d.ts.map +1 -0
  44. package/dist/parser/ast.js +2 -0
  45. package/dist/parser/ast.js.map +1 -0
  46. package/dist/parser/expr.d.ts +4 -0
  47. package/dist/parser/expr.d.ts.map +1 -0
  48. package/dist/parser/expr.js +277 -0
  49. package/dist/parser/expr.js.map +1 -0
  50. package/dist/parser/index.d.ts +12 -0
  51. package/dist/parser/index.d.ts.map +1 -0
  52. package/dist/parser/index.js +40 -0
  53. package/dist/parser/index.js.map +1 -0
  54. package/dist/parser/printer.d.ts +7 -0
  55. package/dist/parser/printer.d.ts.map +1 -0
  56. package/dist/parser/printer.js +130 -0
  57. package/dist/parser/printer.js.map +1 -0
  58. package/dist/parser/stmt.d.ts +7 -0
  59. package/dist/parser/stmt.d.ts.map +1 -0
  60. package/dist/parser/stmt.js +153 -0
  61. package/dist/parser/stmt.js.map +1 -0
  62. package/dist/parser/token-stream.d.ts +19 -0
  63. package/dist/parser/token-stream.d.ts.map +1 -0
  64. package/dist/parser/token-stream.js +111 -0
  65. package/dist/parser/token-stream.js.map +1 -0
  66. package/dist/parser/type-expr.d.ts +5 -0
  67. package/dist/parser/type-expr.d.ts.map +1 -0
  68. package/dist/parser/type-expr.js +54 -0
  69. package/dist/parser/type-expr.js.map +1 -0
  70. package/dist/parser/typechecker.d.ts +9 -0
  71. package/dist/parser/typechecker.d.ts.map +1 -0
  72. package/dist/parser/typechecker.js +446 -0
  73. package/dist/parser/typechecker.js.map +1 -0
  74. package/dist/parser/typed-ast.d.ts +130 -0
  75. package/dist/parser/typed-ast.d.ts.map +1 -0
  76. package/dist/parser/typed-ast.js +2 -0
  77. package/dist/parser/typed-ast.js.map +1 -0
  78. package/dist/parser/typed-printer.d.ts +3 -0
  79. package/dist/parser/typed-printer.d.ts.map +1 -0
  80. package/dist/parser/typed-printer.js +91 -0
  81. package/dist/parser/typed-printer.js.map +1 -0
  82. package/dist/types/types.d.ts +28 -0
  83. package/dist/types/types.d.ts.map +1 -0
  84. package/dist/types/types.js +48 -0
  85. package/dist/types/types.js.map +1 -0
  86. package/package.json +70 -0
  87. package/src/errors/index.ts +38 -0
  88. package/src/errors/lexical.yml +16 -0
  89. package/src/errors/name.yml +11 -0
  90. package/src/errors/syntactic.yml +66 -0
  91. package/src/errors/typechecker.yml +61 -0
  92. package/src/errors/types.ts +13 -0
  93. package/src/index.ts +213 -0
  94. package/src/interpreter.ts +332 -0
  95. package/src/lexer/chars.ts +12 -0
  96. package/src/lexer/cursor.ts +51 -0
  97. package/src/lexer/index.ts +174 -0
  98. package/src/lexer/keywords.ts +43 -0
  99. package/src/lexer/token.ts +62 -0
  100. package/src/lib.ts +33 -0
  101. package/src/parser/ast.ts +64 -0
  102. package/src/parser/expr.ts +313 -0
  103. package/src/parser/index.ts +50 -0
  104. package/src/parser/printer.ts +146 -0
  105. package/src/parser/stmt.ts +176 -0
  106. package/src/parser/token-stream.ts +121 -0
  107. package/src/parser/type-expr.ts +63 -0
  108. package/src/parser/typechecker.ts +406 -0
  109. package/src/parser/typed-ast.ts +63 -0
  110. package/src/parser/typed-printer.ts +105 -0
  111. package/src/types/types.ts +65 -0
@@ -0,0 +1,50 @@
1
+ import type { Token, Marker } from '../lexer/token.js';
2
+ import type { ArgDef, Program } from './ast.js';
3
+ import { TokenStream } from './token-stream.js';
4
+ import { parseStmt } from './stmt.js';
5
+ import { parseArgs } from './type-expr.js';
6
+
7
+ export interface ParseResult {
8
+ program: Program | null;
9
+ errorMarkers: Marker[];
10
+ }
11
+
12
+ // The program is an optional 'args (…);' header followed by a
13
+ // semicolon-separated run of statements up to EOF — the same
14
+ // "item (sep item)* close" shape as a block, with EOF standing in for
15
+ // the closing brace. `recover` is on, so a malformed statement is
16
+ // synchronized past rather than aborting the whole parse.
17
+ function parseProgram(ts: TokenStream): Program | null {
18
+ let args: ArgDef[] = [];
19
+ if (ts.peek().kind === 'KW_ARGS') {
20
+ const result = parseArgs(ts);
21
+ if (result === null) return null;
22
+ args = result;
23
+
24
+ if (ts.expect('SEMICOLON', 'S0011') === null) return null;
25
+ }
26
+
27
+ const parsed = ts.parseSeparated(() => parseStmt(ts), 'SEMICOLON', 'EOF', 'S0011', true);
28
+ if (parsed === null) return null;
29
+
30
+ return { args, stmts: parsed.items };
31
+ }
32
+
33
+ // The parser's public entry point. Phase 5 moved the grammar into
34
+ // free functions over a TokenStream (token-stream.ts, expr.ts, stmt.ts,
35
+ // type-expr.ts); this class is the thin wiring that owns the stream for
36
+ // one parse and exposes the same `new Parser(tokens).parse()` API the
37
+ // rest of the toolchain calls.
38
+ export class Parser {
39
+ private readonly tokens: Token[];
40
+
41
+ public constructor(tokens: Token[]) {
42
+ this.tokens = tokens;
43
+ }
44
+
45
+ public parse(): ParseResult {
46
+ const ts = new TokenStream(this.tokens);
47
+ const program = parseProgram(ts);
48
+ return { program, errorMarkers: ts.errors };
49
+ }
50
+ }
@@ -0,0 +1,146 @@
1
+ import chalk from 'chalk';
2
+ import type { Expr, Statement, TypeExpr } from './ast.js';
3
+ import type { RuntimeValue } from '../interpreter.js';
4
+
5
+ const formatTypeExpr = (te: TypeExpr): string => {
6
+ switch (te.kind) {
7
+ case 'TypeName': return te.name;
8
+ case 'ListType': return `List<${formatTypeExpr(te.elem)}>`;
9
+ }
10
+ };
11
+
12
+ // Returns the node as a list of lines so callers can prefix them with
13
+ // branch characters (├─, └─) when embedding inside a parent node.
14
+ const exprLines = (expr: Expr): string[] => {
15
+ switch (expr.kind) {
16
+ case 'literal':
17
+ switch (expr.valueType) {
18
+ case 'Int':
19
+ return [`${chalk.cyan('Lit')} ${chalk.yellow(String(expr.value))}`];
20
+ case 'Float':
21
+ return [`${chalk.cyan('Lit')} ${chalk.yellow(String(expr.value))}`];
22
+ case 'Bool':
23
+ return [`${chalk.cyan('Lit')} ${chalk.yellow(expr.value ? 'True' : 'False')}`];
24
+ case 'String':
25
+ return [`${chalk.cyan('Lit')} ${chalk.green(JSON.stringify(expr.value))}`];
26
+ case 'None':
27
+ return [`${chalk.cyan('Lit')} ${chalk.yellow('None')}`];
28
+ case 'Done':
29
+ return [`${chalk.cyan('Lit')} ${chalk.yellow('Done')}`];
30
+ }
31
+ case 'slot':
32
+ return [`${chalk.cyan('Slot')} ${chalk.green(expr.name)}`];
33
+ case 'call': {
34
+ const argLines = expr.args.flatMap((arg, i) =>
35
+ branch(exprLines(arg), i === expr.args.length - 1)
36
+ );
37
+ return [`${chalk.cyan('Call')} ${chalk.green(expr.callee)}`, ...argLines];
38
+ }
39
+ case 'methodCall': {
40
+ const children = [expr.receiver, ...expr.args];
41
+ const childLines = children.flatMap((child, i) =>
42
+ branch(exprLines(child), i === children.length - 1)
43
+ );
44
+ return [`${chalk.cyan('MethodCall')} ${chalk.green('.' + expr.method)}`, ...childLines];
45
+ }
46
+ case 'list': {
47
+ if (expr.elements.length === 0) {
48
+ return [`${chalk.cyan('List')} ${chalk.dim('[]')}`];
49
+ }
50
+ const elementLines = expr.elements.flatMap((el, i) =>
51
+ branch(exprLines(el), i === expr.elements.length - 1)
52
+ );
53
+ return [`${chalk.cyan('List')}`, ...elementLines];
54
+ }
55
+ case 'index': {
56
+ const listLines = branch(exprLines(expr.list), false);
57
+ const indexLines = branch(exprLines(expr.index), true);
58
+ return [`${chalk.cyan('Index')}`, ...listLines, ...indexLines];
59
+ }
60
+ case 'unary': {
61
+ const operand = branch(exprLines(expr.operand), true);
62
+ return [`${chalk.cyan('Unary')} ${chalk.magenta(expr.op)}`, ...operand];
63
+ }
64
+ case 'binary': {
65
+ const left = branch(exprLines(expr.left), false);
66
+ const right = branch(exprLines(expr.right), true);
67
+ return [`${chalk.cyan('Binary')} ${chalk.magenta(expr.op)}`, ...left, ...right];
68
+ }
69
+ case 'block': {
70
+ if (expr.stmts.length === 0) {
71
+ return [`${chalk.cyan('Block')} ${chalk.dim('(empty)')}`];
72
+ }
73
+ const lines = expr.stmts.flatMap((stmt, i) =>
74
+ branch(stmtLines(stmt), i === expr.stmts.length - 1)
75
+ );
76
+ return [`${chalk.cyan('Block')}`, ...lines];
77
+ }
78
+ case 'if': {
79
+ const condLines = branch(exprLines(expr.cond), false);
80
+ const thenLines = branch(exprLines(expr.then), expr.else === null);
81
+ const elseLines = expr.else !== null ? branch(exprLines(expr.else), true) : [];
82
+ return [`${chalk.cyan('If')}`, ...condLines, ...thenLines, ...elseLines];
83
+ }
84
+ }
85
+ };
86
+
87
+ // Prefixes a child's lines with tree-drawing characters.
88
+ // Used by parent nodes (Binary, Unary, …) when they are added.
89
+ export const branch = (lines: string[], isLast: boolean): string[] => {
90
+ const head = chalk.dim(isLast ? '└─ ' : '├─ ');
91
+ const body = chalk.dim(isLast ? ' ' : '│ ');
92
+ return lines.map((line, i) => (i === 0 ? head : body) + line);
93
+ };
94
+
95
+ export const formatExpr = (expr: Expr): string => {
96
+ return exprLines(expr).join('\n');
97
+ };
98
+
99
+ // Same line-list shape as exprLines, for the same reason: a block needs
100
+ // to embed a statement's lines and prefix them with tree-drawing chars.
101
+ const stmtLines = (stmt: Statement): string[] => {
102
+ switch (stmt.kind) {
103
+ case 'fix':
104
+ case 'mut': {
105
+ const init = branch(exprLines(stmt.init), true);
106
+ const label = stmt.kind === 'fix' ? 'Fix' : 'Mut';
107
+ const ann = stmt.typeAnnotation !== null ? chalk.dim(`: ${formatTypeExpr(stmt.typeAnnotation)}`) : '';
108
+ return [`${chalk.cyan(label)} ${chalk.green(stmt.name)}${ann}`, ...init];
109
+ }
110
+ case 'assign': {
111
+ const value = branch(exprLines(stmt.value), true);
112
+ return [`${chalk.cyan('Assign')} ${chalk.green(stmt.name)}`, ...value];
113
+ }
114
+ case 'expr':
115
+ return exprLines(stmt.expr);
116
+ case 'while': {
117
+ const cond = branch(exprLines(stmt.cond), false);
118
+ const body = branch(exprLines(stmt.body), true);
119
+ return [`${chalk.cyan('While')}`, ...cond, ...body];
120
+ }
121
+ }
122
+ };
123
+
124
+ export const formatStmt = (stmt: Statement): string => stmtLines(stmt).join('\n');
125
+
126
+ export const formatValue = (value: RuntimeValue): string => {
127
+ switch (value.type) {
128
+ case 'Int':
129
+ return chalk.yellow(String(value.value));
130
+ case 'Float':
131
+ const floatStr = String(value.value);
132
+ return floatStr.includes('.') ? chalk.yellow(floatStr) : chalk.yellow(floatStr + '.0');
133
+ case 'Bool':
134
+ return chalk.yellow(value.value ? 'True' : 'False');
135
+ case 'String':
136
+ return chalk.green(JSON.stringify(value.value));
137
+ case 'List': {
138
+ const items = value.elements.map(formatValue).join(', ');
139
+ return chalk.yellow(`[${items}]`);
140
+ }
141
+ case 'None':
142
+ return chalk.yellow('None');
143
+ case 'Done':
144
+ return chalk.yellow('Done');
145
+ }
146
+ };
@@ -0,0 +1,176 @@
1
+ import type { Token } from '../lexer/token.js';
2
+ import type { Expr, Statement, Block, If, TypeExpr } from './ast.js';
3
+ import type { TokenStream } from './token-stream.js';
4
+ import { parseExpr } from './expr.js';
5
+ import { parseTypeExpr } from './type-expr.js';
6
+
7
+ // A block is '{' stmt* '}', each statement separated by ';' — the
8
+ // same "item (sep item)* close" shape as a call's args or a list
9
+ // literal, just with SEMICOLON as the separator and RBRACE as the
10
+ // close (§ design.md: "semicolons terminate every statement"; the
11
+ // trailing one is optional exactly like a list's trailing comma).
12
+ // `openTok` lets parseRequiredBlock pass in a '{' it already consumed
13
+ // via `expect`; parseAtom, which hasn't consumed one, omits it and
14
+ // this consumes its own.
15
+ export function parseBlock(ts: TokenStream, openTok?: Token): Block | null {
16
+ openTok ??= ts.advance(); // consume '{' unless already consumed
17
+ const parsed = ts.parseSeparated(() => parseStmt(ts), 'SEMICOLON', 'RBRACE', 'S0005', true);
18
+ if (parsed === null) return null;
19
+
20
+ return { kind: 'block', stmts: parsed.items, span: { start: openTok.span.start, end: parsed.close.span.end } };
21
+ }
22
+
23
+ // The parenthesized test shared by 'if' and 'while' — '(' expr ')'.
24
+ // The body braces already delimit the construct, but the test stays
25
+ // parenthesized to match the C-family/TS surface (§5).
26
+ function parseCond(ts: TokenStream): Expr | null {
27
+ if (ts.expect('LPAREN', 'S0006') === null) return null;
28
+
29
+ const cond = parseExpr(ts);
30
+ if (cond === null) {
31
+ return null;
32
+ }
33
+
34
+ if (ts.expect('RPAREN', 'S0001') === null) return null;
35
+
36
+ return cond;
37
+ }
38
+
39
+ // A mandatory body block — every 'if'/'while' branch needs one, even
40
+ // single-statement (§2: no dangling-else, no goto-fail class of bug).
41
+ function parseRequiredBlock(ts: TokenStream): Block | null {
42
+ const openTok = ts.expect('LBRACE', 'S0007');
43
+ if (openTok === null) return null;
44
+ return parseBlock(ts, openTok);
45
+ }
46
+
47
+ // 'if (cond) { } else if (cond) { } else { }' — 'else if' is not its
48
+ // own grammar rule, it's an If recursively parsed as the else branch.
49
+ // It's an expression (see parseAtom), but it clusters with the other
50
+ // braced control constructs, so it lives here beside 'while'.
51
+ export function parseIf(ts: TokenStream): If | null {
52
+ const ifTok = ts.advance(); // consume 'if'
53
+
54
+ const cond = parseCond(ts);
55
+ if (cond === null) {
56
+ return null;
57
+ }
58
+
59
+ const thenBlock = parseRequiredBlock(ts);
60
+ if (thenBlock === null) {
61
+ return null;
62
+ }
63
+
64
+ let elseBranch: Block | If | null = null;
65
+ if (ts.peek().kind === 'KW_ELSE') {
66
+ ts.advance(); // consume 'else'
67
+ elseBranch = ts.peek().kind === 'KW_IF' ? parseIf(ts) : parseRequiredBlock(ts);
68
+ if (elseBranch === null) {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ return {
74
+ kind: 'if',
75
+ cond,
76
+ then: thenBlock,
77
+ else: elseBranch,
78
+ span: { start: ifTok.span.start, end: (elseBranch ?? thenBlock).span.end },
79
+ };
80
+ }
81
+
82
+ // 'while (cond) { }' — a statement, not an expression (§5): a loop has
83
+ // no single meaningful result, so it isn't usable where a value is
84
+ // expected the way 'if' is.
85
+ function parseWhile(ts: TokenStream): Statement | null {
86
+ const whileTok = ts.advance(); // consume 'while'
87
+
88
+ const cond = parseCond(ts);
89
+ if (cond === null) {
90
+ return null;
91
+ }
92
+
93
+ const body = parseRequiredBlock(ts);
94
+ if (body === null) {
95
+ return null;
96
+ }
97
+
98
+ return { kind: 'while', cond, body, span: { start: whileTok.span.start, end: body.span.end } };
99
+ }
100
+
101
+ // 'fix' and 'mut' share every rule but the keyword itself and the
102
+ // mutability it grants — one parse method, told which by 'kind'.
103
+ function parseDecl(ts: TokenStream, kind: 'fix' | 'mut'): Statement | null {
104
+ const kwTok = ts.advance(); // consume 'fix' or 'mut'
105
+
106
+ const nameTok = ts.peek();
107
+ if (nameTok.kind !== 'SLOT') {
108
+ ts.report('S0003', nameTok.span);
109
+ return null;
110
+ }
111
+ ts.advance(); // consume slot name
112
+
113
+ let typeAnnotation: TypeExpr | null = null;
114
+ if (ts.peek().kind === 'COLON') {
115
+ ts.advance(); // consume ':'
116
+ typeAnnotation = parseTypeExpr(ts);
117
+ if (typeAnnotation === null) return null;
118
+ }
119
+
120
+ if (ts.expect('EQUALS', 'S0004') === null) return null;
121
+
122
+ const init = parseExpr(ts);
123
+ if (init === null) {
124
+ return null;
125
+ }
126
+
127
+ return {
128
+ kind,
129
+ name: nameTok.value,
130
+ typeAnnotation,
131
+ init,
132
+ span: { start: kwTok.span.start, end: init.span.end },
133
+ };
134
+ }
135
+
136
+ // 'name = expr;' — reassigns a slot already declared with 'fix' or
137
+ // 'mut'. Whether that's actually allowed (the slot must be 'mut') is
138
+ // a name-binding rule, not a grammar rule, so it's checked at
139
+ // evaluation time (interpreter.ts), not here.
140
+ function parseAssign(ts: TokenStream): Statement | null {
141
+ const nameTok = ts.advance(); // consume slot name
142
+ ts.advance(); // consume '='
143
+
144
+ const value = parseExpr(ts);
145
+ if (value === null) {
146
+ return null;
147
+ }
148
+
149
+ return {
150
+ kind: 'assign',
151
+ name: nameTok.value,
152
+ value,
153
+ span: { start: nameTok.span.start, end: value.span.end },
154
+ };
155
+ }
156
+
157
+ export function parseStmt(ts: TokenStream): Statement | null {
158
+ if (ts.peek().kind === 'KW_FIX') {
159
+ return parseDecl(ts, 'fix');
160
+ }
161
+ if (ts.peek().kind === 'KW_MUT') {
162
+ return parseDecl(ts, 'mut');
163
+ }
164
+ if (ts.peek().kind === 'KW_WHILE') {
165
+ return parseWhile(ts);
166
+ }
167
+ if (ts.peek().kind === 'SLOT' && ts.peekNext().kind === 'EQUALS') {
168
+ return parseAssign(ts);
169
+ }
170
+
171
+ const expr = parseExpr(ts);
172
+ if (expr === null) {
173
+ return null;
174
+ }
175
+ return { kind: 'expr', expr, span: expr.span };
176
+ }
@@ -0,0 +1,121 @@
1
+ import type { Token, TokenKind, Marker, Span } from '../lexer/token.js';
2
+
3
+ // The token stream is everything the grammar productions in expr.ts,
4
+ // stmt.ts and type-expr.ts share but that isn't grammar itself: the
5
+ // cursor (peek/advance/peekNext), the error log, and the two
6
+ // stream-level combinators (expect, parseSeparated) built on top of
7
+ // them. Each production is a free function taking one of these, instead
8
+ // of a method on a monolithic Parser — so "how do I read the next
9
+ // token" lives in one place and the grammar files only describe grammar.
10
+ export class TokenStream {
11
+ private readonly tokens: Token[];
12
+ private pos: number = 0;
13
+
14
+ // The accumulated diagnostics. Productions append via report()/expect();
15
+ // the top-level parse() hands this straight out as errorMarkers.
16
+ public readonly errors: Marker[] = [];
17
+
18
+ public constructor(tokens: Token[]) {
19
+ this.tokens = tokens;
20
+ }
21
+
22
+ // The lexer guarantees the last token is always EOF, so the fallback
23
+ // is only reached if pos somehow exceeds the array — it never will.
24
+ public peek(): Token {
25
+ return this.tokens[this.pos] ?? this.tokens[this.tokens.length - 1]!;
26
+ }
27
+
28
+ public advance(): Token {
29
+ return this.tokens[this.pos++] ?? this.tokens[this.tokens.length - 1]!;
30
+ }
31
+
32
+ // Looks past the current token without consuming anything — needed to
33
+ // tell an assignment ('x = …') apart from an expression starting with
34
+ // a slot reference, which otherwise look identical for one token.
35
+ public peekNext(): Token {
36
+ return this.tokens[this.pos + 1] ?? this.tokens[this.tokens.length - 1]!;
37
+ }
38
+
39
+ // Record a diagnostic at a given span. The one place productions push
40
+ // to the error log when they need to report something expect() can't
41
+ // express (e.g. "this token was fine but the *next* thing is wrong").
42
+ public report(code: string, span: Span): void {
43
+ this.errors.push({ code, span });
44
+ }
45
+
46
+ // Consume-or-diagnose: the shape every "expect this exact token here"
47
+ // check in the grammar shares. Returns the consumed token, or records
48
+ // `code` at the offending token's span and returns null.
49
+ public expect(kind: TokenKind, code: string): Token | null {
50
+ const tok = this.peek();
51
+ if (tok.kind !== kind) {
52
+ this.report(code, tok.span);
53
+ return null;
54
+ }
55
+ return this.advance();
56
+ }
57
+
58
+ // Keywords that start a statement outright (mirrors parseStmt's own
59
+ // dispatch in stmt.ts). synchronize() treats one of these as a safe
60
+ // place to resume, since it's a far more reliable restart point than
61
+ // an arbitrary token that merely happens to also start an expression.
62
+ private static readonly STMT_START_KINDS: ReadonlySet<TokenKind> = new Set(['KW_FIX', 'KW_MUT', 'KW_WHILE']);
63
+
64
+ // Panic-mode recovery: skip tokens until the next statement boundary —
65
+ // the separator, the enclosing close token, EOF, or a statement-start
66
+ // keyword — without consuming whichever of those it lands on. Only
67
+ // parseSeparated's `recover` path calls this; it never reports a
68
+ // diagnostic itself, since the caller already recorded one for
69
+ // whatever failed before giving up and calling this.
70
+ private synchronize(sep: TokenKind, close: TokenKind): void {
71
+ while (this.peek().kind !== sep && this.peek().kind !== close && this.peek().kind !== 'EOF') {
72
+ if (TokenStream.STMT_START_KINDS.has(this.peek().kind)) return;
73
+ this.advance();
74
+ }
75
+ }
76
+
77
+ // Parses `item (sep item)* close`, allowing an empty list and a
78
+ // trailing separator right before `close`. `close` is returned
79
+ // alongside the items since callers need its span to close off the
80
+ // enclosing node.
81
+ //
82
+ // Without `recover`, a failing item or a missing close token aborts
83
+ // the whole list — the right call for a call's args or a list
84
+ // literal, where a bad element can't be recovered from without
85
+ // guessing. With `recover` (set only by parseBlock and parseProgram),
86
+ // a failing item instead calls synchronize() and keeps going, so one
87
+ // malformed statement doesn't take the rest of the file's diagnostics
88
+ // down with it. The list can still come back null if synchronize()
89
+ // runs all the way to EOF without ever finding `close`.
90
+ public parseSeparated<T>(
91
+ parseItem: () => T | null,
92
+ sep: TokenKind,
93
+ close: TokenKind,
94
+ closeCode: string,
95
+ recover = false,
96
+ ): { items: T[]; close: Token } | null {
97
+ const items: T[] = [];
98
+ if (this.peek().kind !== close) {
99
+ for (; ;) {
100
+ const item = parseItem();
101
+ if (item === null) {
102
+ if (!recover) return null;
103
+ this.synchronize(sep, close);
104
+ if (this.peek().kind === close || this.peek().kind === 'EOF') break;
105
+ if (this.peek().kind === sep) {
106
+ this.advance(); // consume the separator synchronize stopped on
107
+ if (this.peek().kind === close) break; // trailing separator after a recovered statement
108
+ }
109
+ continue;
110
+ }
111
+ items.push(item);
112
+ if (this.peek().kind !== sep) break;
113
+ this.advance(); // consume separator
114
+ if (this.peek().kind === close) break; // trailing separator
115
+ }
116
+ }
117
+ const closeTok = this.expect(close, closeCode);
118
+ if (closeTok === null) return null;
119
+ return { items, close: closeTok };
120
+ }
121
+ }
@@ -0,0 +1,63 @@
1
+ import type { TypeExpr, ArgDef, ArgType } from './ast.js';
2
+ import type { TokenStream } from './token-stream.js';
3
+
4
+ // 'Int', 'Float', 'Bool', 'String', or 'List<Type>' — used in type annotations.
5
+ export function parseTypeExpr(ts: TokenStream): TypeExpr | null {
6
+ const tok = ts.peek();
7
+ if (tok.kind !== 'TYPE_NAME') {
8
+ ts.report('S0010', tok.span);
9
+ return null;
10
+ }
11
+ ts.advance(); // consume type name
12
+
13
+ if (tok.value === 'List') {
14
+ if (ts.expect('LT', 'S0010') === null) return null;
15
+
16
+ const elem = parseTypeExpr(ts);
17
+ if (elem === null) return null;
18
+
19
+ const gt = ts.expect('GT', 'S0010');
20
+ if (gt === null) return null;
21
+
22
+ return { kind: 'ListType', elem, span: { start: tok.span.start, end: gt.span.end } };
23
+ }
24
+
25
+ const name = tok.value as 'Int' | 'Float' | 'Bool' | 'String';
26
+ return { kind: 'TypeName', name, span: tok.span };
27
+ }
28
+
29
+ // 'name: Type' — one entry in an args declaration. Unlike a slot's type
30
+ // annotation this only allows a bare type name (no 'List<…>'), so it
31
+ // reads the TYPE_NAME token directly rather than going through
32
+ // parseTypeExpr.
33
+ function parseArgDef(ts: TokenStream): ArgDef | null {
34
+ const nameTok = ts.peek();
35
+ if (nameTok.kind !== 'SLOT') {
36
+ ts.report('S0003', nameTok.span);
37
+ return null;
38
+ }
39
+ ts.advance(); // consume name
40
+
41
+ if (ts.expect('COLON', 'S0009') === null) return null;
42
+
43
+ const typeTok = ts.peek();
44
+ if (typeTok.kind !== 'TYPE_NAME') {
45
+ ts.report('S0010', typeTok.span);
46
+ return null;
47
+ }
48
+ ts.advance(); // consume type name
49
+
50
+ return { name: nameTok.value, type: typeTok.value as ArgType };
51
+ }
52
+
53
+ // 'args (name: Type, …)' — the program's typed input declaration.
54
+ export function parseArgs(ts: TokenStream): ArgDef[] | null {
55
+ ts.advance(); // consume 'args'
56
+
57
+ if (ts.expect('LPAREN', 'S0006') === null) return null;
58
+
59
+ const parsed = ts.parseSeparated(() => parseArgDef(ts), 'COMMA', 'RPAREN', 'S0001');
60
+ if (parsed === null) return null;
61
+
62
+ return parsed.items;
63
+ }