@bramas/simulant-lang 0.0.0-stage → 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.
@@ -0,0 +1,1094 @@
1
+ /**
2
+ * Elaboration of `.sim` declarations to IR (plan, Section 3).
3
+ *
4
+ * Two levels:
5
+ * - definitions whose type is a model piece (`Component`, `Model`, `Sensor`,
6
+ * `Topology`, `FrameModel`, `Round`, `Memory`, or lists of them) are
7
+ * evaluated at compile time, with their arguments: model families are
8
+ * functions;
9
+ * - expressions inside hooks, sensors, tasks, algorithms, initial values and
10
+ * parameter domains are translated to IR expressions. A component's
11
+ * parameters stay symbolic in its expressions.
12
+ *
13
+ * Constructors, `none` and `[]` become literals of type `infer`, resolved by
14
+ * the checker once the experiment is assembled (`resolve.ts`).
15
+ */
16
+ import { BUILTINS } from "../builtins.js";
17
+ import { check } from "../check.js";
18
+ import { IR_VERSION } from "../ir.js";
19
+ import { Parser } from "../parse/parser.js";
20
+ import { fillActionDefaults, lower } from "./lower.js";
21
+ import { resolveTypes } from "../resolve.js";
22
+ import { T } from "../types.js";
23
+ import { Program } from "./program.js";
24
+ export class ElabError extends Error {
25
+ span;
26
+ constructor(message, span) {
27
+ super(message);
28
+ this.span = span;
29
+ }
30
+ }
31
+ const fail = (msg, span) => { throw new ElabError(msg, span); };
32
+ const ROUND = T.enum('Round', ['fsync', 'ssync']);
33
+ const MEMORY = T.enum('Memory', ['persistent', 'oblivious']);
34
+ const SYMMETRIES = T.enum('Symmetries', ['derived', 'none']);
35
+ const META_TYPES = new Set(['Component', 'Model', 'Sensor', 'Topology', 'FrameModel', 'Round', 'Memory', 'Symmetries']);
36
+ function isMetaType(t) {
37
+ if (!t || t.t !== 'name')
38
+ return false;
39
+ return META_TYPES.has(t.name) || (t.name === 'List' && isMetaType(t.args[0]));
40
+ }
41
+ /** A function on values (`def f (x : T) : R := e`), as opposed to a model-level definition. */
42
+ function isObjectDef(d) {
43
+ return !!d && d.d === 'def' && !!d.body && !!d.type && !isMetaType(d.type);
44
+ }
45
+ const COLLECTION_METHODS_WITH_FN = new Set(['filter', 'map', 'any', 'all', 'count']);
46
+ const BUILTIN_FUNCTIONS = new Set([
47
+ 'present', 'correct', 'decisionOf', 'target', 'advance', 'edgePresent', 'offset', 'toGlobal', 'node', 'range', 'subsets',
48
+ 'subsetsUpTo', 'ofFn', 'show', 'min', 'max', 'some', 'opposite', 'toInt',
49
+ ]);
50
+ /** Builtins whose arguments at these positions are functions. */
51
+ const FUNCTION_ARGS = { filter: [1], map: [1], any: [1], all: [1], count: [1], ofFn: [1] };
52
+ const BINOPS = {
53
+ '=': 'eq', '≠': 'ne', '<': 'lt', '>': 'gt', '≤': 'le', '≥': 'ge', '+': 'add', '-': 'sub', '*': 'mul', '/': 'div', '%': 'mod',
54
+ };
55
+ /** What a declaration is, in `Experiment.source` (model-level definitions by their type). */
56
+ function declKind(d) {
57
+ switch (d.d) {
58
+ case 'experiment':
59
+ case 'algorithm':
60
+ case 'task':
61
+ case 'sensor': return d.d;
62
+ case 'def': {
63
+ const t = d.type?.t === 'name' ? d.type.name : undefined;
64
+ return t === 'Component' ? 'component' : t === 'Model' ? 'model' : t === 'Sensor' ? 'sensor' : 'definition';
65
+ }
66
+ }
67
+ return undefined;
68
+ }
69
+ export class Elaborator {
70
+ program;
71
+ /** Source spans of IR expressions, for diagnostics. */
72
+ spans = new WeakMap();
73
+ diagnostics = [];
74
+ globals = new Map();
75
+ enums = new Map();
76
+ structures = new Map();
77
+ goals = new Map();
78
+ /** Object-level definitions, elaborated once, in dependency order, with the definitions they call. */
79
+ objectDefs = new Map();
80
+ /** The definitions called by what is being elaborated (they go with it into the IR). */
81
+ calledDefs;
82
+ /** The declarations the experiment being built uses, for `Experiment.source` (display only). */
83
+ uses;
84
+ constructor(program) {
85
+ this.program = program;
86
+ this.diagnostics.push(...program.diagnostics);
87
+ for (const d of program.decls()) {
88
+ if (d.d === 'import')
89
+ continue;
90
+ if (d.d === 'goal') {
91
+ this.goals.set(d.name, { goal: d.goal, span: d.span });
92
+ continue;
93
+ }
94
+ if (this.globals.has(d.name)) {
95
+ this.diagnostics.push({ message: `${d.name} is declared twice`, span: d.span });
96
+ continue;
97
+ }
98
+ this.globals.set(d.name, d);
99
+ if (d.d === 'inductive')
100
+ this.enums.set(d.name, T.enum(d.name, d.ctors));
101
+ if (d.d === 'structure')
102
+ this.structures.set(d.name, d);
103
+ }
104
+ }
105
+ /** Evaluates a model-level expression, such as `Hazards.BlackHole .mayEmerge`. */
106
+ evaluate(text) {
107
+ const v = this.meval(new Parser(text, '<expression>').expr(), new Map());
108
+ if (v.m === 'component' || v.m === 'model' || v.m === 'sensor')
109
+ return v.ir;
110
+ return fail(`${text} is not a component, a model or a sensor`);
111
+ }
112
+ /** The experiments declared in the entry module. */
113
+ experiments() {
114
+ const entry = this.program.modules.get(this.program.entry);
115
+ return (entry?.decls ?? []).filter(d => d.d === 'experiment').map(d => d.name);
116
+ }
117
+ // -------------------------------------------------------------------------
118
+ // Types
119
+ type(t) {
120
+ if (t.t === 'record')
121
+ return T.record(t.fields.map(f => ({ name: f.name, type: this.type(f.type) })));
122
+ const arg = (i = 0) => t.args[i] ? this.type(t.args[i]) : fail(`${t.name} needs a type argument`, t.span);
123
+ switch (t.name) {
124
+ case 'Nat': return T.nat;
125
+ case 'Int': return T.int;
126
+ case 'Bool': return T.bool;
127
+ case 'String': return T.string;
128
+ case 'Unit': return T.unit;
129
+ case 'Node': return T.node;
130
+ case 'Agent': return T.agent;
131
+ case 'Move': return T.move;
132
+ case 'Frame': return T.frame;
133
+ case 'Config': return T.config;
134
+ case 'Verdict': return T.verdict;
135
+ case 'Pub': return T.pub;
136
+ case 'Option': return T.option(arg());
137
+ case 'List': return T.list(arg());
138
+ case 'Multiset': return T.multiset(arg());
139
+ }
140
+ const e = this.enums.get(t.name);
141
+ if (e)
142
+ return e;
143
+ const s = this.structures.get(t.name);
144
+ if (s)
145
+ return T.record(s.fields.map(f => ({ name: f.name, type: this.type(f.type) })));
146
+ return fail(`unknown type ${t.name}`, t.span);
147
+ }
148
+ enumType(t) {
149
+ if (!t || t.t !== 'name')
150
+ return undefined;
151
+ if (t.name === 'Round')
152
+ return ROUND;
153
+ if (t.name === 'Memory')
154
+ return MEMORY;
155
+ if (t.name === 'Symmetries')
156
+ return SYMMETRIES;
157
+ return this.enums.get(t.name);
158
+ }
159
+ // -------------------------------------------------------------------------
160
+ // Meta level
161
+ meval(e, env, expected) {
162
+ switch (e.t) {
163
+ case 'num': return { m: 'nat', v: e.value };
164
+ case 'bool': return { m: 'bool', v: e.value };
165
+ case 'str': return { m: 'str', v: e.value };
166
+ case 'ctor': {
167
+ const t = this.enumType(expected) ?? this.uniqueEnum(e.name, e.span);
168
+ if (!t.ctors.includes(e.name))
169
+ fail(`.${e.name} is not a constructor of ${t.name}`, e.span);
170
+ return { m: 'enum', type: t, ctor: e.name };
171
+ }
172
+ case 'list': {
173
+ const elem = expected?.t === 'name' && expected.name === 'List' ? expected.args[0] : undefined;
174
+ return { m: 'list', items: e.items.map(i => this.meval(i, env, elem)) };
175
+ }
176
+ case 'binop': {
177
+ if (e.op === '++') {
178
+ const [a, b] = [this.meval(e.l, env, expected), this.meval(e.r, env, expected)];
179
+ if (a.m !== 'list' || b.m !== 'list')
180
+ fail('++ joins lists', e.span);
181
+ return { m: 'list', items: [...a.items, ...b.items] };
182
+ }
183
+ if (e.op === '=' || e.op === '≠') {
184
+ const a = this.meval(e.l, env);
185
+ const b = this.meval(e.r, env, a.m === 'enum' ? { t: 'name', name: a.type.name, args: [], span: e.span } : undefined);
186
+ const same = JSON.stringify(stripType(a)) === JSON.stringify(stripType(b));
187
+ return { m: 'bool', v: e.op === '=' ? same : !same };
188
+ }
189
+ if (e.op === '∧' || e.op === '∨') {
190
+ const a = this.bool(this.meval(e.l, env), e.l.span), b = this.bool(this.meval(e.r, env), e.r.span);
191
+ return { m: 'bool', v: e.op === '∧' ? a && b : a || b };
192
+ }
193
+ return fail(`${e.op} cannot be evaluated when building a model`, e.span);
194
+ }
195
+ case 'if':
196
+ return this.bool(this.meval(e.cond, env), e.cond.span) ? this.meval(e.then, env, expected) : this.meval(e.else, env, expected);
197
+ case 'ascribe':
198
+ return this.meval(e.e, env, e.type);
199
+ case 'name':
200
+ return env.get(e.name) ?? this.metaCall(e.name, [], e.span, env);
201
+ case 'app': {
202
+ if (e.fn.t !== 'name')
203
+ fail('expected the name of a definition', e.fn.span);
204
+ return this.metaCall(e.fn.name, e.args, e.span, env);
205
+ }
206
+ default:
207
+ return fail('this expression cannot be evaluated when building a model', e.span);
208
+ }
209
+ }
210
+ bool(v, span) {
211
+ return v.m === 'bool' ? v.v : fail('expected a Bool', span);
212
+ }
213
+ uniqueEnum(ctor, span) {
214
+ const all = [...this.enums.values(), ROUND, MEMORY, SYMMETRIES].filter(t => t.ctors.includes(ctor));
215
+ if (all.length === 1)
216
+ return all[0];
217
+ return fail(all.length ? `ambiguous constructor .${ctor}: ${all.map(t => t.name).join(' or ')}` : `unknown constructor .${ctor}`, span);
218
+ }
219
+ /** Calls a definition, a sensor, or a builtin of the model level. */
220
+ metaCall(name, args, span, env) {
221
+ switch (name) {
222
+ case 'Ring': return { m: 'topology', ir: { kind: 'ring', size: { e: 'param', name: 'N' } } };
223
+ case 'DynamicRing': {
224
+ const k = args.length ? this.meval(args[0].value, env) : { m: 'nat', v: 1 };
225
+ if (k.m !== 'nat')
226
+ fail('DynamicRing takes a number of edges', span);
227
+ return { m: 'topology', ir: { kind: 'dynamicRing', size: { e: 'param', name: 'N' }, k: k.v } };
228
+ }
229
+ case 'Frames.common': return { m: 'frame', ir: { kind: 'common' } };
230
+ case 'Frames.dynamicReflection': return { m: 'frame', ir: { kind: 'dynamicReflection' } };
231
+ case 'Frames.persistent': {
232
+ const f = args[0]?.value;
233
+ if (!f || f.t !== 'name')
234
+ fail('Frames.persistent takes the name of an agent field of type Frame', span);
235
+ return { m: 'frame', ir: { kind: 'persistent', field: f.name } };
236
+ }
237
+ }
238
+ const decl = this.globals.get(name) ?? fail(`unknown name ${name}`, span);
239
+ this.use(decl);
240
+ if (decl.d === 'sensor')
241
+ return { m: 'sensor', ir: this.sensor(decl, this.bindArgs(decl.params.filter(p => !runtimeBinder(p.type)), args, env, span)) };
242
+ if (decl.d !== 'def' || !isMetaType(decl.type))
243
+ return fail(`${name} cannot be used when building a model`, span);
244
+ const bound = this.bindArgs(decl.params, args, env, span);
245
+ const t = decl.type;
246
+ if (decl.body)
247
+ return this.meval(decl.body, bound, decl.type);
248
+ if (t.name === 'Component')
249
+ return { m: 'component', ir: this.component(decl, bound) };
250
+ if (t.name === 'Model')
251
+ return { m: 'model', ir: this.model(decl, bound) };
252
+ return fail(`a ${t.name} is defined by a value (:=), not a where block`, decl.span);
253
+ }
254
+ bindArgs(params, args, env, span) {
255
+ const bound = new Map();
256
+ const positional = args.filter(a => !a.name);
257
+ let next = 0;
258
+ for (const p of params) {
259
+ const named = args.find(a => a.name === p.name);
260
+ const arg = named?.value ?? positional[next++]?.value;
261
+ if (arg)
262
+ bound.set(p.name, this.meval(arg, env, p.type));
263
+ else if (p.default)
264
+ bound.set(p.name, this.meval(p.default, bound, p.type));
265
+ else
266
+ fail(`missing argument ${p.name}`, span);
267
+ }
268
+ if (next < positional.length)
269
+ fail('too many arguments', span);
270
+ for (const a of args)
271
+ if (a.name && !params.some(p => p.name === a.name))
272
+ fail(`no parameter named ${a.name}`, a.value.span);
273
+ return bound;
274
+ }
275
+ /** A meta value as an IR literal (component arguments, sensor parameters). */
276
+ json(v, span) {
277
+ switch (v.m) {
278
+ case 'nat':
279
+ case 'bool':
280
+ case 'str': return v.v;
281
+ case 'enum': return v.ctor;
282
+ case 'list': return v.items.map(i => this.json(i, span));
283
+ default: return fail('expected a value', span);
284
+ }
285
+ }
286
+ equivariance(d) {
287
+ const a = d.attrs.find(a => a.name === 'equivariant');
288
+ return a && { rotations: a.args.includes('rotations'), reflections: a.args.includes('reflections') };
289
+ }
290
+ // -------------------------------------------------------------------------
291
+ // Components, sensors, models
292
+ component(decl, args) {
293
+ const name = decl.name.split('.').pop();
294
+ const items = new Map(decl.items.map(i => [i.name + (i.args.length ? '/' + i.args.length : ''), i]));
295
+ const typeItem = (n) => {
296
+ const i = items.get(n);
297
+ if (!i)
298
+ return undefined;
299
+ if (i.value.t !== 'typeValue' || i.value.type.t !== 'record')
300
+ fail(`${n} is a record type: { f : T, ... }`, i.span);
301
+ return i.value.type.fields.map(f => ({ name: f.name, type: this.type(f.type) }));
302
+ };
303
+ const params = decl.params.map(p => ({ name: p.name, type: this.type(p.type) }));
304
+ const scope = new Map(params.map(p => [p.name, p.name]));
305
+ const ctx = (extra = []) => ({ scope: new Map([...scope, ...extra]), params: new Set(), owner: name });
306
+ const listItem = (n, f) => {
307
+ const i = items.get(n);
308
+ if (!i)
309
+ return [];
310
+ if (i.value.t !== 'list')
311
+ fail(`${n} is a list`, i.value.span);
312
+ return i.value.items.map(f);
313
+ };
314
+ const flags = listItem('flags', e => e.t === 'flag'
315
+ ? { name: e.name, visibility: e.visibility, acts: e.acts }
316
+ : fail('expected `flag name visible|hidden, acts never|adversary`', e.span));
317
+ const actions = listItem('actions', e => {
318
+ if (e.t !== 'action')
319
+ return fail('expected `action name : Type := default`', e.span);
320
+ const type = this.type(e.type);
321
+ const d = this.meval(e.default, new Map(), e.type);
322
+ return { name: e.name, type, default: this.json(d, e.default.span) };
323
+ });
324
+ const sensors = listItem('sensors', e => {
325
+ if (e.t !== 'name')
326
+ return fail('expected the name of a sensor', e.span);
327
+ const full = this.globals.has(`${decl.name}.${e.name}`) ? `${decl.name}.${e.name}` : e.name;
328
+ const s = this.globals.get(full);
329
+ if (!s || s.d !== 'sensor')
330
+ return fail(`unknown sensor ${e.name}`, e.span);
331
+ this.use(s);
332
+ return this.sensor(s, new Map(), { owner: name, scope });
333
+ });
334
+ const initRecord = (item, extra) => {
335
+ if (!item)
336
+ return undefined;
337
+ if (item.value.t !== 'record' || item.value.base)
338
+ fail('init is a record of initial values: { f := e, ... }', item.value.span);
339
+ const out = {};
340
+ for (const it of item.value.items) {
341
+ if (it.t !== 'field')
342
+ fail('unexpected raise in init', it.span);
343
+ const f = it;
344
+ out[f.name] = this.tr(f.value ?? { t: 'name', name: f.name, span: f.span }, { ...ctx(extra), letName: f.name });
345
+ }
346
+ return out;
347
+ };
348
+ const agentInit = items.get('init/1');
349
+ const hook = (n) => {
350
+ const i = [...items.values()].find(i => i.name === n);
351
+ if (!i)
352
+ return undefined;
353
+ if (i.args.length !== 1)
354
+ fail(`${n} takes the configuration: ${n} C := ...`, i.span);
355
+ return this.tr(i.value, { ...ctx([[i.args[0], 'C']]), effects: true });
356
+ };
357
+ const c = {
358
+ name,
359
+ params: params.length ? params : undefined,
360
+ args: params.length ? Object.fromEntries(params.map(p => [p.name, this.json(args.get(p.name), decl.span)])) : undefined,
361
+ world: typeItem('World'),
362
+ agent: typeItem('Agent'),
363
+ flags: flags.length ? flags : undefined,
364
+ sensors: sensors.length ? sensors : undefined,
365
+ actions: actions.length ? actions : undefined,
366
+ init: dropUndefined({
367
+ world: initRecord(items.get('init'), []),
368
+ agent: agentInit && initRecord(agentInit, [[agentInit.args[0], 'i']]),
369
+ }),
370
+ beforeLook: hook('beforeLook'),
371
+ beforeMove: hook('beforeMove'),
372
+ afterMove: hook('afterMove'),
373
+ equivariance: this.equivariance(decl),
374
+ };
375
+ for (const i of decl.items) {
376
+ const known = ['World', 'Agent', 'flags', 'actions', 'sensors', 'init', 'beforeLook', 'beforeMove', 'afterMove'];
377
+ if (!known.includes(i.name))
378
+ fail(`unknown component field ${i.name} (expected one of ${known.join(', ')})`, i.span);
379
+ }
380
+ if (c.init && !c.init.world && !c.init.agent)
381
+ delete c.init;
382
+ return dropUndefined(c);
383
+ }
384
+ /**
385
+ * A sensor: binders of type `Config`, `Agent` and `Frame` are the observed
386
+ * configuration, the observer and its frame; the others are parameters of
387
+ * the family, fixed by `args`.
388
+ */
389
+ sensor(decl, args, component) {
390
+ const scope = new Map(component?.scope ?? []);
391
+ const constants = new Map();
392
+ for (const p of decl.params) {
393
+ const runtime = runtimeBinder(p.type);
394
+ if (runtime)
395
+ scope.set(p.name, runtime);
396
+ else {
397
+ const v = args.get(p.name) ?? fail(`missing argument ${p.name} of sensor ${decl.name}`, decl.span);
398
+ constants.set(p.name, { e: 'lit', type: this.type(p.type), value: this.json(v, decl.span) });
399
+ }
400
+ }
401
+ const implemented = decl.attrs.find(a => a.name === 'implements')?.args[0];
402
+ const name = implemented ?? decl.name.split('.').pop();
403
+ const owner = component?.owner ?? decl.name.split('.').pop();
404
+ const body = this.tr(decl.body, { scope, params: new Set(), owner, constants });
405
+ return dropUndefined({ name, type: this.type(decl.type), body, equivariance: this.equivariance(decl) });
406
+ }
407
+ model(decl, env) {
408
+ const v = (n) => {
409
+ const i = decl.items.find(i => i.name === n);
410
+ return i && this.meval(i.value, env, {
411
+ topology: { t: 'name', name: 'Topology', args: [], span: i.span },
412
+ memory: { t: 'name', name: 'Memory', args: [], span: i.span },
413
+ round: { t: 'name', name: 'Round', args: [], span: i.span },
414
+ symmetries: { t: 'name', name: 'Symmetries', args: [], span: i.span },
415
+ }[n]);
416
+ };
417
+ for (const i of decl.items) {
418
+ const known = ['topology', 'sensors', 'memory', 'frame', 'round', 'components', 'symmetries'];
419
+ if (!known.includes(i.name))
420
+ fail(`unknown model field ${i.name} (expected one of ${known.join(', ')})`, i.span);
421
+ }
422
+ const list = (x, m, span) => !x ? [] : x.m === 'list'
423
+ ? x.items.map(i => i.m === m ? i.ir : fail(`expected a list of ${m}s`, span))
424
+ : fail(`expected a list of ${m}s`, span);
425
+ const topology = v('topology');
426
+ const args = [...env.values()].filter(x => x.m === 'enum' || x.m === 'nat').map(x => x.m === 'enum' ? x.ctor : x.v);
427
+ return dropUndefined({
428
+ name: `${decl.name}${args.length ? `(${args.join(', ')})` : ''}`,
429
+ topology: topology ? (topology.m === 'topology' ? topology.ir : fail('expected a topology', decl.span)) : { kind: 'ring', size: { e: 'param', name: 'N' } },
430
+ sensors: list(v('sensors'), 'sensor', decl.span),
431
+ memory: v('memory')?.ctor ?? 'persistent',
432
+ frame: v('frame')?.ir ?? { kind: 'common' },
433
+ round: v('round')?.ctor ?? 'fsync',
434
+ movement: 'rigid',
435
+ components: list(v('components'), 'component', decl.span),
436
+ symmetries: v('symmetries')?.ctor ?? 'derived',
437
+ });
438
+ }
439
+ // -------------------------------------------------------------------------
440
+ // Algorithms, tasks, experiments
441
+ algorithm(decl, memory = 'persistent') {
442
+ if (decl.items.some(i => i.proc))
443
+ return this.structured(decl, memory);
444
+ const item = (n) => decl.items.find(i => i.name === n);
445
+ const typeOf = (n) => {
446
+ const i = item(n);
447
+ if (!i)
448
+ return T.unit;
449
+ if (i.value.t !== 'typeValue')
450
+ fail(`${n} is a type`, i.span);
451
+ return this.type(i.value.type);
452
+ };
453
+ const owner = decl.name;
454
+ const init = item('init') ?? fail('an algorithm needs `init i := { pub := ..., mem := ... }`', decl.span);
455
+ const iName = init.args[0] ?? 'i';
456
+ const fields = init.value.t === 'record' ? init.value.items : fail('init is a record { pub := ..., mem := ... }', init.value.span);
457
+ const initOf = (n) => {
458
+ const f = fields.find(f => f.t === 'field' && f.name === n);
459
+ return f?.value ? this.tr(f.value, { scope: new Map([[iName, 'i']]), params: new Set(), owner }) : this.hole(null, init.span);
460
+ };
461
+ const step = item('step') ?? fail('an algorithm needs `step mem pub view := ...`', decl.span);
462
+ const [m, p, v] = [step.args[0] ?? 'mem', step.args[1] ?? 'pub', step.args[2] ?? 'view'];
463
+ const line = item('line');
464
+ for (const i of decl.items) {
465
+ const known = ['Pub', 'Mem', 'init', 'step', 'line'];
466
+ if (!known.includes(i.name) && !i.name.startsWith('output.'))
467
+ fail(`unknown algorithm field ${i.name} (expected one of ${known.join(', ')}, or output)`, i.span);
468
+ }
469
+ const mem = typeOf('Mem');
470
+ return dropUndefined({
471
+ name: decl.name,
472
+ uses: decl.uses,
473
+ pub: typeOf('Pub'),
474
+ mem,
475
+ init: { pub: initOf('pub'), mem: mem.k === 'unit' && !fields.some(f => f.t === 'field' && f.name === 'mem') ? this.lit(T.unit, null, init.span) : initOf('mem') },
476
+ step: this.tr(step.value, { scope: new Map([[m, 'mem'], [p, 'pub'], [v, 'view']]), params: new Set(), owner, decision: { pub: 'pub', mem: 'mem' } }),
477
+ line: line && this.tr(line.value, { scope: new Map([[line.args[0] ?? 'mem', 'mem']]), params: new Set(), owner }),
478
+ source: { file: decl.span.file, line: decl.span.start.line, endLine: decl.span.end.line },
479
+ });
480
+ }
481
+ /** A structured algorithm (procedures, `main`), lowered to a state machine (`lower.ts`). */
482
+ structured(decl, memory) {
483
+ const owner = decl.name;
484
+ const procs = new Map();
485
+ let main;
486
+ for (const i of decl.items) {
487
+ if (!i.proc)
488
+ continue;
489
+ if (i.name === 'main')
490
+ main = i.proc;
491
+ else
492
+ procs.set(i.name, i.proc);
493
+ }
494
+ if (!main)
495
+ return fail(`${decl.name}: a structured algorithm needs \`main := ...\``, decl.span);
496
+ for (const i of decl.items) {
497
+ if (!i.proc && !['Pub', 'init'].includes(i.name) && !i.name.startsWith('output.')) {
498
+ fail(`unknown field ${i.name} of a structured algorithm (expected Pub, init, output, def, main; memory is derived)`, i.span);
499
+ }
500
+ }
501
+ const pubItem = decl.items.find(i => i.name === 'Pub');
502
+ const pub = pubItem ? (pubItem.value.t === 'typeValue' ? this.type(pubItem.value.type) : fail('Pub is a type', pubItem.span)) : T.unit;
503
+ const init = decl.items.find(i => i.name === 'init');
504
+ let initPub = this.lit(T.unit, null, decl.span);
505
+ if (init) {
506
+ if (init.value.t !== 'record')
507
+ fail('init is a record { pub := ... }', init.value.span);
508
+ const f = init.value.items.find(x => x.t === 'field' && x.name === 'pub');
509
+ if (f?.value)
510
+ initPub = this.tr(f.value, { scope: new Map([[init.args[0] ?? 'i', 'i']]), params: new Set(), owner });
511
+ }
512
+ const lowered = lower(decl.name, procs, main.body, decl.uses, memory === 'oblivious', {
513
+ tr: (e, scope, constants) => this.tr(e, { scope, params: new Set(), owner, constants }),
514
+ type: t => this.type(t),
515
+ fail: (msg, span) => fail(msg, span),
516
+ });
517
+ return dropUndefined({
518
+ name: decl.name, uses: decl.uses, pub, mem: lowered.mem,
519
+ init: { pub: initPub, mem: lowered.initMem },
520
+ step: lowered.step, line: lowered.line,
521
+ source: {
522
+ file: decl.span.file, line: decl.span.start.line, endLine: decl.span.end.line,
523
+ ...(Object.keys(lowered.calls).length ? { calls: lowered.calls } : {}),
524
+ },
525
+ });
526
+ }
527
+ /** The outputs an algorithm declares (`output claim := pub.bh`). */
528
+ outputs(decl) {
529
+ return new Map(decl.items.filter(i => i.name.startsWith('output.')).map(i => [i.name.slice('output.'.length), i.value]));
530
+ }
531
+ task(decl, outputs = new Map()) {
532
+ const C = decl.params.find(p => runtimeBinder(p.type) === 'C')?.name ?? 'C';
533
+ return dropUndefined({
534
+ name: decl.name,
535
+ goal: this.goals.get(decl.name)?.goal ?? 'eventually',
536
+ verdict: this.tr(decl.body, { scope: new Map([[C, 'C']]), params: new Set(), owner: decl.name, outputs }),
537
+ equivariance: this.equivariance(decl),
538
+ });
539
+ }
540
+ /** Elaborates, resolves and checks an experiment of the entry module. */
541
+ experiment(name) {
542
+ try {
543
+ const decl = this.globals.get(name);
544
+ if (!decl || decl.d !== 'experiment')
545
+ fail(`no experiment ${name}`);
546
+ const exp = this.buildExperiment(decl);
547
+ const first = check(exp);
548
+ const diagnostics = first.diagnostics.map(d => this.locate(d, decl.span));
549
+ if (diagnostics.length)
550
+ return { diagnostics: [...this.diagnostics, ...diagnostics] };
551
+ const types = resolveTypes(exp, first.resolutions, (from, to) => {
552
+ const s = this.spans.get(from);
553
+ if (s)
554
+ this.spans.set(to, s);
555
+ });
556
+ // After resolution, which replaces literals (and carries their spans over).
557
+ const resolved = { ...types, algorithm: this.located(types.algorithm) };
558
+ const second = check(resolved);
559
+ return second.errors.length
560
+ ? { diagnostics: [...this.diagnostics, ...second.diagnostics.map(d => this.locate(d, decl.span))] }
561
+ : { experiment: resolved, diagnostics: this.diagnostics };
562
+ }
563
+ catch (e) {
564
+ if (!(e instanceof ElabError))
565
+ throw e;
566
+ return { diagnostics: [...this.diagnostics, { message: e.message, span: e.span }] };
567
+ }
568
+ }
569
+ locate(d, fallback) {
570
+ return { message: d.message, span: (d.node && this.spans.get(d.node)) ?? fallback };
571
+ }
572
+ buildExperiment(decl) {
573
+ const uses = this.uses = new Map();
574
+ try {
575
+ this.use(decl);
576
+ return this.experimentOf(decl, uses);
577
+ }
578
+ finally {
579
+ this.uses = undefined;
580
+ }
581
+ }
582
+ /** Records that the experiment being built uses `decl` (display only). */
583
+ use(decl) {
584
+ const kind = declKind(decl);
585
+ const name = 'name' in decl ? decl.name : undefined;
586
+ if (!this.uses || !kind || !name || this.uses.has(`${kind} ${name}`))
587
+ return;
588
+ this.uses.set(`${kind} ${name}`, { kind, name, file: decl.span.file, line: decl.span.start.line, endLine: decl.span.end.line });
589
+ }
590
+ experimentOf(decl, uses) {
591
+ const item = (n) => decl.items.find(i => i.name === n && !i.param);
592
+ const required = (n) => item(n) ?? fail(`an experiment needs \`${n} := ...\``, decl.span);
593
+ const modelValue = this.meval(required('model').value, new Map(), { t: 'name', name: 'Model', args: [], span: decl.span });
594
+ const ref = (n, kind) => {
595
+ const v = required(n).value;
596
+ const d = v.t === 'name' ? this.globals.get(v.name) : undefined;
597
+ if (!d || d.d !== kind)
598
+ fail(`expected the name of ${kind === 'algorithm' ? 'an algorithm' : 'a task'}`, v.span);
599
+ return d;
600
+ };
601
+ const algDecl = ref('algorithm', 'algorithm');
602
+ const algorithm = this.withDefs(() => this.algorithm(algDecl, modelValue.ir.memory));
603
+ const task = this.withDefs(() => this.task(ref('task', 'task'), this.outputs(algDecl)));
604
+ const paramNames = new Set();
605
+ // Definitions called by parameter domains and initial positions belong to the model (plan 3.9).
606
+ const used = new Set();
607
+ const outer = this.calledDefs;
608
+ this.calledDefs = used;
609
+ const params = decl.items.filter(i => i.param).map(i => {
610
+ const p = { name: i.name, type: this.type(i.param.type) };
611
+ if (i.value.t !== 'nothing')
612
+ p.domain = this.tr(i.value, { scope: new Map(), params: new Set(paramNames), owner: decl.name });
613
+ paramNames.add(i.name);
614
+ return p;
615
+ });
616
+ const ictx = (extra = []) => ({ scope: new Map(extra), params: paramNames, owner: decl.name });
617
+ const pos = item('position');
618
+ const limits = item('limits');
619
+ for (const i of decl.items) {
620
+ const known = ['model', 'algorithm', 'task', 'agents', 'position', 'limits'];
621
+ if (!i.param && !known.includes(i.name))
622
+ fail(`unknown experiment field ${i.name} (expected one of ${known.join(', ')}, or param)`, i.span);
623
+ }
624
+ const model0 = modelValue.m === 'model' ? modelValue.ir : fail('expected a model', required('model').span);
625
+ const actions = model0.components.flatMap(c => c.actions ?? []);
626
+ let agents;
627
+ try {
628
+ agents = {
629
+ count: this.tr(required('agents').value, ictx()),
630
+ pos: pos ? this.tr(pos.value, ictx([[pos.args[0] ?? 'i', 'i']])) : fail('an experiment needs `position i := ...`', decl.span),
631
+ };
632
+ }
633
+ finally {
634
+ this.calledDefs = outer;
635
+ }
636
+ const model = used.size ? { ...model0, defs: [...(model0.defs ?? []), ...this.defsIn(used)] } : model0;
637
+ this.use(algDecl);
638
+ const taskDecl = ref('task', 'task');
639
+ this.use(taskDecl);
640
+ const goal = this.goals.get(taskDecl.name);
641
+ if (goal)
642
+ uses.set(`goal ${taskDecl.name}`, { kind: 'goal', name: taskDecl.name, file: goal.span.file, line: goal.span.start.line, endLine: goal.span.end.line });
643
+ for (const d of [...model.defs ?? [], ...algorithm.defs ?? [], ...task.defs ?? []]) {
644
+ const span = this.objectDefs.get(d.name)?.span;
645
+ if (span)
646
+ uses.set(`definition ${d.name}`, { kind: 'definition', name: d.name, file: span.file, line: span.start.line, endLine: span.end.line });
647
+ }
648
+ return dropUndefined({
649
+ irVersion: IR_VERSION,
650
+ name: decl.name,
651
+ model,
652
+ algorithm: algorithm.native ? algorithm : { ...algorithm, step: fillActionDefaults(algorithm.step, actions) },
653
+ task,
654
+ params,
655
+ agents,
656
+ limits: limits && this.limits(limits),
657
+ source: { file: decl.span.file, line: decl.span.start.line, endLine: decl.span.end.line, uses: [...uses.values()] },
658
+ });
659
+ }
660
+ limits(i) {
661
+ if (i.value.t !== 'record')
662
+ return fail('limits is a record { depth := n, states := n }', i.span);
663
+ const out = {};
664
+ for (const it of i.value.items) {
665
+ if (it.t !== 'field' || !it.value || it.value.t !== 'num')
666
+ fail('expected `depth := n` or `states := n`', it.span);
667
+ out[it.name] = (it.value).value;
668
+ }
669
+ return out;
670
+ }
671
+ // -------------------------------------------------------------------------
672
+ // Object-level definitions
673
+ /** Elaborates an algorithm or a task, with the definitions it calls. */
674
+ withDefs(f) {
675
+ const outer = this.calledDefs;
676
+ const used = this.calledDefs = new Set();
677
+ try {
678
+ const x = f();
679
+ return used.size ? { ...x, defs: this.defsIn(used) } : x;
680
+ }
681
+ finally {
682
+ this.calledDefs = outer;
683
+ }
684
+ }
685
+ /**
686
+ * For the explanation of a step (display only): the source lines of the algorithm's step and
687
+ * of its definitions (`at`), and where the definitions are written (`source.defs`).
688
+ */
689
+ located(a) {
690
+ const mark = (x) => {
691
+ if (!x || typeof x !== 'object')
692
+ return;
693
+ if (Array.isArray(x)) {
694
+ x.forEach(mark);
695
+ return;
696
+ }
697
+ const o = x;
698
+ if (typeof o.e === 'string' && o.at === undefined) {
699
+ const span = this.spans.get(o);
700
+ if (span)
701
+ o.at = span.start.line;
702
+ }
703
+ for (const [k, v] of Object.entries(o))
704
+ if (k !== 'type')
705
+ mark(v);
706
+ };
707
+ mark(a.step);
708
+ for (const d of a.defs ?? [])
709
+ mark(d.body);
710
+ const defs = (a.defs ?? []).flatMap(d => {
711
+ const span = this.objectDefs.get(d.name)?.span;
712
+ return span ? [{ name: d.name, file: span.file, line: span.start.line, endLine: span.end.line }] : [];
713
+ });
714
+ return defs.length && a.source ? { ...a, source: { ...a.source, defs } } : a;
715
+ }
716
+ /** The definitions named in `used`, callees first. */
717
+ defsIn(used) {
718
+ return [...this.objectDefs].filter(([n]) => used.has(n)).map(([, d]) => d.def);
719
+ }
720
+ /** Records a call to an object-level definition (elaborating it the first time). */
721
+ useDef(decl, span) {
722
+ let entry = this.objectDefs.get(decl.name);
723
+ if (!entry) {
724
+ if (BUILTINS[decl.name])
725
+ fail(`${decl.name} is a builtin: give the definition another name`, decl.span);
726
+ entry = { deps: new Set(), span: decl.span };
727
+ this.objectDefs.set(decl.name, entry);
728
+ const outer = this.calledDefs;
729
+ this.calledDefs = entry.deps;
730
+ try {
731
+ const params = decl.params.map(p => ({ name: p.name, type: this.type(p.type) }));
732
+ const scope = new Map(params.map(p => [p.name, p.name]));
733
+ const body = this.tr(decl.body, { scope, params: new Set(), owner: decl.name });
734
+ // Callees first: the map keeps insertion order, so re-insert after the dependencies.
735
+ this.objectDefs.delete(decl.name);
736
+ this.objectDefs.set(decl.name, entry);
737
+ entry.def = { name: decl.name, params, ret: this.type(decl.type), body };
738
+ }
739
+ catch (e) {
740
+ this.objectDefs.delete(decl.name);
741
+ throw e;
742
+ }
743
+ finally {
744
+ this.calledDefs = outer;
745
+ }
746
+ }
747
+ else if (!entry.def) {
748
+ fail(`definitions cannot be recursive: ${decl.name}`, span);
749
+ }
750
+ if (this.calledDefs) {
751
+ this.calledDefs.add(decl.name);
752
+ for (const d of entry.deps)
753
+ this.calledDefs.add(d);
754
+ }
755
+ return entry.def;
756
+ }
757
+ // -------------------------------------------------------------------------
758
+ // Object level
759
+ mark(e, span) {
760
+ this.spans.set(e, span);
761
+ return e;
762
+ }
763
+ lit(type, value, span) {
764
+ return this.mark({ e: 'lit', type, value }, span);
765
+ }
766
+ hole(value, span) {
767
+ return this.lit(T.infer, value, span);
768
+ }
769
+ call(fn, args, span) {
770
+ return this.mark({ e: 'call', fn, args }, span);
771
+ }
772
+ /** A lambda from a surface expression: `fun x => e`, a section, or the name of a function (eta-expanded). */
773
+ lambda(e, ctx) {
774
+ if (e.t === 'fun')
775
+ return this.tr(e, ctx);
776
+ const depth = ctx.depth ?? 0;
777
+ const x = `x${depth}`;
778
+ const inner = { ...ctx, scope: new Map(ctx.scope).set(x, x), depth: depth + 1 };
779
+ const call = this.tr({ t: 'app', fn: e, args: [{ value: { t: 'name', name: x, span: e.span } }], span: e.span }, inner);
780
+ return this.mark({ e: 'fn', params: [{ name: x }], body: call }, e.span);
781
+ }
782
+ tr(e, ctx) {
783
+ const span = e.span;
784
+ const tail = { effects: ctx.effects, decision: ctx.decision };
785
+ const inner = { ...ctx, effects: undefined, decision: undefined };
786
+ switch (e.t) {
787
+ case 'num': return this.lit(T.nat, e.value, span);
788
+ case 'str': return this.lit(T.string, e.value, span);
789
+ case 'bool': return this.lit(T.bool, e.value, span);
790
+ case 'ctor': return this.hole(e.name, span);
791
+ case 'unit': return this.lit(T.unit, null, span);
792
+ case 'nothing':
793
+ if (!ctx.effects)
794
+ fail('`nothing` is an effect, returned by hooks', span);
795
+ return this.mark({ e: 'effect' }, span);
796
+ case 'param': return this.mark({ e: 'param', name: e.name }, span);
797
+ case 'name': return this.name(e.name, span, ctx);
798
+ case 'proj': return this.projection(this.tr(e.on, inner), e.field, [], span, inner);
799
+ case 'app': return this.app(e, inner);
800
+ case 'binop': {
801
+ if (e.op === '∧' || e.op === '∨') {
802
+ const parts = flatten(e, e.op).map(x => this.tr(x, inner));
803
+ return this.call(e.op === '∧' ? 'and' : 'or', parts, span);
804
+ }
805
+ const [l, r] = [this.tr(e.l, inner), this.tr(e.r, inner)];
806
+ if (e.op === '++')
807
+ return this.call(isStringy(e.l) || isStringy(e.r) ? 'concat' : 'append', [l, r], span);
808
+ return this.call(BINOPS[e.op] ?? fail(`unknown operator ${e.op}`, span), [l, r], span);
809
+ }
810
+ case 'unop': {
811
+ if (e.op === '-' && e.e.t === 'num')
812
+ return this.lit(T.int, -e.e.value, span);
813
+ if (e.op === '-')
814
+ return fail('negation applies to number literals', span);
815
+ return this.call('not', [this.tr(e.e, inner)], span);
816
+ }
817
+ case 'if':
818
+ return this.mark({ e: 'if', cond: this.tr(e.cond, inner), then: this.tr(e.then, ctx), else: this.tr(e.else, ctx) }, span);
819
+ case 'let': {
820
+ const value = this.tr(e.value, { ...inner, letName: e.name });
821
+ const body = this.tr(e.body, { ...ctx, scope: new Map(ctx.scope).set(e.name, e.name) });
822
+ return this.mark({ e: 'let', name: e.name, value, body }, span);
823
+ }
824
+ case 'match': {
825
+ const cases = {};
826
+ let otherwise;
827
+ for (const c of e.cases) {
828
+ const body = this.tr(c.body, ctx);
829
+ if (c.pat.t === 'wild')
830
+ otherwise = body;
831
+ else if (c.pat.t === 'ctor')
832
+ cases[c.pat.name.split('.').pop()] = body;
833
+ else
834
+ fail('only constructors and _ can be matched for now', c.pat.span);
835
+ }
836
+ return this.mark({ e: 'match', on: this.tr(e.on, inner), cases, ...(otherwise ? { else: otherwise } : {}) }, span);
837
+ }
838
+ case 'fun': {
839
+ const scope = new Map(ctx.scope);
840
+ const depth = ctx.depth ?? 0;
841
+ const params = e.params.map(p => {
842
+ const n = p.name === '·' ? `x${depth}` : p.name;
843
+ scope.set(p.name, n);
844
+ return p.type ? { name: n, type: this.type(p.type) } : { name: n };
845
+ });
846
+ return this.mark({ e: 'fn', params, body: this.tr(e.body, { ...inner, scope, depth: depth + 1 }) }, span);
847
+ }
848
+ case 'list':
849
+ return e.items.length ? this.call('list', e.items.map(i => this.tr(i, inner)), span) : this.hole([], span);
850
+ case 'record':
851
+ return this.record(e, ctx, tail);
852
+ case 'choose': {
853
+ const from = this.tr(e.from, inner);
854
+ const site = `${ctx.owner}.${ctx.letName ?? 'choice'}`;
855
+ const such = e.such && this.lambda(e.such, inner);
856
+ let label;
857
+ if (e.labels?.t === 'list') {
858
+ if (e.from.t !== 'list')
859
+ fail('a list of labels needs a list of options: give a function instead', e.labels.span);
860
+ const x = `x${inner.depth ?? 0}`;
861
+ const lctx = { ...inner, scope: new Map(inner.scope).set(x, x), depth: (inner.depth ?? 0) + 1 };
862
+ const options = e.from.items;
863
+ const labels = e.labels.items;
864
+ let body = this.lit(T.string, '', e.labels.span);
865
+ for (let k = options.length - 1; k >= 0; k--) {
866
+ const cond = this.call('eq', [this.mark({ e: 'var', name: x }, span), this.tr(options[k], lctx)], span);
867
+ body = this.mark({ e: 'if', cond, then: this.tr(labels[k], lctx), else: body }, span);
868
+ }
869
+ label = this.mark({ e: 'fn', params: [{ name: x }], body }, e.labels.span);
870
+ }
871
+ else if (e.labels) {
872
+ label = this.lambda(e.labels, inner);
873
+ }
874
+ return this.mark({ e: 'choose', site, from, ...(such ? { such } : {}), ...(label ? { label } : {}) }, span);
875
+ }
876
+ case 'ascribe': {
877
+ const x = this.tr(e.e, inner);
878
+ if (x.e === 'lit' && x.type.k === 'infer')
879
+ return this.lit(this.type(e.type), x.value, span);
880
+ return x;
881
+ }
882
+ case 'hole': return fail('· is only allowed inside parentheses', span);
883
+ case 'typeValue': return fail('a type is not a value', span);
884
+ case 'flag':
885
+ case 'action': return fail(`${e.t} declarations belong to components`, span);
886
+ }
887
+ }
888
+ record(e, ctx, tail) {
889
+ const span = e.span;
890
+ const inner = { ...ctx, effects: undefined, decision: undefined };
891
+ const fieldsOf = (items, c) => {
892
+ const out = {};
893
+ for (const it of items) {
894
+ if (it.t !== 'field')
895
+ fail('`raise` only appears in an effect', it.span);
896
+ const f = it;
897
+ out[f.name] = this.tr(f.value ?? { t: 'name', name: f.name, span: f.span }, { ...c, letName: f.name });
898
+ }
899
+ return out;
900
+ };
901
+ if (tail.effects && !e.base) {
902
+ // An effect: `{ world := { C.world with f := v }, raise flag := agents or predicate }`.
903
+ const world = {};
904
+ const raise = {};
905
+ for (const it of e.items) {
906
+ if (it.t === 'raise') {
907
+ const v = it.value;
908
+ raise[it.flag] = this.tr(v, inner);
909
+ }
910
+ else if (it.name === 'world') {
911
+ const w = it.value;
912
+ if (!w || w.t !== 'record' || !w.base)
913
+ fail('world := { C.world with f := v, ... }', it.span);
914
+ Object.assign(world, fieldsOf(w.items, inner));
915
+ }
916
+ else {
917
+ fail(`an effect has \`world :=\` and \`raise flag :=\`, not ${it.name}`, it.span);
918
+ }
919
+ }
920
+ return this.mark({ e: 'effect', ...(Object.keys(world).length ? { world } : {}), ...(Object.keys(raise).length ? { raise } : {}) }, span);
921
+ }
922
+ if (e.base)
923
+ return this.mark({ e: 'with', on: this.tr(e.base, inner), fields: fieldsOf(e.items, inner) }, span);
924
+ const fields = fieldsOf(e.items, inner);
925
+ if (tail.decision) {
926
+ // A decision keeps the public state and the memory unless it sets them.
927
+ fields.pub ??= this.mark({ e: 'var', name: tail.decision.pub }, span);
928
+ fields.mem ??= this.mark({ e: 'var', name: tail.decision.mem }, span);
929
+ if (!fields.move)
930
+ fail('a decision needs a move', span);
931
+ }
932
+ return this.mark({ e: 'record', fields }, span);
933
+ }
934
+ /** A possibly dotted name: a local with projections, a parameter, a constructor, or a definition. */
935
+ name(full, span, ctx) {
936
+ const parts = full.split('.');
937
+ for (let n = parts.length; n >= 1; n--) {
938
+ const head = parts.slice(0, n).join('.');
939
+ const local = ctx.scope.get(head);
940
+ const constant = ctx.constants?.get(head);
941
+ const param = n === 1 && local === undefined && ctx.params.has(head);
942
+ if (local !== undefined || constant || param) {
943
+ let e = constant ? this.mark({ ...constant }, span)
944
+ : param ? this.mark({ e: 'param', name: head }, span)
945
+ : this.mark({ e: 'var', name: local }, span);
946
+ for (const f of parts.slice(n))
947
+ e = this.projection(e, f, [], span, ctx);
948
+ return e;
949
+ }
950
+ }
951
+ if (full === 'none')
952
+ return this.hole(null, span);
953
+ if (full === 'frames')
954
+ return this.call('frames', [], span);
955
+ if (parts.length >= 2) {
956
+ const enumName = parts.slice(0, -1).join('.');
957
+ const t = enumName === 'Move' ? T.move : enumName === 'Verdict' ? T.verdict : this.enums.get(enumName);
958
+ if (t) {
959
+ const ctor = parts[parts.length - 1];
960
+ const ctors = t.k === 'enum' ? t.ctors : t.k === 'move' ? ['stay', 'cw', 'ccw'] : ['success', 'failure', 'inProgress'];
961
+ if (!ctors.includes(ctor))
962
+ fail(`${ctor} is not a constructor of ${enumName}`, span);
963
+ return this.lit(t, ctor, span);
964
+ }
965
+ }
966
+ if (BUILTIN_FUNCTIONS.has(full))
967
+ return fail(`${full} needs arguments`, span);
968
+ const g = this.globals.get(full);
969
+ if (isObjectDef(g)) {
970
+ if (g.params.length)
971
+ fail(`${full} needs ${g.params.length} argument(s)`, span);
972
+ this.useDef(g, span);
973
+ return this.call(full, [], span);
974
+ }
975
+ return fail(`unknown name ${full}`, span);
976
+ }
977
+ /** `e.f`, with methods of collections, options, configurations and moves. */
978
+ projection(on, field, args, span, ctx) {
979
+ const rest = args.map((a, i) => COLLECTION_METHODS_WITH_FN.has(field) && i === 0 ? this.lambda(a.value, ctx) : this.tr(a.value, ctx));
980
+ const noArgs = () => { if (args.length)
981
+ fail(`${field} takes no arguments`, span); };
982
+ switch (field) {
983
+ case 'length':
984
+ case 'card':
985
+ noArgs();
986
+ return this.call('length', [on], span);
987
+ case 'isEmpty':
988
+ case 'sum':
989
+ case 'isSome':
990
+ case 'isNone':
991
+ case 'toMultiset':
992
+ case 'toList':
993
+ case 'dedup':
994
+ noArgs();
995
+ return this.call(field, [on], span);
996
+ case 'opposite':
997
+ noArgs();
998
+ return this.call('opposite', [on], span);
999
+ case 'size':
1000
+ case 'nodes':
1001
+ case 'agents':
1002
+ // On the configuration (always `C` in IR): builtins.
1003
+ if (on.e === 'var' && on.name === 'C') {
1004
+ noArgs();
1005
+ return this.call(field, [on], span);
1006
+ }
1007
+ break;
1008
+ case 'filter':
1009
+ case 'map':
1010
+ case 'any':
1011
+ case 'all':
1012
+ case 'count':
1013
+ case 'getD':
1014
+ case 'diff':
1015
+ case 'at':
1016
+ if (rest.length !== 1)
1017
+ fail(`${field} takes one argument`, span);
1018
+ return this.call(field, [on, ...rest], span);
1019
+ case 'contains':
1020
+ if (rest.length !== 1)
1021
+ fail('contains takes one argument', span);
1022
+ return this.call('mem', [rest[0], on], span);
1023
+ }
1024
+ if (args.length)
1025
+ fail(`${field} is a field, not a function`, span);
1026
+ const output = ctx.outputs?.get(field);
1027
+ if (output) {
1028
+ // An output of the algorithm, read on an agent: its definition, with the agent's state.
1029
+ const constants = new Map([['pub', { e: 'get', on, field: 'pub' }], ['mem', { e: 'get', on, field: 'mem' }]]);
1030
+ return this.tr(output, { scope: new Map(), params: new Set(), owner: ctx.owner, constants });
1031
+ }
1032
+ return this.mark({ e: 'get', on, field }, span);
1033
+ }
1034
+ app(e, ctx) {
1035
+ const span = e.span;
1036
+ if (e.args.some(a => a.name))
1037
+ fail('named arguments are only for definitions of models and components', span);
1038
+ // `x.method args`: the last part of a dotted name, or a projection.
1039
+ if (e.fn.t === 'proj')
1040
+ return this.projection(this.tr(e.fn.on, ctx), e.fn.field, e.args, span, ctx);
1041
+ if (e.fn.t === 'name') {
1042
+ const name = e.fn.name;
1043
+ const parts = name.split('.');
1044
+ if (parts.length > 1 && !this.globals.has(name)) {
1045
+ const recv = this.name(parts.slice(0, -1).join('.'), e.fn.span, ctx);
1046
+ return this.projection(recv, parts[parts.length - 1], e.args, span, ctx);
1047
+ }
1048
+ if (BUILTIN_FUNCTIONS.has(name)) {
1049
+ const fnArgs = FUNCTION_ARGS[name] ?? [];
1050
+ const args = e.args.map((a, i) => fnArgs.includes(i) ? this.lambda(a.value, ctx) : this.tr(a.value, ctx));
1051
+ // `subsetsUpTo k xs`, as in the syntax document; the IR takes the collection first.
1052
+ if (name === 'subsetsUpTo' && args.length === 2)
1053
+ args.reverse();
1054
+ return this.call(name, args, span);
1055
+ }
1056
+ const local = ctx.scope.get(name);
1057
+ if (local !== undefined)
1058
+ fail(`${name} is not a function`, span);
1059
+ const g = this.globals.get(name);
1060
+ if (isObjectDef(g)) {
1061
+ if (e.args.length !== g.params.length)
1062
+ fail(`${name} takes ${g.params.length} argument(s)`, span);
1063
+ this.useDef(g, span);
1064
+ return this.call(name, e.args.map(a => this.tr(a.value, ctx)), span);
1065
+ }
1066
+ }
1067
+ return fail('unknown function', e.fn.span);
1068
+ }
1069
+ }
1070
+ function runtimeBinder(t) {
1071
+ if (t.t !== 'name')
1072
+ return undefined;
1073
+ return { Config: 'C', Agent: 'me', Frame: 'frame' }[t.name];
1074
+ }
1075
+ function flatten(e, op) {
1076
+ return e.t === 'binop' && e.op === op ? [...flatten(e.l, op), ...flatten(e.r, op)] : [e];
1077
+ }
1078
+ function isStringy(e) {
1079
+ return e.t === 'str' || (e.t === 'app' && e.fn.t === 'name' && e.fn.name === 'show') || (e.t === 'binop' && e.op === '++' && (isStringy(e.l) || isStringy(e.r)));
1080
+ }
1081
+ function stripType(v) {
1082
+ return v.m === 'enum' ? { m: 'enum', ctor: v.ctor, type: v.type.name } : v;
1083
+ }
1084
+ function dropUndefined(o) {
1085
+ return Object.fromEntries(Object.entries(o).filter(([, x]) => x !== undefined));
1086
+ }
1087
+ /** Compiles the experiments of an entry module. */
1088
+ export function compile(program) {
1089
+ const elab = new Elaborator(program);
1090
+ const experiments = {};
1091
+ for (const name of elab.experiments())
1092
+ experiments[name] = elab.experiment(name);
1093
+ return { experiments, diagnostics: elab.diagnostics };
1094
+ }