@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,405 @@
1
+ /**
2
+ * Lowering of structured algorithms to state machines (plan, Section 3.8).
3
+ *
4
+ * `move d`, `stay` and `halt` end the activation. Every one of them is a
5
+ * resume point: the program counter records where the agent resumes, and
6
+ * the step function, for each value of the program counter, runs the code
7
+ * from there to the next `move`, `stay` or `halt`. Procedures are inlined
8
+ * (they are not recursive); arguments that are constants are substituted,
9
+ * others become locals. Reaching the end of `main` restarts it at the next
10
+ * activation, so an algorithm that ends with its only move needs no memory.
11
+ *
12
+ * `with { pebble := .drop }` after `move`, `stay` or `halt` sets actions of
13
+ * the model for that activation only (a halted agent does not repeat them).
14
+ *
15
+ * `wait n` evaluates `n` once and stays that many activations: a counter
16
+ * (one local shared by every `wait`, zero outside them) and a `stay`, all at
17
+ * the line of the `wait`.
18
+ *
19
+ * For display, each program-counter value also records the lines of the
20
+ * procedure calls it is inside (`calls`), outermost first.
21
+ *
22
+ * What is kept across activations is declared memory: the program counter
23
+ * (when there is more than one resume point) and the typed locals. In an
24
+ * oblivious model, an algorithm that needs either is rejected.
25
+ */
26
+ import type { Expr, Field, Json, Type } from '../ir';
27
+ import type { SBinder, SExpr, SRecordItem, SStmt, SType } from '../parse/ast';
28
+ import type { Span } from '../parse/lexer';
29
+ import { T } from '../types';
30
+
31
+ export type Procedure = { params: SBinder[]; body: SStmt[] };
32
+
33
+ /** What the lowering needs from the elaborator. */
34
+ export type LowerHost = {
35
+ /** Translates an expression; `scope` maps surface names to IR variables, `constants` to IR expressions. */
36
+ tr(e: SExpr, scope: Map<string, string>, constants: Map<string, Expr>): Expr;
37
+ type(t: SType): Type;
38
+ fail(msg: string, span?: Span): never;
39
+ };
40
+
41
+ export type Lowered = {
42
+ mem: Type;
43
+ initMem: Expr;
44
+ step: Expr;
45
+ line?: Expr;
46
+ /** The source line of each value of the program counter. */
47
+ lines: Record<string, number>;
48
+ /** The lines of the procedure calls around each value of the program counter (outermost first), when there are any. */
49
+ calls: Record<string, number[]>;
50
+ };
51
+
52
+ /** Bindings of an inlined procedure: constants, locals (by IR name), and the lines of the calls that led to it. */
53
+ type Env = { consts: Map<string, Expr>; locals: Map<string, string>; calls: number[] };
54
+
55
+ type IStmt = { span: Span; env: Env } & (
56
+ | { k: 'move'; dir: SExpr; actions?: SRecordItem[] }
57
+ | { k: 'stay'; actions?: SRecordItem[] }
58
+ | { k: 'halt'; actions?: SRecordItem[] }
59
+ | { k: 'repeat'; body: IStmt[] }
60
+ | { k: 'while'; cond: SExpr; body: IStmt[] }
61
+ | { k: 'if'; cond: SExpr; then: IStmt[]; else: IStmt[] }
62
+ | { k: 'set'; local: string | 'pub'; value: SExpr }
63
+ );
64
+
65
+ type Frame = { rest: IStmt[]; loop?: IStmt };
66
+
67
+ /** Expressions that can be substituted for a parameter without memory. */
68
+ function isConstant(e: SExpr): boolean {
69
+ switch (e.t) {
70
+ case 'num': case 'str': case 'bool': case 'ctor': case 'unit': return true;
71
+ case 'name': return /^[A-Z]\w*\.\w+$/.test(e.name); // Move.cw
72
+ case 'list': return e.items.every(isConstant);
73
+ case 'unop': return e.op === '-' && e.e.t === 'num';
74
+ default: return false;
75
+ }
76
+ }
77
+
78
+ export function lower(
79
+ name: string, procs: Map<string, Procedure>, main: SStmt[], sensors: string[], oblivious: boolean, host: LowerHost,
80
+ ): Lowered {
81
+ const locals = new Map<string, { type?: Type; span: Span }>();
82
+ let fresh = 0;
83
+ const sensorConsts = new Map<string, Expr>(sensors.map(s => [s, { e: 'get', on: { e: 'var', name: 'view' }, field: s } as Expr]));
84
+
85
+ // 1. Inlining
86
+ let waitLocal: string | undefined;
87
+ const inline = (stmts: SStmt[], env: Env, stack: string[]): IStmt[] => stmts.flatMap((s): IStmt[] => {
88
+ const at = { span: s.span, env };
89
+ switch (s.s) {
90
+ case 'move': return [{ ...at, k: 'move', dir: s.dir, actions: s.actions }];
91
+ case 'stay': return [{ ...at, k: 'stay', actions: s.actions }];
92
+ case 'halt': return [{ ...at, k: 'halt', actions: s.actions }];
93
+ case 'wait': {
94
+ if (!waitLocal) {
95
+ waitLocal = `wait_${fresh++}`;
96
+ locals.set(waitLocal, { type: T.nat, span: s.span });
97
+ }
98
+ // `#wait` is not a surface name: the counter cannot clash with the algorithm's names.
99
+ const w = { ...env, locals: new Map(env.locals).set('#wait', waitLocal) };
100
+ const span = s.span;
101
+ const left: SExpr = { t: 'name', name: '#wait', span };
102
+ return [
103
+ { span, env, k: 'set', local: waitLocal, value: s.count },
104
+ { span, env: w, k: 'while', cond: { t: 'binop', op: '>', l: left, r: { t: 'num', value: 0, span }, span }, body: [
105
+ { span, env: w, k: 'set', local: waitLocal, value: { t: 'binop', op: '-', l: left, r: { t: 'num', value: 1, span }, span } },
106
+ { span, env: w, k: 'stay' },
107
+ ] },
108
+ ];
109
+ }
110
+ case 'repeat': return [{ ...at, k: 'repeat', body: inline(s.body, env, stack) }];
111
+ case 'while': return [{ ...at, k: 'while', cond: s.cond, body: inline(s.body, env, stack) }];
112
+ case 'if': return [{ ...at, k: 'if', cond: s.cond, then: inline(s.then, env, stack), else: inline(s.else ?? [], env, stack) }];
113
+ case 'let': {
114
+ // Locals are scoped to their procedure (one declaration per name).
115
+ if (env.locals.has(s.name)) host.fail(`${s.name} is already declared in this procedure`, s.span);
116
+ const local = `${s.name}_${fresh++}`;
117
+ locals.set(local, { type: s.type && host.type(s.type), span: s.span });
118
+ env.locals.set(s.name, local);
119
+ return [{ ...at, k: 'set', local, value: s.value }];
120
+ }
121
+ case 'assign': {
122
+ if (s.name === 'pub') return [{ ...at, k: 'set', local: 'pub', value: s.value }];
123
+ const local = env.locals.get(s.name) ?? host.fail(`${s.name} is not a local variable (declare it with let)`, s.span);
124
+ return [{ ...at, k: 'set', local, value: s.value }];
125
+ }
126
+ case 'call': {
127
+ const p = procs.get(s.name) ?? host.fail(`unknown procedure ${s.name}`, s.span);
128
+ if (stack.includes(s.name)) host.fail(`procedures cannot be recursive: ${[...stack, s.name].join(' → ')}`, s.span);
129
+ if (s.args.length !== p.params.length) host.fail(`${s.name} takes ${p.params.length} argument(s)`, s.span);
130
+ const callee: Env = { consts: new Map(env.consts), locals: new Map(), calls: [...env.calls, s.span.start.line] };
131
+ const setup: IStmt[] = [];
132
+ p.params.forEach((param, i) => {
133
+ const arg = s.args[i];
134
+ if (isConstant(arg)) {
135
+ callee.consts.set(param.name, host.tr(arg, new Map(), sensorConsts));
136
+ } else {
137
+ const local = `${param.name}_${fresh++}`;
138
+ locals.set(local, { type: host.type(param.type), span: param.span });
139
+ callee.locals.set(param.name, local);
140
+ setup.push({ span: arg.span, env, k: 'set', local, value: arg });
141
+ }
142
+ });
143
+ return [...setup, ...inline(p.body, callee, [...stack, s.name])];
144
+ }
145
+ }
146
+ });
147
+ const program = inline(main, { consts: new Map(), locals: new Map(), calls: [] }, []);
148
+
149
+ // 2. Resume points
150
+ type Resume = { pc: string; cont: Frame[]; line: number; calls: number[] };
151
+ const resumes = new Map<IStmt, Resume>();
152
+ const halts = new Map<IStmt, string>();
153
+ const order: Resume[] = [];
154
+ // Program-counter values are numbered by kind in program order, not by line, so that an
155
+ // edit that only moves lines (a comment) keeps the experiment, its states and Lean names.
156
+ const counts = new Map<string, number>();
157
+ const pcName = (prefix: string, _line: number) => {
158
+ const k = (counts.get(prefix) ?? 0) + 1;
159
+ counts.set(prefix, k);
160
+ return `${prefix}${k}`;
161
+ };
162
+ const typedLocals = () => [...locals].filter(([, l]) => l.type).map(([n, l]) => ({ name: n, type: l.type! }));
163
+ const ended = (cont: Frame[]) => cont.every(f => f.rest.length === 0 && !f.loop);
164
+
165
+ type State = { pub: string; values: Map<string, string> };
166
+ let tmp = 0;
167
+ /** The type of the program counter; its constructors are known at the end. */
168
+ const PC = { k: 'enum' as const, name: `${name}PC`, ctors: [] as string[] };
169
+
170
+ /** The decision ending this activation, resuming at `pc` (with continuation `cont`), with the activation's actions. */
171
+ const decision = (move: Expr, pc: string, st: State, span: Span, actions: Record<string, Expr> = {}, cont: Frame[] = [], line: number | null = span?.start.line ?? null): Expr => {
172
+ const mem: Record<string, Expr> = {};
173
+ const restart = pc === 'start';
174
+ for (const l of typedLocals()) {
175
+ // At a restart nothing is kept: locals go back to their initial values, so equal states stay equal.
176
+ mem[l.name] = restart
177
+ ? { e: 'lit', type: l.type, value: defaultValue(l.type, host, span) }
178
+ : { e: 'var', name: st.values.get(l.name) ?? host.fail('internal: lost local', span) };
179
+ }
180
+ if (!restart) {
181
+ for (const [l, info] of locals) {
182
+ // An untyped local only matters if what follows reads it before writing it.
183
+ if (!info.type && st.values.has(l) && live(cont, l) === 'read') {
184
+ host.fail(`${l.replace(/_\d+$/, '')} is kept across a move: give its type (let x : T := ...)`, info.span);
185
+ }
186
+ }
187
+ }
188
+ return {
189
+ e: 'record',
190
+ fields: { move, pub: { e: 'var', name: st.pub }, mem: { e: 'record', fields: { pc: { e: 'lit', type: PC, value: pc }, ...mem } }, ...actions },
191
+ // The statement that decides (display only, see `Expr.at`).
192
+ ...(line !== null ? { at: line } : {}),
193
+ };
194
+ };
195
+ const actionsOf = (s: { actions?: SRecordItem[]; env: Env }, st: State): Record<string, Expr> => {
196
+ const out: Record<string, Expr> = {};
197
+ for (const item of s.actions ?? []) {
198
+ if (item.t !== 'field' || !item.value) continue;
199
+ if (['move', 'pub', 'mem'].includes(item.name)) host.fail(`${item.name} is not an action`, item.span);
200
+ out[item.name] = tr(item.value, s.env, st);
201
+ }
202
+ return out;
203
+ };
204
+
205
+ const scopeOf = (env: Env, st: State) => {
206
+ const scope = new Map<string, string>([['pub', st.pub]]);
207
+ for (const [surface, local] of env.locals) {
208
+ const v = st.values.get(local);
209
+ if (v) scope.set(surface, v);
210
+ }
211
+ return scope;
212
+ };
213
+ const tr = (e: SExpr, env: Env, st: State) => host.tr(e, scopeOf(env, st), new Map([...sensorConsts, ...env.consts]));
214
+
215
+ const run = (cont: Frame[], st: State, seen: Set<IStmt>): Expr => {
216
+ if (!cont.length) {
217
+ // End of `main`: nothing more this activation; start again at the next one.
218
+ return decision({ e: 'lit', type: T.move, value: 'stay' }, 'start', st, program[0]?.span ?? main[0]?.span, {}, [], null);
219
+ }
220
+ const [frame, ...outer] = cont;
221
+ if (!frame.rest.length) return run(frame.loop ? [{ rest: [frame.loop] }, ...outer] : outer, st, seen);
222
+ const [s, ...rest] = frame.rest;
223
+ const next: Frame[] = [{ rest, loop: frame.loop }, ...outer];
224
+ switch (s.k) {
225
+ case 'move': case 'stay': {
226
+ const move = s.k === 'move' ? tr(s.dir, s.env, st) : { e: 'lit', type: T.move, value: 'stay' } as Expr;
227
+ let pc = 'start';
228
+ if (!ended(next)) {
229
+ let r = resumes.get(s);
230
+ if (!r) {
231
+ r = { pc: pcName(s.k === 'move' ? 'move' : 'stay', s.span.start.line), cont: next, line: s.span.start.line, calls: s.env.calls };
232
+ resumes.set(s, r);
233
+ order.push(r);
234
+ }
235
+ pc = r.pc;
236
+ if (oblivious) host.fail(`${name}: in an oblivious model, the algorithm cannot continue after this ${s.k} (it would need memory)`, s.span);
237
+ }
238
+ return decision(move, pc, st, s.span, actionsOf(s, st), next);
239
+ }
240
+ case 'halt': {
241
+ if (oblivious) host.fail(`${name}: in an oblivious model, an algorithm cannot halt (it would need memory)`, s.span);
242
+ let pc = halts.get(s);
243
+ if (!pc) { pc = pcName('halt', s.span.start.line); halts.set(s, pc); }
244
+ return decision({ e: 'lit', type: T.move, value: 'stay' }, pc, st, s.span, actionsOf(s, st));
245
+ }
246
+ case 'set': {
247
+ const value = tr(s.value, s.env, st);
248
+ const v = `${s.local === 'pub' ? 'pub' : s.local.replace(/_\d+$/, '')}_v${tmp++}`;
249
+ const st2: State = s.local === 'pub' ? { ...st, pub: v } : { ...st, values: new Map(st.values).set(s.local, v) };
250
+ return { e: 'let', name: v, value, body: run(next, st2, seen) };
251
+ }
252
+ case 'if':
253
+ return {
254
+ e: 'if', cond: tr(s.cond, s.env, st),
255
+ then: run([{ rest: s.then }, ...next], st, seen),
256
+ else: run([{ rest: s.else }, ...next], st, seen),
257
+ };
258
+ case 'repeat': case 'while': {
259
+ if (seen.has(s)) host.fail(`this loop can run again without move, stay or halt`, s.span);
260
+ const seen2 = new Set(seen).add(s);
261
+ const body = run([{ rest: s.body, loop: s }, ...next], st, seen2);
262
+ return s.k === 'repeat' ? body : { e: 'if', cond: tr(s.cond, s.env, st), then: body, else: run(next, st, seen) };
263
+ }
264
+ }
265
+ };
266
+
267
+ // Liveness of locals across a move: is the local read before it is written?
268
+ type Use = 'read' | 'written' | 'none';
269
+ const readsIn = (e: unknown, env: Env, local: string): boolean => {
270
+ if (!e || typeof e !== 'object') return false;
271
+ if (Array.isArray(e)) return e.some(x => readsIn(x, env, local));
272
+ const o = e as { t?: string; name?: string };
273
+ if (o.t === 'name' && o.name && env.locals.get(o.name) === local) return true;
274
+ return Object.entries(o).some(([k, v]) => k !== 'span' && readsIn(v, env, local));
275
+ };
276
+ const seq = (stmts: IStmt[], local: string, loops: Set<IStmt>): Use => {
277
+ for (const s of stmts) {
278
+ const u = stmt(s, local, loops);
279
+ if (u !== 'none') return u;
280
+ }
281
+ return 'none';
282
+ };
283
+ const stmt = (s: IStmt, local: string, loops: Set<IStmt>): Use => {
284
+ switch (s.k) {
285
+ case 'move': return readsIn(s.dir, s.env, local) || readsIn(s.actions, s.env, local) ? 'read' : 'none';
286
+ case 'stay': case 'halt': return readsIn(s.actions, s.env, local) ? 'read' : 'none';
287
+ case 'set':
288
+ if (readsIn(s.value, s.env, local)) return 'read';
289
+ return s.local === local ? 'written' : 'none';
290
+ case 'if': {
291
+ if (readsIn(s.cond, s.env, local)) return 'read';
292
+ const a = seq(s.then, local, loops), b = seq(s.else, local, loops);
293
+ return a === 'read' || b === 'read' ? 'read' : a === 'written' && b === 'written' ? 'written' : 'none';
294
+ }
295
+ case 'repeat': case 'while': {
296
+ if (loops.has(s)) return 'none';
297
+ const inner = new Set(loops).add(s);
298
+ if (s.k === 'while' && readsIn(s.cond, s.env, local)) return 'read';
299
+ const u = seq(s.body, local, inner);
300
+ return u === 'read' ? 'read' : s.k === 'repeat' ? u : 'none';
301
+ }
302
+ }
303
+ };
304
+ const live = (cont: Frame[], local: string): Use => {
305
+ const loops = new Set<IStmt>();
306
+ for (const f of cont) {
307
+ const u = seq(f.rest, local, loops);
308
+ if (u !== 'none') return u;
309
+ if (f.loop) {
310
+ const again = stmt(f.loop, local, loops);
311
+ if (again !== 'none') return again;
312
+ loops.add(f.loop);
313
+ }
314
+ }
315
+ return 'none';
316
+ };
317
+
318
+ // 3. The step function: one case per resume point, generated until no new one appears.
319
+ const atResume = (): State => ({ pub: 'pub', values: new Map(typedLocals().map(l => [l.name, `${l.name}_m`])) });
320
+ /** Reads the saved locals from memory. */
321
+ const withLocals = (body: Expr): Expr => typedLocals().reduceRight<Expr>((acc, l) =>
322
+ ({ e: 'let', name: `${l.name}_m`, value: { e: 'get', on: { e: 'var', name: 'mem' }, field: l.name }, body: acc }), body);
323
+ const cases: Record<string, Expr> = { start: withLocals(run([{ rest: program }], atResume(), new Set())) };
324
+ for (let i = 0; i < order.length; i++) cases[order[i].pc] = withLocals(run(order[i].cont, atResume(), new Set()));
325
+ for (const [s, pc] of halts) cases[pc] = withLocals(decision({ e: 'lit', type: T.move, value: 'stay' }, pc, atResume(), s.span));
326
+
327
+ const pcs = Object.keys(cases);
328
+ const lines: Record<string, number> = { start: (program[0] ?? main[0])?.span.start.line ?? 1 };
329
+ for (const r of order) lines[r.pc] = r.line;
330
+ for (const [s, pc] of halts) lines[pc] = s.span.start.line;
331
+ const calls: Record<string, number[]> = {};
332
+ const addCalls = (pc: string, c: number[] | undefined) => { if (c?.length) calls[pc] = c; };
333
+ addCalls('start', program[0]?.env.calls);
334
+ for (const r of order) addCalls(r.pc, r.calls);
335
+ for (const [s, pc] of halts) addCalls(pc, s.env.calls);
336
+ const locs = typedLocals();
337
+ if (pcs.length === 1 && !locs.length) {
338
+ // No memory: the program restarts at every activation.
339
+ const step = stripMem(cases.start);
340
+ return { mem: T.unit, initMem: { e: 'lit', type: T.unit, value: null }, step, lines, calls };
341
+ }
342
+ if (oblivious && locs.length) host.fail(`${name}: in an oblivious model, locals cannot be kept across activations`, locals.get(locs[0].name)!.span);
343
+ PC.ctors.push(...pcs);
344
+ const fields: Field[] = [{ name: 'pc', type: PC }, ...locs];
345
+ return {
346
+ mem: T.record(fields),
347
+ initMem: { e: 'record', fields: { pc: { e: 'lit', type: PC, value: 'start' }, ...Object.fromEntries(locs.map(l => [l.name, { e: 'lit', type: l.type, value: defaultValue(l.type, host, locals.get(l.name)!.span) }])) } },
348
+ step: { e: 'match', on: { e: 'get', on: { e: 'var', name: 'mem' }, field: 'pc' }, cases },
349
+ line: { e: 'match', on: { e: 'get', on: { e: 'var', name: 'mem' }, field: 'pc' }, cases: Object.fromEntries(pcs.map(pc => [pc, { e: 'lit', type: T.nat, value: lines[pc] }])) },
350
+ lines,
351
+ calls,
352
+ };
353
+ }
354
+
355
+ /**
356
+ * Decisions that omit an action keep its default, but the IR (and Lean) need every decision of
357
+ * a step to have the same fields: once the model is known, each action that some decision sets
358
+ * is added, with its default, to the decisions that omit it. Steps that set no action are
359
+ * unchanged.
360
+ */
361
+ export function fillActionDefaults(step: Expr, actions: { name: string; type: Type; default: Json }[]): Expr {
362
+ const isDecision = (x: Expr): x is Extract<Expr, { e: 'record' }> => x.e === 'record' && !!x.fields.move && !!x.fields.pub && !!x.fields.mem;
363
+ const used = new Set<string>();
364
+ mapExprs(step, x => {
365
+ if (isDecision(x)) for (const a of actions) if (x.fields[a.name]) used.add(a.name);
366
+ return x;
367
+ });
368
+ if (!used.size) return step;
369
+ return mapExprs(step, x => {
370
+ if (!isDecision(x)) return x;
371
+ const fields = { ...x.fields };
372
+ for (const a of actions) if (used.has(a.name) && !fields[a.name]) fields[a.name] = { e: 'lit', type: a.type, value: a.default };
373
+ return { ...x, fields };
374
+ });
375
+ }
376
+
377
+ /** Without memory, decisions do not mention it. */
378
+ function stripMem(e: Expr): Expr {
379
+ return mapExprs(e, x => x.e === 'record' && x.fields.mem && x.fields.move
380
+ ? { ...x, fields: { ...x.fields, mem: { e: 'var', name: 'mem' } } } : x);
381
+ }
382
+
383
+ function mapExprs(e: Expr, f: (x: Expr) => Expr): Expr {
384
+ const walk = (x: unknown): unknown => {
385
+ if (!x || typeof x !== 'object') return x;
386
+ if (Array.isArray(x)) return x.map(walk);
387
+ const o = Object.fromEntries(Object.entries(x as object).map(([k, v]) => [k, walk(v)]));
388
+ return typeof (o as { e?: unknown }).e === 'string' ? f(o as Expr) : o;
389
+ };
390
+ return walk(e) as Expr;
391
+ }
392
+
393
+ function defaultValue(t: Type, host: LowerHost, span: Span): Json {
394
+ switch (t.k) {
395
+ case 'nat': case 'int': return 0;
396
+ case 'bool': case 'frame': return false;
397
+ case 'string': return '';
398
+ case 'unit': case 'option': return null;
399
+ case 'list': case 'multiset': return [];
400
+ case 'move': return 'stay';
401
+ case 'enum': return t.ctors[0];
402
+ case 'record': return Object.fromEntries(t.fields.map(f => [f.name, defaultValue(f.type, host, span)]));
403
+ default: return host.fail(`a local of type ${t.k} cannot be kept in memory`, span);
404
+ }
405
+ }
@@ -0,0 +1,32 @@
1
+ /** Node.js helpers: reading `.sim` programs from disk. Not for the browser. */
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join, resolve, sep } from 'node:path';
4
+ import { createRequire } from 'node:module';
5
+ import { Elaborator } from './elaborate';
6
+ import { Program, type SourceHost } from './program';
7
+
8
+ /** Resolve data through its package, both in the workspace and in an npm installation. */
9
+ export const stdlibDir = join(dirname(createRequire(import.meta.url).resolve('@bramas/simulant-stdlib/package.json')), 'sim') + sep;
10
+
11
+ /** A host reading files from disk; `std/...` imports resolve in `stdlib`. */
12
+ export function nodeHost(stdlib = stdlibDir): SourceHost {
13
+ return {
14
+ read: p => {
15
+ try {
16
+ return readFileSync(p, 'utf8');
17
+ } catch {
18
+ return undefined;
19
+ }
20
+ },
21
+ resolve: (from, spec) => spec.startsWith('std/') ? join(stdlib, spec.slice(4)) : resolve(dirname(from), spec),
22
+ };
23
+ }
24
+
25
+ export function loadProgram(entry: string, stdlib = stdlibDir): Program {
26
+ return new Program(nodeHost(stdlib), entry);
27
+ }
28
+
29
+ /** An elaborator over a module of the standard library (`models.sim` imports all components and sensors). */
30
+ export function stdlibElaborator(module = 'models.sim'): Elaborator {
31
+ return new Elaborator(loadProgram(join(stdlibDir, module)));
32
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Loading a `.sim` program: an entry module and everything it imports.
3
+ * Imports are relative paths (`"./models.sim"`) or standard-library paths
4
+ * (`"std/models.sim"`). Every declaration of an imported module is visible.
5
+ */
6
+ import type { SDecl, SFile } from '../parse/ast';
7
+ import { LexError, type Span } from '../parse/lexer';
8
+ import { parse, ParseError } from '../parse/parser';
9
+
10
+ export type Diagnostic = { message: string; span?: Span };
11
+
12
+ export interface SourceHost {
13
+ read(path: string): string | undefined;
14
+ /** The path of module `spec` imported from `from`. */
15
+ resolve(from: string, spec: string): string;
16
+ }
17
+
18
+ export class Program {
19
+ readonly modules = new Map<string, SFile>();
20
+ readonly diagnostics: Diagnostic[] = [];
21
+ /** Every file read, including those that failed: watched for changes. */
22
+ readonly files = new Set<string>();
23
+
24
+ constructor(readonly host: SourceHost, readonly entry: string) {
25
+ this.load(entry);
26
+ }
27
+
28
+ private load(path: string, importedAt?: Span) {
29
+ if (this.modules.has(path) || this.files.has(path)) return;
30
+ this.files.add(path);
31
+ const source = this.host.read(path);
32
+ if (source === undefined) {
33
+ this.diagnostics.push({ message: `cannot read module ${path}`, span: importedAt });
34
+ return;
35
+ }
36
+ let file: SFile;
37
+ try {
38
+ file = parse(source, path);
39
+ } catch (e) {
40
+ if (e instanceof ParseError || e instanceof LexError) {
41
+ this.diagnostics.push({ message: e.message, span: e.span });
42
+ return;
43
+ }
44
+ throw e;
45
+ }
46
+ this.modules.set(path, file);
47
+ for (const d of file.decls) {
48
+ if (d.d === 'import') this.load(this.host.resolve(path, d.path), d.span);
49
+ }
50
+ }
51
+
52
+ /** All declarations of the program, entry module last. */
53
+ decls(): SDecl[] {
54
+ return [...this.modules.values()].reverse().flatMap(f => f.decls);
55
+ }
56
+ }