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