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