@bramas/simulant-lang 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,746 @@
1
+ /**
2
+ * Parser of `.sim` files (syntax: docs/sim-language.md).
3
+ *
4
+ * Layout follows Lean: a construct ends at a token that starts a line at or
5
+ * to the left of the construct's column (the "guard"). Declarations start
6
+ * at the left margin; items of a `where` block share a column.
7
+ */
8
+ import type { SArg, SAttribute, SBinder, SDecl, SExpr, SFieldType, SFile, SPat, SRecordItem, SStmt, SType, SWhereItem } from './ast';
9
+ import { lex, type Pos, type Span, type Token } from './lexer';
10
+
11
+ export class ParseError extends Error {
12
+ constructor(message: string, readonly span: Span) {
13
+ super(message);
14
+ }
15
+ }
16
+
17
+ const INFIX: Record<string, { prec: number; right?: boolean }> = {
18
+ '∨': { prec: 30, right: true },
19
+ '∧': { prec: 35, right: true },
20
+ '=': { prec: 50 }, '≠': { prec: 50 }, '<': { prec: 50 }, '>': { prec: 50 }, '≤': { prec: 50 }, '≥': { prec: 50 },
21
+ '++': { prec: 65 }, '+': { prec: 65 }, '-': { prec: 65 },
22
+ '*': { prec: 70 }, '/': { prec: 70 }, '%': { prec: 70 },
23
+ };
24
+
25
+ const DECL_KEYWORDS = new Set(['import', 'def', 'structure', 'inductive', 'sensor', 'task', 'goal', 'algorithm', 'experiment']);
26
+
27
+ export class Parser {
28
+ private i = 0;
29
+ private guard = 0;
30
+ /** The first token of a block item, which sits at the block's column. */
31
+ private exempt = -1;
32
+ private readonly tokens: Token[];
33
+
34
+ constructor(source: string, readonly file: string) {
35
+ this.tokens = lex(source, file);
36
+ }
37
+
38
+ // -------------------------------------------------------------------------
39
+ // Tokens
40
+
41
+ private peek(k = 0): Token {
42
+ return this.tokens[Math.min(this.i + k, this.tokens.length - 1)];
43
+ }
44
+
45
+ private next(): Token {
46
+ const t = this.peek();
47
+ if (t.kind !== 'eof') this.i++;
48
+ return t;
49
+ }
50
+
51
+ /** The current construct ends here (layout or end of file). */
52
+ private atEnd(): boolean {
53
+ if (this.i === this.exempt) return false;
54
+ const t = this.peek();
55
+ return t.kind === 'eof' || (t.lineStart && t.start.col <= this.guard);
56
+ }
57
+
58
+ private is(text: string, k = 0): boolean {
59
+ const t = this.peek(k);
60
+ return (t.kind === 'op' || t.kind === 'keyword') && t.text === text && !(k === 0 && this.atEnd());
61
+ }
62
+
63
+ private span(start: Pos, end: Pos = this.tokens[Math.max(0, this.i - 1)].end): Span {
64
+ return { file: this.file, start, end };
65
+ }
66
+
67
+ private fail(msg: string, t: Token = this.peek()): never {
68
+ throw new ParseError(msg, { file: this.file, start: t.start, end: t.end });
69
+ }
70
+
71
+ private expect(text: string): Token {
72
+ if (!this.is(text)) this.fail(`expected '${text}'${this.peek().kind === 'eof' ? ' before the end of the file' : `, found '${this.peek().text}'`}`);
73
+ return this.next();
74
+ }
75
+
76
+ private ident(): string {
77
+ const t = this.peek();
78
+ if (t.kind !== 'ident' || this.atEnd()) this.fail(`expected a name, found '${t.text || 'end of file'}'`);
79
+ return this.next().text;
80
+ }
81
+
82
+ /** Parses with a layout guard at `col`. */
83
+ private guarded<X>(col: number, f: () => X): X {
84
+ const saved = this.guard;
85
+ this.guard = col;
86
+ try {
87
+ return f();
88
+ } finally {
89
+ this.guard = saved;
90
+ }
91
+ }
92
+
93
+ // -------------------------------------------------------------------------
94
+ // Files and declarations
95
+
96
+ parseFile(): SFile {
97
+ const decls: SDecl[] = [];
98
+ while (this.peek().kind !== 'eof') decls.push(this.decl());
99
+ return { file: this.file, decls };
100
+ }
101
+
102
+ private attributes(): SAttribute[] {
103
+ const attrs: SAttribute[] = [];
104
+ while (this.is('@')) {
105
+ this.next();
106
+ this.expect('[');
107
+ do {
108
+ const name = this.ident();
109
+ const args: string[] = [];
110
+ while (this.peek().kind === 'ident') args.push(this.next().text);
111
+ attrs.push({ name, args });
112
+ } while (this.is(',') && this.next());
113
+ this.expect(']');
114
+ }
115
+ return attrs;
116
+ }
117
+
118
+ private decl(): SDecl {
119
+ const attrs = this.attributes();
120
+ const t = this.peek();
121
+ if (!t.lineStart || t.start.col !== 1) this.fail('a declaration starts at the beginning of a line');
122
+ if (t.kind !== 'keyword' || !DECL_KEYWORDS.has(t.text)) this.fail(`expected a declaration, found '${t.text}'`);
123
+ const start = t.start;
124
+ this.next();
125
+ // The declaration's body is guarded by the left margin.
126
+ return this.guarded(1, () => {
127
+ const base = { attrs };
128
+ switch (t.text) {
129
+ case 'import': {
130
+ const p = this.peek();
131
+ const path = p.kind === 'string' || p.kind === 'ident' ? this.next().text : this.fail('expected a module path');
132
+ return { ...base, d: 'import', path, span: this.span(start) };
133
+ }
134
+ case 'inductive': {
135
+ const name = this.ident();
136
+ if (this.is('where')) this.next();
137
+ const ctors: string[] = [];
138
+ while (this.is('|')) {
139
+ this.next();
140
+ ctors.push(this.ident());
141
+ }
142
+ if (!ctors.length) this.fail('an inductive type needs constructors: | a | b');
143
+ return { ...base, d: 'inductive', name, ctors, span: this.span(start) };
144
+ }
145
+ case 'structure': {
146
+ const name = this.ident();
147
+ this.expect('where');
148
+ const fields: SFieldType[] = [];
149
+ this.block(() => {
150
+ const fs = this.peek().start;
151
+ const fname = this.ident();
152
+ this.expect(':');
153
+ const type = this.type();
154
+ const def = this.is(':=') ? (this.next(), this.expr()) : undefined;
155
+ fields.push({ name: fname, type, default: def, span: this.span(fs) });
156
+ });
157
+ return { ...base, d: 'structure', name, fields, span: this.span(start) };
158
+ }
159
+ case 'def': {
160
+ const name = this.ident();
161
+ const params = this.binders();
162
+ const type = this.is(':') ? (this.next(), this.type()) : undefined;
163
+ if (this.is('where')) {
164
+ this.next();
165
+ return { ...base, d: 'def', name, params, type, items: this.whereItems(), span: this.span(start) };
166
+ }
167
+ this.expect(':=');
168
+ return { ...base, d: 'def', name, params, type, body: this.expr(), span: this.span(start) };
169
+ }
170
+ case 'sensor': case 'task': {
171
+ const name = this.ident();
172
+ const params = this.binders();
173
+ this.expect(':');
174
+ const type = this.type();
175
+ this.expect(':=');
176
+ return { ...base, d: t.text as 'sensor' | 'task', name, params, type, body: this.expr(), span: this.span(start) };
177
+ }
178
+ case 'goal': {
179
+ const name = this.ident();
180
+ this.expect(':=');
181
+ let goal: 'eventually' | 'eventuallyAlways' | 'safety';
182
+ const word = () => this.peek().kind === 'ident' ? this.peek().text : '';
183
+ if (word() === 'eventually') {
184
+ this.next();
185
+ if (word() === 'always') { this.next(); goal = 'eventuallyAlways'; } else goal = 'eventually';
186
+ const v = this.next();
187
+ if (v.text !== 'success') this.fail('expected `.success`', v);
188
+ } else if (word() === 'always') {
189
+ this.next();
190
+ this.expect('¬');
191
+ const v = this.next();
192
+ if (v.text !== 'failure') this.fail('expected `¬ .failure`', v);
193
+ goal = 'safety';
194
+ } else {
195
+ return this.fail('expected `eventually .success`, `eventually always .success` or `always ¬ .failure`');
196
+ }
197
+ return { ...base, d: 'goal', name, goal, span: this.span(start) };
198
+ }
199
+ case 'algorithm': {
200
+ const name = this.ident();
201
+ const uses: string[] = [];
202
+ if (this.is('uses')) {
203
+ this.next();
204
+ do uses.push(this.ident()); while (this.is(',') && this.next());
205
+ }
206
+ this.expect('where');
207
+ return { ...base, d: 'algorithm', name, uses, items: this.whereItems(true), span: this.span(start) };
208
+ }
209
+ case 'experiment': {
210
+ const name = this.ident();
211
+ this.expect('where');
212
+ return { ...base, d: 'experiment', name, items: this.whereItems(), span: this.span(start) };
213
+ }
214
+ }
215
+ return this.fail('unreachable');
216
+ });
217
+ }
218
+
219
+ /** Items on their own lines, indented at a common column. */
220
+ private block(item: () => void) {
221
+ const first = this.peek();
222
+ if (this.atEnd() || !first.lineStart) {
223
+ // A block may also start on the same line (one item).
224
+ if (!this.atEnd()) item();
225
+ return;
226
+ }
227
+ const col = first.start.col;
228
+ while (!this.atEnd() && this.peek().lineStart && this.peek().start.col === col) {
229
+ this.exempt = this.i;
230
+ this.guarded(col, item);
231
+ }
232
+ if (!this.atEnd()) this.fail('unexpected indentation');
233
+ }
234
+
235
+ private whereItems(algorithm = false): SWhereItem[] {
236
+ const items: SWhereItem[] = [];
237
+ this.block(() => {
238
+ const start = this.peek().start;
239
+ if (algorithm && (this.is('def') || (this.peek().text === 'main' && this.peek(1).text === ':='))) {
240
+ // `def p (x : T) := statements` or `main := statements`
241
+ const isMain = !this.is('def');
242
+ this.next();
243
+ const name = isMain ? 'main' : this.ident();
244
+ const params = isMain ? [] : this.binders();
245
+ if (this.is(':')) { this.next(); this.type(); }
246
+ this.expect(':=');
247
+ const body = this.statements();
248
+ items.push({ name, args: [], value: { t: 'nothing', span: this.span(start) }, span: this.span(start), proc: { params, body } });
249
+ return;
250
+ }
251
+ // `output claim := expression`: what tasks may read of an agent.
252
+ const output = algorithm && this.peek().text === 'output' && this.peek(1).kind === 'ident';
253
+ if (output) this.next();
254
+ if (this.is('param')) {
255
+ // `param N : Nat := domain`
256
+ this.next();
257
+ const name = this.ident();
258
+ this.expect(':');
259
+ const type = this.type();
260
+ const value: SExpr = this.is(':=') ? (this.next(), this.expr()) : { t: 'nothing', span: this.span(start) };
261
+ items.push({ name, args: [], value, span: this.span(start), param: { type } });
262
+ return;
263
+ }
264
+ // Item names may be keywords: `algorithm := ...`, `task := ...`.
265
+ const name = (output ? 'output.' : '') + (this.peek().kind === 'keyword' ? this.next().text : this.ident());
266
+ const args: string[] = [];
267
+ while (this.peek().kind === 'ident' && !this.atEnd()) args.push(this.next().text);
268
+ if (this.atEnd() && !args.length) {
269
+ // `topology` alone stands for `topology := topology`.
270
+ items.push({ name, args, value: { t: 'name', name, span: this.span(start) }, span: this.span(start) });
271
+ return;
272
+ }
273
+ this.expect(':=');
274
+ // Capitalised items hold types: `World := { site : Node }`.
275
+ const value: SExpr = /^[A-Z]/.test(name) && args.length === 0
276
+ ? (() => { const s = this.peek().start; const type = this.type(); return { t: 'typeValue', type, span: this.span(s) } as SExpr; })()
277
+ : this.expr();
278
+ items.push({ name, args, value, span: this.span(start) });
279
+ });
280
+ return items;
281
+ }
282
+
283
+ private binders(): SBinder[] {
284
+ const out: SBinder[] = [];
285
+ while (this.is('(')) {
286
+ const start = this.peek().start;
287
+ this.next();
288
+ const names: string[] = [];
289
+ while (this.peek().kind === 'ident') names.push(this.next().text);
290
+ if (!names.length) this.fail('expected parameter names');
291
+ this.expect(':');
292
+ const type = this.type();
293
+ const def = this.is(':=') ? (this.next(), this.expr()) : undefined;
294
+ this.expect(')');
295
+ for (const name of names) out.push({ name, type, default: def, span: this.span(start) });
296
+ }
297
+ return out;
298
+ }
299
+
300
+ // -------------------------------------------------------------------------
301
+ // Statements (structured algorithms)
302
+
303
+ /** A block: statements on the following lines, at a common column, or one statement on this line. */
304
+ statements(): SStmt[] {
305
+ if (this.peek().kind === 'ident' && this.peek().text === 'do' && !this.atEnd()) this.next();
306
+ const out: SStmt[] = [];
307
+ const first = this.peek();
308
+ if (this.atEnd()) this.fail('expected statements');
309
+ if (!first.lineStart) {
310
+ out.push(this.statement());
311
+ return out;
312
+ }
313
+ const col = first.start.col;
314
+ while (!this.atEnd() && this.peek().lineStart && this.peek().start.col === col) {
315
+ this.exempt = this.i;
316
+ out.push(this.guarded(col, () => this.statement()));
317
+ }
318
+ if (!this.atEnd()) this.fail('unexpected indentation');
319
+ return out;
320
+ }
321
+
322
+ /** `with { action := value, ... }` after `move`, `stay` or `halt`: the actions of that activation. */
323
+ private actions(): SRecordItem[] | undefined {
324
+ if (this.atEnd() || this.word() !== 'with') return undefined;
325
+ this.next();
326
+ const r = this.expr();
327
+ if (r.t !== 'record' || r.base || r.items.some(i => i.t !== 'field' || !i.value)) this.fail('expected { action := value, ... } after with');
328
+ return (r as { items: SRecordItem[] }).items;
329
+ }
330
+
331
+ /** The column of the `if` that starts the `else if` chain being parsed. */
332
+ private elseColumn: number | undefined;
333
+
334
+ private word(): string {
335
+ const t = this.peek();
336
+ return !this.atEnd() && (t.kind === 'ident' || t.kind === 'keyword') ? t.text : '';
337
+ }
338
+
339
+ private statement(): SStmt {
340
+ const start = this.peek().start;
341
+ const w = this.word();
342
+ const done = <S extends object>(s: S) => ({ ...s, span: this.span(start) }) as unknown as SStmt;
343
+ switch (w) {
344
+ case 'move': { this.next(); const dir = this.expr(); return done({ s: 'move', dir, actions: this.actions() }); }
345
+ case 'stay': this.next(); return done({ s: 'stay', actions: this.actions() });
346
+ case 'halt': this.next(); return done({ s: 'halt', actions: this.actions() });
347
+ case 'wait': { this.next(); return done({ s: 'wait', count: this.expr() }); }
348
+ case 'repeat': this.next(); return done({ s: 'repeat', body: this.statements() });
349
+ case 'while': {
350
+ this.next();
351
+ // The condition ends with its line (or at `do`): the body follows on the next lines.
352
+ const cond = this.guarded(Number.MAX_SAFE_INTEGER, () => this.expr());
353
+ if (this.word() === 'do') this.next();
354
+ return done({ s: 'while', cond, body: this.statements() });
355
+ }
356
+ case 'if': {
357
+ // In `else if` chains, every `else` sits at the column of the chain's first `if`.
358
+ const column = this.elseColumn ?? start.col;
359
+ this.elseColumn = undefined;
360
+ this.next();
361
+ const cond = this.expr();
362
+ this.expect('then');
363
+ const then = this.statements();
364
+ let otherwise: SStmt[] | undefined;
365
+ // `else` may sit at the column of its `if`.
366
+ const t = this.peek();
367
+ if (t.kind === 'keyword' && t.text === 'else' && (!t.lineStart || t.start.col >= column)) {
368
+ this.next();
369
+ if (this.word() === 'if' && !this.peek().lineStart) {
370
+ this.elseColumn = column;
371
+ otherwise = [this.statement()];
372
+ } else {
373
+ otherwise = this.statements();
374
+ }
375
+ }
376
+ return done({ s: 'if', cond, then, ...(otherwise ? { else: otherwise } : {}) });
377
+ }
378
+ case 'let': {
379
+ this.next();
380
+ const name = this.ident();
381
+ const type = this.is(':') ? (this.next(), this.type()) : undefined;
382
+ this.expect(':=');
383
+ return done({ s: 'let', name, type, value: this.expr() });
384
+ }
385
+ }
386
+ if (this.peek().kind === 'ident' && this.peek(1).text === ':=') {
387
+ const name = this.next().text;
388
+ this.next();
389
+ return done({ s: 'assign', name, value: this.expr() });
390
+ }
391
+ if (this.peek().kind === 'ident') {
392
+ // A procedure call: `cautiousWalk 0 1 .cw`
393
+ const name = this.next().text;
394
+ const args: SExpr[] = [];
395
+ while (this.canStartArgument()) args.push(this.postfix());
396
+ return done({ s: 'call', name, args });
397
+ }
398
+ return this.fail('expected a statement: move, stay, halt, wait, repeat, while, if, let, an assignment or a procedure call');
399
+ }
400
+
401
+ // -------------------------------------------------------------------------
402
+ // Types
403
+
404
+ type(): SType {
405
+ const start = this.peek().start;
406
+ const head = this.typeAtom();
407
+ if (head.t !== 'name') return head;
408
+ const args: SType[] = [];
409
+ while (!this.atEnd() && (this.peek().kind === 'ident' || this.is('(') || this.is('{'))) args.push(this.typeAtom());
410
+ return args.length ? { ...head, args, span: this.span(start) } : head;
411
+ }
412
+
413
+ private typeAtom(): SType {
414
+ const start = this.peek().start;
415
+ if (this.is('(')) {
416
+ this.next();
417
+ const t = this.type();
418
+ this.expect(')');
419
+ return t;
420
+ }
421
+ if (this.is('{')) {
422
+ this.next();
423
+ const fields: SFieldType[] = [];
424
+ while (!this.is('}')) {
425
+ const fs = this.peek().start;
426
+ const name = this.ident();
427
+ this.expect(':');
428
+ const type = this.type();
429
+ const def = this.is(':=') ? (this.next(), this.expr()) : undefined;
430
+ fields.push({ name, type, default: def, span: this.span(fs) });
431
+ if (!this.is('}')) this.expect(',');
432
+ }
433
+ this.expect('}');
434
+ return { t: 'record', fields, span: this.span(start) };
435
+ }
436
+ return { t: 'name', name: this.ident(), args: [], span: this.span(start) };
437
+ }
438
+
439
+ // -------------------------------------------------------------------------
440
+ // Expressions
441
+
442
+ expr(minPrec = 0): SExpr {
443
+ const start = this.peek().start;
444
+ let left = this.prefix();
445
+ for (;;) {
446
+ const t = this.peek();
447
+ if (this.atEnd() || t.kind !== 'op') break;
448
+ const info = INFIX[t.text];
449
+ if (!info || info.prec < minPrec) break;
450
+ this.next();
451
+ const right = this.expr(info.right ? info.prec : info.prec + 1);
452
+ left = { t: 'binop', op: t.text, l: left, r: right, span: this.span(start) };
453
+ }
454
+ return left;
455
+ }
456
+
457
+ private prefix(): SExpr {
458
+ const start = this.peek().start;
459
+ if (this.atEnd()) this.fail(this.peek().kind === 'eof' ? 'unexpected end of the file' : 'expected an expression (check the indentation)');
460
+ if (this.is('fun')) {
461
+ this.next();
462
+ const params: { name: string; type?: SType }[] = [];
463
+ while (!this.is('=>')) {
464
+ if (this.is('(')) {
465
+ this.next();
466
+ const names: string[] = [];
467
+ while (this.peek().kind === 'ident') names.push(this.next().text);
468
+ this.expect(':');
469
+ const type = this.type();
470
+ this.expect(')');
471
+ for (const name of names) params.push({ name, type });
472
+ } else {
473
+ params.push({ name: this.ident() });
474
+ }
475
+ }
476
+ this.next();
477
+ return { t: 'fun', params, body: this.expr(), span: this.span(start) };
478
+ }
479
+ if (this.is('let')) {
480
+ const col = this.peek().start.col;
481
+ this.next();
482
+ const name = this.ident();
483
+ const type = this.is(':') ? (this.next(), this.type()) : undefined;
484
+ this.expect(':=');
485
+ const value = this.guarded(col, () => this.expr());
486
+ if (this.is(';')) this.next();
487
+ return { t: 'let', name, type, value, body: this.expr(), span: this.span(start) };
488
+ }
489
+ if (this.is('if')) {
490
+ this.next();
491
+ const cond = this.expr();
492
+ this.expect('then');
493
+ const then = this.expr();
494
+ this.expect('else');
495
+ return { t: 'if', cond, then, else: this.expr(), span: this.span(start) };
496
+ }
497
+ if (this.is('match')) {
498
+ this.next();
499
+ const on = this.expr();
500
+ this.expect('with');
501
+ const cases: { pat: SPat; body: SExpr }[] = [];
502
+ while (this.is('|')) {
503
+ const col = this.peek().start.col;
504
+ this.next();
505
+ const pat = this.pattern();
506
+ this.expect('=>');
507
+ // The body continues on lines indented more than its `|`.
508
+ cases.push({ pat, body: this.guarded(col, () => this.expr()) });
509
+ }
510
+ if (!cases.length) this.fail('expected `| pattern => value`');
511
+ return { t: 'match', on, cases, span: this.span(start) };
512
+ }
513
+ if (this.is('choose')) {
514
+ this.next();
515
+ const from = this.application();
516
+ let such: SExpr | undefined, labels: SExpr | undefined;
517
+ for (;;) {
518
+ if (this.is('such')) { this.next(); this.expect('that'); such = this.expr(); continue; }
519
+ if (this.is('labels')) { this.next(); labels = this.is('fun') ? this.prefix() : this.application(); continue; }
520
+ break;
521
+ }
522
+ return { t: 'choose', from, such, labels, span: this.span(start) };
523
+ }
524
+ if (this.is('¬')) {
525
+ this.next();
526
+ return { t: 'unop', op: '¬', e: this.expr(40), span: this.span(start) };
527
+ }
528
+ if (this.is('-')) {
529
+ this.next();
530
+ return { t: 'unop', op: '-', e: this.expr(75), span: this.span(start) };
531
+ }
532
+ return this.application();
533
+ }
534
+
535
+ private canStartArgument(): boolean {
536
+ if (this.atEnd()) return false;
537
+ const t = this.peek();
538
+ if (t.kind === 'ident' && t.text === 'do') return false;
539
+ if (t.kind === 'ident' || t.kind === 'dotIdent' || t.kind === 'number' || t.kind === 'string') return true;
540
+ if (t.kind === 'keyword') return ['true', 'false', 'param', 'nothing', 'fun'].includes(t.text);
541
+ return t.kind === 'op' && ['(', '[', '{', '!', '·'].includes(t.text);
542
+ }
543
+
544
+ private application(): SExpr {
545
+ const start = this.peek().start;
546
+ const fn = this.postfix();
547
+ const args: SArg[] = [];
548
+ while (this.canStartArgument()) {
549
+ // A trailing `fun` is the last argument: `xs.filter fun b => ...`
550
+ if (this.is('fun')) {
551
+ args.push({ value: this.prefix() });
552
+ break;
553
+ }
554
+ // Named argument: `(faults := [...])`
555
+ if (this.is('(') && this.peek(1).kind === 'ident' && this.peek(2).text === ':=') {
556
+ this.next();
557
+ const name = this.next().text;
558
+ this.next();
559
+ const value = this.expr();
560
+ this.expect(')');
561
+ args.push({ name, value });
562
+ } else {
563
+ args.push({ value: this.postfix() });
564
+ }
565
+ }
566
+ return args.length ? { t: 'app', fn, args, span: this.span(start) } : fn;
567
+ }
568
+
569
+ private postfix(): SExpr {
570
+ const start = this.peek().start;
571
+ let e = this.atom();
572
+ while (this.peek().kind === 'field' && !this.peek().spaced && !this.atEnd()) {
573
+ e = { t: 'proj', on: e, field: this.next().text, span: this.span(start) };
574
+ }
575
+ return e;
576
+ }
577
+
578
+ private atom(): SExpr {
579
+ const t = this.peek();
580
+ const start = t.start;
581
+ if (t.kind === 'number') { this.next(); return { t: 'num', value: Number(t.text), span: this.span(start) }; }
582
+ if (t.kind === 'string') { this.next(); return { t: 'str', value: t.text, span: this.span(start) }; }
583
+ if (t.kind === 'ident') { this.next(); return { t: 'name', name: t.text, span: this.span(start) }; }
584
+ if (t.kind === 'dotIdent') { this.next(); return { t: 'ctor', name: t.text, span: this.span(start) }; }
585
+ if (t.kind === 'keyword') {
586
+ if (t.text === 'true' || t.text === 'false') { this.next(); return { t: 'bool', value: t.text === 'true', span: this.span(start) }; }
587
+ if (t.text === 'param') { this.next(); return { t: 'param', name: this.ident(), span: this.span(start) }; }
588
+ if (t.text === 'nothing') { this.next(); return { t: 'nothing', span: this.span(start) }; }
589
+ if (t.text === 'flag') return this.flagDecl();
590
+ if (t.text === 'action') return this.actionDecl();
591
+ }
592
+ if (t.kind === 'op') {
593
+ // As in Lean (`!b` is `notation:max "!" b:40`): `!v.at i` is `!(v.at i)`, `!a ∧ b` is `(!a) ∧ b`.
594
+ if (t.text === '!') { this.next(); return { t: 'unop', op: '!', e: this.expr(40), span: this.span(start) }; }
595
+ if (t.text === '·') { this.next(); return { t: 'hole', span: this.span(start) }; }
596
+ if (t.text === '(') return this.paren();
597
+ if (t.text === '[') {
598
+ this.next();
599
+ const items: SExpr[] = [];
600
+ this.guarded(0, () => {
601
+ while (!this.is(']')) {
602
+ items.push(this.expr());
603
+ if (!this.is(']')) this.expect(',');
604
+ }
605
+ });
606
+ this.expect(']');
607
+ return { t: 'list', items, span: this.span(start) };
608
+ }
609
+ if (t.text === '{') return this.record();
610
+ }
611
+ return this.fail(`unexpected '${t.text || 'end of file'}'`);
612
+ }
613
+
614
+ /** `( e )`, `( e : T )`, or a section `( · ≥ 3 )`. */
615
+ private paren(): SExpr {
616
+ const start = this.peek().start;
617
+ this.next();
618
+ if (this.is(')')) {
619
+ this.next();
620
+ return { t: 'unit', span: this.span(start) };
621
+ }
622
+ const inner = this.guarded(0, () => this.expr());
623
+ let e: SExpr = inner;
624
+ if (this.is(':')) {
625
+ this.next();
626
+ const type = this.guarded(0, () => this.type());
627
+ e = { t: 'ascribe', e: inner, type, span: this.span(start) };
628
+ }
629
+ this.expect(')');
630
+ return hasHole(e) ? { t: 'fun', params: [{ name: '·' }], body: fillHole(e), span: this.span(start) } : e;
631
+ }
632
+
633
+ private record(): SExpr {
634
+ const start = this.peek().start;
635
+ this.next();
636
+ return this.guarded(0, () => {
637
+ let base: SExpr | undefined;
638
+ // `{ e with ... }`: look for `with` before the first `:=` or `,`.
639
+ let j = this.i, depth = 0;
640
+ for (; j < this.tokens.length; j++) {
641
+ const x = this.tokens[j];
642
+ if (['(', '[', '{'].includes(x.text) && x.kind === 'op') depth++;
643
+ else if ([')', ']', '}'].includes(x.text) && x.kind === 'op') { if (depth === 0) break; depth--; }
644
+ else if (depth === 0 && (x.text === ':=' || x.text === ',')) break;
645
+ else if (depth === 0 && x.text === 'with' && x.kind === 'keyword') {
646
+ base = this.expr();
647
+ this.expect('with');
648
+ break;
649
+ }
650
+ }
651
+ const items: SRecordItem[] = [];
652
+ while (!this.is('}')) {
653
+ const is = this.peek().start;
654
+ if (this.is('raise')) {
655
+ this.next();
656
+ const flag = this.ident();
657
+ this.expect(':=');
658
+ items.push({ t: 'raise', flag, value: this.expr(), span: this.span(is) });
659
+ } else {
660
+ const name = this.ident();
661
+ const value = this.is(':=') ? (this.next(), this.expr()) : undefined;
662
+ items.push({ t: 'field', name, value, span: this.span(is) });
663
+ }
664
+ if (!this.is('}')) this.expect(',');
665
+ }
666
+ this.expect('}');
667
+ return { t: 'record', base, items, span: this.span(start) } as SExpr;
668
+ });
669
+ }
670
+
671
+ /** `flag dead hidden, acts never` */
672
+ private flagDecl(): SExpr {
673
+ const start = this.peek().start;
674
+ this.next();
675
+ const name = this.ident();
676
+ const vis = this.ident();
677
+ if (vis !== 'visible' && vis !== 'hidden') this.fail('expected `visible` or `hidden`');
678
+ let acts: 'never' | 'adversary' = 'never';
679
+ if (this.is(',') && this.peek(1).text === 'acts') {
680
+ this.next();
681
+ this.next();
682
+ const a = this.ident();
683
+ if (a !== 'never' && a !== 'adversary') this.fail('expected `never` or `adversary`');
684
+ acts = a;
685
+ }
686
+ return { t: 'flag', name, visibility: vis, acts, span: this.span(start) };
687
+ }
688
+
689
+ /** `action pebble : PebbleAction := .none` */
690
+ private actionDecl(): SExpr {
691
+ const start = this.peek().start;
692
+ this.next();
693
+ const name = this.ident();
694
+ this.expect(':');
695
+ const type = this.type();
696
+ this.expect(':=');
697
+ return { t: 'action', name, type, default: this.postfix(), span: this.span(start) };
698
+ }
699
+
700
+ private pattern(): SPat {
701
+ const t = this.peek();
702
+ const span = { file: this.file, start: t.start, end: t.end };
703
+ if (t.kind === 'dotIdent') { this.next(); return { t: 'ctor', name: t.text, span }; }
704
+ if (t.kind === 'keyword' && (t.text === 'true' || t.text === 'false')) { this.next(); return { t: 'bool', value: t.text === 'true', span }; }
705
+ if (t.kind === 'ident') {
706
+ this.next();
707
+ if (t.text === '_') return { t: 'wild', span };
708
+ if (t.text === 'none') return { t: 'none', span };
709
+ if (t.text === 'some') return { t: 'some', name: this.ident(), span };
710
+ return { t: 'ctor', name: t.text, span };
711
+ }
712
+ return this.fail('expected a pattern');
713
+ }
714
+ }
715
+
716
+ function hasHole(e: SExpr): boolean {
717
+ return e.t === 'hole' || children(e).some(hasHole);
718
+ }
719
+
720
+ function children(e: SExpr): SExpr[] {
721
+ switch (e.t) {
722
+ case 'proj': return [e.on];
723
+ case 'app': return [e.fn, ...e.args.map(a => a.value)];
724
+ case 'binop': return [e.l, e.r];
725
+ case 'unop': return [e.e];
726
+ case 'ascribe': return [e.e];
727
+ default: return [];
728
+ }
729
+ }
730
+
731
+ /** Replaces the hole of a section by the parameter `·`. */
732
+ function fillHole(e: SExpr): SExpr {
733
+ switch (e.t) {
734
+ case 'hole': return { t: 'name', name: '·', span: e.span };
735
+ case 'proj': return { ...e, on: fillHole(e.on) };
736
+ case 'app': return { ...e, fn: fillHole(e.fn), args: e.args.map(a => ({ ...a, value: fillHole(a.value) })) };
737
+ case 'binop': return { ...e, l: fillHole(e.l), r: fillHole(e.r) };
738
+ case 'unop': return { ...e, e: fillHole(e.e) };
739
+ case 'ascribe': return { ...e, e: fillHole(e.e) };
740
+ default: return e;
741
+ }
742
+ }
743
+
744
+ export function parse(source: string, file = '<input>'): SFile {
745
+ return new Parser(source, file).parseFile();
746
+ }