@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.
- package/LICENCE.txt +515 -0
- package/dist/builder.js +221 -0
- package/dist/builtins.js +156 -0
- package/dist/check.js +540 -0
- package/dist/elab/elaborate.js +1094 -0
- package/dist/elab/lower.js +372 -0
- package/dist/elab/node.js +29 -0
- package/dist/elab/program.js +45 -0
- package/dist/ir.js +9 -0
- package/dist/layout.js +56 -0
- package/dist/parse/ast.js +1 -0
- package/dist/parse/lexer.js +151 -0
- package/dist/parse/parser.js +825 -0
- package/dist/resolve.js +31 -0
- package/dist/standalone.js +45 -0
- package/dist/types.js +109 -0
- package/package.json +27 -3
- package/src/builder.ts +358 -0
- package/src/builtins.ts +191 -0
- package/src/check.ts +508 -0
- package/src/elab/elaborate.ts +1045 -0
- package/src/elab/lower.ts +405 -0
- package/src/elab/node.ts +32 -0
- package/src/elab/program.ts +56 -0
- package/src/ir.ts +223 -0
- package/src/layout.ts +77 -0
- package/src/parse/ast.ts +89 -0
- package/src/parse/lexer.ts +146 -0
- package/src/parse/parser.ts +746 -0
- package/src/resolve.ts +35 -0
- package/src/standalone.ts +47 -0
- package/src/types.ts +109 -0
- package/README.md +0 -3
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { T } from "../types.js";
|
|
2
|
+
/** Expressions that can be substituted for a parameter without memory. */
|
|
3
|
+
function isConstant(e) {
|
|
4
|
+
switch (e.t) {
|
|
5
|
+
case 'num':
|
|
6
|
+
case 'str':
|
|
7
|
+
case 'bool':
|
|
8
|
+
case 'ctor':
|
|
9
|
+
case 'unit': return true;
|
|
10
|
+
case 'name': return /^[A-Z]\w*\.\w+$/.test(e.name); // Move.cw
|
|
11
|
+
case 'list': return e.items.every(isConstant);
|
|
12
|
+
case 'unop': return e.op === '-' && e.e.t === 'num';
|
|
13
|
+
default: return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function lower(name, procs, main, sensors, oblivious, host) {
|
|
17
|
+
const locals = new Map();
|
|
18
|
+
let fresh = 0;
|
|
19
|
+
const sensorConsts = new Map(sensors.map(s => [s, { e: 'get', on: { e: 'var', name: 'view' }, field: s }]));
|
|
20
|
+
// 1. Inlining
|
|
21
|
+
let waitLocal;
|
|
22
|
+
const inline = (stmts, env, stack) => stmts.flatMap((s) => {
|
|
23
|
+
const at = { span: s.span, env };
|
|
24
|
+
switch (s.s) {
|
|
25
|
+
case 'move': return [{ ...at, k: 'move', dir: s.dir, actions: s.actions }];
|
|
26
|
+
case 'stay': return [{ ...at, k: 'stay', actions: s.actions }];
|
|
27
|
+
case 'halt': return [{ ...at, k: 'halt', actions: s.actions }];
|
|
28
|
+
case 'wait': {
|
|
29
|
+
if (!waitLocal) {
|
|
30
|
+
waitLocal = `wait_${fresh++}`;
|
|
31
|
+
locals.set(waitLocal, { type: T.nat, span: s.span });
|
|
32
|
+
}
|
|
33
|
+
// `#wait` is not a surface name: the counter cannot clash with the algorithm's names.
|
|
34
|
+
const w = { ...env, locals: new Map(env.locals).set('#wait', waitLocal) };
|
|
35
|
+
const span = s.span;
|
|
36
|
+
const left = { t: 'name', name: '#wait', span };
|
|
37
|
+
return [
|
|
38
|
+
{ span, env, k: 'set', local: waitLocal, value: s.count },
|
|
39
|
+
{ span, env: w, k: 'while', cond: { t: 'binop', op: '>', l: left, r: { t: 'num', value: 0, span }, span }, body: [
|
|
40
|
+
{ span, env: w, k: 'set', local: waitLocal, value: { t: 'binop', op: '-', l: left, r: { t: 'num', value: 1, span }, span } },
|
|
41
|
+
{ span, env: w, k: 'stay' },
|
|
42
|
+
] },
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
case 'repeat': return [{ ...at, k: 'repeat', body: inline(s.body, env, stack) }];
|
|
46
|
+
case 'while': return [{ ...at, k: 'while', cond: s.cond, body: inline(s.body, env, stack) }];
|
|
47
|
+
case 'if': return [{ ...at, k: 'if', cond: s.cond, then: inline(s.then, env, stack), else: inline(s.else ?? [], env, stack) }];
|
|
48
|
+
case 'let': {
|
|
49
|
+
// Locals are scoped to their procedure (one declaration per name).
|
|
50
|
+
if (env.locals.has(s.name))
|
|
51
|
+
host.fail(`${s.name} is already declared in this procedure`, s.span);
|
|
52
|
+
const local = `${s.name}_${fresh++}`;
|
|
53
|
+
locals.set(local, { type: s.type && host.type(s.type), span: s.span });
|
|
54
|
+
env.locals.set(s.name, local);
|
|
55
|
+
return [{ ...at, k: 'set', local, value: s.value }];
|
|
56
|
+
}
|
|
57
|
+
case 'assign': {
|
|
58
|
+
if (s.name === 'pub')
|
|
59
|
+
return [{ ...at, k: 'set', local: 'pub', value: s.value }];
|
|
60
|
+
const local = env.locals.get(s.name) ?? host.fail(`${s.name} is not a local variable (declare it with let)`, s.span);
|
|
61
|
+
return [{ ...at, k: 'set', local, value: s.value }];
|
|
62
|
+
}
|
|
63
|
+
case 'call': {
|
|
64
|
+
const p = procs.get(s.name) ?? host.fail(`unknown procedure ${s.name}`, s.span);
|
|
65
|
+
if (stack.includes(s.name))
|
|
66
|
+
host.fail(`procedures cannot be recursive: ${[...stack, s.name].join(' → ')}`, s.span);
|
|
67
|
+
if (s.args.length !== p.params.length)
|
|
68
|
+
host.fail(`${s.name} takes ${p.params.length} argument(s)`, s.span);
|
|
69
|
+
const callee = { consts: new Map(env.consts), locals: new Map(), calls: [...env.calls, s.span.start.line] };
|
|
70
|
+
const setup = [];
|
|
71
|
+
p.params.forEach((param, i) => {
|
|
72
|
+
const arg = s.args[i];
|
|
73
|
+
if (isConstant(arg)) {
|
|
74
|
+
callee.consts.set(param.name, host.tr(arg, new Map(), sensorConsts));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const local = `${param.name}_${fresh++}`;
|
|
78
|
+
locals.set(local, { type: host.type(param.type), span: param.span });
|
|
79
|
+
callee.locals.set(param.name, local);
|
|
80
|
+
setup.push({ span: arg.span, env, k: 'set', local, value: arg });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return [...setup, ...inline(p.body, callee, [...stack, s.name])];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const program = inline(main, { consts: new Map(), locals: new Map(), calls: [] }, []);
|
|
88
|
+
const resumes = new Map();
|
|
89
|
+
const halts = new Map();
|
|
90
|
+
const order = [];
|
|
91
|
+
// Program-counter values are numbered by kind in program order, not by line, so that an
|
|
92
|
+
// edit that only moves lines (a comment) keeps the experiment, its states and Lean names.
|
|
93
|
+
const counts = new Map();
|
|
94
|
+
const pcName = (prefix, _line) => {
|
|
95
|
+
const k = (counts.get(prefix) ?? 0) + 1;
|
|
96
|
+
counts.set(prefix, k);
|
|
97
|
+
return `${prefix}${k}`;
|
|
98
|
+
};
|
|
99
|
+
const typedLocals = () => [...locals].filter(([, l]) => l.type).map(([n, l]) => ({ name: n, type: l.type }));
|
|
100
|
+
const ended = (cont) => cont.every(f => f.rest.length === 0 && !f.loop);
|
|
101
|
+
let tmp = 0;
|
|
102
|
+
/** The type of the program counter; its constructors are known at the end. */
|
|
103
|
+
const PC = { k: 'enum', name: `${name}PC`, ctors: [] };
|
|
104
|
+
/** The decision ending this activation, resuming at `pc` (with continuation `cont`), with the activation's actions. */
|
|
105
|
+
const decision = (move, pc, st, span, actions = {}, cont = [], line = span?.start.line ?? null) => {
|
|
106
|
+
const mem = {};
|
|
107
|
+
const restart = pc === 'start';
|
|
108
|
+
for (const l of typedLocals()) {
|
|
109
|
+
// At a restart nothing is kept: locals go back to their initial values, so equal states stay equal.
|
|
110
|
+
mem[l.name] = restart
|
|
111
|
+
? { e: 'lit', type: l.type, value: defaultValue(l.type, host, span) }
|
|
112
|
+
: { e: 'var', name: st.values.get(l.name) ?? host.fail('internal: lost local', span) };
|
|
113
|
+
}
|
|
114
|
+
if (!restart) {
|
|
115
|
+
for (const [l, info] of locals) {
|
|
116
|
+
// An untyped local only matters if what follows reads it before writing it.
|
|
117
|
+
if (!info.type && st.values.has(l) && live(cont, l) === 'read') {
|
|
118
|
+
host.fail(`${l.replace(/_\d+$/, '')} is kept across a move: give its type (let x : T := ...)`, info.span);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
e: 'record',
|
|
124
|
+
fields: { move, pub: { e: 'var', name: st.pub }, mem: { e: 'record', fields: { pc: { e: 'lit', type: PC, value: pc }, ...mem } }, ...actions },
|
|
125
|
+
// The statement that decides (display only, see `Expr.at`).
|
|
126
|
+
...(line !== null ? { at: line } : {}),
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
const actionsOf = (s, st) => {
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const item of s.actions ?? []) {
|
|
132
|
+
if (item.t !== 'field' || !item.value)
|
|
133
|
+
continue;
|
|
134
|
+
if (['move', 'pub', 'mem'].includes(item.name))
|
|
135
|
+
host.fail(`${item.name} is not an action`, item.span);
|
|
136
|
+
out[item.name] = tr(item.value, s.env, st);
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
};
|
|
140
|
+
const scopeOf = (env, st) => {
|
|
141
|
+
const scope = new Map([['pub', st.pub]]);
|
|
142
|
+
for (const [surface, local] of env.locals) {
|
|
143
|
+
const v = st.values.get(local);
|
|
144
|
+
if (v)
|
|
145
|
+
scope.set(surface, v);
|
|
146
|
+
}
|
|
147
|
+
return scope;
|
|
148
|
+
};
|
|
149
|
+
const tr = (e, env, st) => host.tr(e, scopeOf(env, st), new Map([...sensorConsts, ...env.consts]));
|
|
150
|
+
const run = (cont, st, seen) => {
|
|
151
|
+
if (!cont.length) {
|
|
152
|
+
// End of `main`: nothing more this activation; start again at the next one.
|
|
153
|
+
return decision({ e: 'lit', type: T.move, value: 'stay' }, 'start', st, program[0]?.span ?? main[0]?.span, {}, [], null);
|
|
154
|
+
}
|
|
155
|
+
const [frame, ...outer] = cont;
|
|
156
|
+
if (!frame.rest.length)
|
|
157
|
+
return run(frame.loop ? [{ rest: [frame.loop] }, ...outer] : outer, st, seen);
|
|
158
|
+
const [s, ...rest] = frame.rest;
|
|
159
|
+
const next = [{ rest, loop: frame.loop }, ...outer];
|
|
160
|
+
switch (s.k) {
|
|
161
|
+
case 'move':
|
|
162
|
+
case 'stay': {
|
|
163
|
+
const move = s.k === 'move' ? tr(s.dir, s.env, st) : { e: 'lit', type: T.move, value: 'stay' };
|
|
164
|
+
let pc = 'start';
|
|
165
|
+
if (!ended(next)) {
|
|
166
|
+
let r = resumes.get(s);
|
|
167
|
+
if (!r) {
|
|
168
|
+
r = { pc: pcName(s.k === 'move' ? 'move' : 'stay', s.span.start.line), cont: next, line: s.span.start.line, calls: s.env.calls };
|
|
169
|
+
resumes.set(s, r);
|
|
170
|
+
order.push(r);
|
|
171
|
+
}
|
|
172
|
+
pc = r.pc;
|
|
173
|
+
if (oblivious)
|
|
174
|
+
host.fail(`${name}: in an oblivious model, the algorithm cannot continue after this ${s.k} (it would need memory)`, s.span);
|
|
175
|
+
}
|
|
176
|
+
return decision(move, pc, st, s.span, actionsOf(s, st), next);
|
|
177
|
+
}
|
|
178
|
+
case 'halt': {
|
|
179
|
+
if (oblivious)
|
|
180
|
+
host.fail(`${name}: in an oblivious model, an algorithm cannot halt (it would need memory)`, s.span);
|
|
181
|
+
let pc = halts.get(s);
|
|
182
|
+
if (!pc) {
|
|
183
|
+
pc = pcName('halt', s.span.start.line);
|
|
184
|
+
halts.set(s, pc);
|
|
185
|
+
}
|
|
186
|
+
return decision({ e: 'lit', type: T.move, value: 'stay' }, pc, st, s.span, actionsOf(s, st));
|
|
187
|
+
}
|
|
188
|
+
case 'set': {
|
|
189
|
+
const value = tr(s.value, s.env, st);
|
|
190
|
+
const v = `${s.local === 'pub' ? 'pub' : s.local.replace(/_\d+$/, '')}_v${tmp++}`;
|
|
191
|
+
const st2 = s.local === 'pub' ? { ...st, pub: v } : { ...st, values: new Map(st.values).set(s.local, v) };
|
|
192
|
+
return { e: 'let', name: v, value, body: run(next, st2, seen) };
|
|
193
|
+
}
|
|
194
|
+
case 'if':
|
|
195
|
+
return {
|
|
196
|
+
e: 'if', cond: tr(s.cond, s.env, st),
|
|
197
|
+
then: run([{ rest: s.then }, ...next], st, seen),
|
|
198
|
+
else: run([{ rest: s.else }, ...next], st, seen),
|
|
199
|
+
};
|
|
200
|
+
case 'repeat':
|
|
201
|
+
case 'while': {
|
|
202
|
+
if (seen.has(s))
|
|
203
|
+
host.fail(`this loop can run again without move, stay or halt`, s.span);
|
|
204
|
+
const seen2 = new Set(seen).add(s);
|
|
205
|
+
const body = run([{ rest: s.body, loop: s }, ...next], st, seen2);
|
|
206
|
+
return s.k === 'repeat' ? body : { e: 'if', cond: tr(s.cond, s.env, st), then: body, else: run(next, st, seen) };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const readsIn = (e, env, local) => {
|
|
211
|
+
if (!e || typeof e !== 'object')
|
|
212
|
+
return false;
|
|
213
|
+
if (Array.isArray(e))
|
|
214
|
+
return e.some(x => readsIn(x, env, local));
|
|
215
|
+
const o = e;
|
|
216
|
+
if (o.t === 'name' && o.name && env.locals.get(o.name) === local)
|
|
217
|
+
return true;
|
|
218
|
+
return Object.entries(o).some(([k, v]) => k !== 'span' && readsIn(v, env, local));
|
|
219
|
+
};
|
|
220
|
+
const seq = (stmts, local, loops) => {
|
|
221
|
+
for (const s of stmts) {
|
|
222
|
+
const u = stmt(s, local, loops);
|
|
223
|
+
if (u !== 'none')
|
|
224
|
+
return u;
|
|
225
|
+
}
|
|
226
|
+
return 'none';
|
|
227
|
+
};
|
|
228
|
+
const stmt = (s, local, loops) => {
|
|
229
|
+
switch (s.k) {
|
|
230
|
+
case 'move': return readsIn(s.dir, s.env, local) || readsIn(s.actions, s.env, local) ? 'read' : 'none';
|
|
231
|
+
case 'stay':
|
|
232
|
+
case 'halt': return readsIn(s.actions, s.env, local) ? 'read' : 'none';
|
|
233
|
+
case 'set':
|
|
234
|
+
if (readsIn(s.value, s.env, local))
|
|
235
|
+
return 'read';
|
|
236
|
+
return s.local === local ? 'written' : 'none';
|
|
237
|
+
case 'if': {
|
|
238
|
+
if (readsIn(s.cond, s.env, local))
|
|
239
|
+
return 'read';
|
|
240
|
+
const a = seq(s.then, local, loops), b = seq(s.else, local, loops);
|
|
241
|
+
return a === 'read' || b === 'read' ? 'read' : a === 'written' && b === 'written' ? 'written' : 'none';
|
|
242
|
+
}
|
|
243
|
+
case 'repeat':
|
|
244
|
+
case 'while': {
|
|
245
|
+
if (loops.has(s))
|
|
246
|
+
return 'none';
|
|
247
|
+
const inner = new Set(loops).add(s);
|
|
248
|
+
if (s.k === 'while' && readsIn(s.cond, s.env, local))
|
|
249
|
+
return 'read';
|
|
250
|
+
const u = seq(s.body, local, inner);
|
|
251
|
+
return u === 'read' ? 'read' : s.k === 'repeat' ? u : 'none';
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
const live = (cont, local) => {
|
|
256
|
+
const loops = new Set();
|
|
257
|
+
for (const f of cont) {
|
|
258
|
+
const u = seq(f.rest, local, loops);
|
|
259
|
+
if (u !== 'none')
|
|
260
|
+
return u;
|
|
261
|
+
if (f.loop) {
|
|
262
|
+
const again = stmt(f.loop, local, loops);
|
|
263
|
+
if (again !== 'none')
|
|
264
|
+
return again;
|
|
265
|
+
loops.add(f.loop);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return 'none';
|
|
269
|
+
};
|
|
270
|
+
// 3. The step function: one case per resume point, generated until no new one appears.
|
|
271
|
+
const atResume = () => ({ pub: 'pub', values: new Map(typedLocals().map(l => [l.name, `${l.name}_m`])) });
|
|
272
|
+
/** Reads the saved locals from memory. */
|
|
273
|
+
const withLocals = (body) => typedLocals().reduceRight((acc, l) => ({ e: 'let', name: `${l.name}_m`, value: { e: 'get', on: { e: 'var', name: 'mem' }, field: l.name }, body: acc }), body);
|
|
274
|
+
const cases = { start: withLocals(run([{ rest: program }], atResume(), new Set())) };
|
|
275
|
+
for (let i = 0; i < order.length; i++)
|
|
276
|
+
cases[order[i].pc] = withLocals(run(order[i].cont, atResume(), new Set()));
|
|
277
|
+
for (const [s, pc] of halts)
|
|
278
|
+
cases[pc] = withLocals(decision({ e: 'lit', type: T.move, value: 'stay' }, pc, atResume(), s.span));
|
|
279
|
+
const pcs = Object.keys(cases);
|
|
280
|
+
const lines = { start: (program[0] ?? main[0])?.span.start.line ?? 1 };
|
|
281
|
+
for (const r of order)
|
|
282
|
+
lines[r.pc] = r.line;
|
|
283
|
+
for (const [s, pc] of halts)
|
|
284
|
+
lines[pc] = s.span.start.line;
|
|
285
|
+
const calls = {};
|
|
286
|
+
const addCalls = (pc, c) => { if (c?.length)
|
|
287
|
+
calls[pc] = c; };
|
|
288
|
+
addCalls('start', program[0]?.env.calls);
|
|
289
|
+
for (const r of order)
|
|
290
|
+
addCalls(r.pc, r.calls);
|
|
291
|
+
for (const [s, pc] of halts)
|
|
292
|
+
addCalls(pc, s.env.calls);
|
|
293
|
+
const locs = typedLocals();
|
|
294
|
+
if (pcs.length === 1 && !locs.length) {
|
|
295
|
+
// No memory: the program restarts at every activation.
|
|
296
|
+
const step = stripMem(cases.start);
|
|
297
|
+
return { mem: T.unit, initMem: { e: 'lit', type: T.unit, value: null }, step, lines, calls };
|
|
298
|
+
}
|
|
299
|
+
if (oblivious && locs.length)
|
|
300
|
+
host.fail(`${name}: in an oblivious model, locals cannot be kept across activations`, locals.get(locs[0].name).span);
|
|
301
|
+
PC.ctors.push(...pcs);
|
|
302
|
+
const fields = [{ name: 'pc', type: PC }, ...locs];
|
|
303
|
+
return {
|
|
304
|
+
mem: T.record(fields),
|
|
305
|
+
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) }])) } },
|
|
306
|
+
step: { e: 'match', on: { e: 'get', on: { e: 'var', name: 'mem' }, field: 'pc' }, cases },
|
|
307
|
+
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] }])) },
|
|
308
|
+
lines,
|
|
309
|
+
calls,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Decisions that omit an action keep its default, but the IR (and Lean) need every decision of
|
|
314
|
+
* a step to have the same fields: once the model is known, each action that some decision sets
|
|
315
|
+
* is added, with its default, to the decisions that omit it. Steps that set no action are
|
|
316
|
+
* unchanged.
|
|
317
|
+
*/
|
|
318
|
+
export function fillActionDefaults(step, actions) {
|
|
319
|
+
const isDecision = (x) => x.e === 'record' && !!x.fields.move && !!x.fields.pub && !!x.fields.mem;
|
|
320
|
+
const used = new Set();
|
|
321
|
+
mapExprs(step, x => {
|
|
322
|
+
if (isDecision(x))
|
|
323
|
+
for (const a of actions)
|
|
324
|
+
if (x.fields[a.name])
|
|
325
|
+
used.add(a.name);
|
|
326
|
+
return x;
|
|
327
|
+
});
|
|
328
|
+
if (!used.size)
|
|
329
|
+
return step;
|
|
330
|
+
return mapExprs(step, x => {
|
|
331
|
+
if (!isDecision(x))
|
|
332
|
+
return x;
|
|
333
|
+
const fields = { ...x.fields };
|
|
334
|
+
for (const a of actions)
|
|
335
|
+
if (used.has(a.name) && !fields[a.name])
|
|
336
|
+
fields[a.name] = { e: 'lit', type: a.type, value: a.default };
|
|
337
|
+
return { ...x, fields };
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/** Without memory, decisions do not mention it. */
|
|
341
|
+
function stripMem(e) {
|
|
342
|
+
return mapExprs(e, x => x.e === 'record' && x.fields.mem && x.fields.move
|
|
343
|
+
? { ...x, fields: { ...x.fields, mem: { e: 'var', name: 'mem' } } } : x);
|
|
344
|
+
}
|
|
345
|
+
function mapExprs(e, f) {
|
|
346
|
+
const walk = (x) => {
|
|
347
|
+
if (!x || typeof x !== 'object')
|
|
348
|
+
return x;
|
|
349
|
+
if (Array.isArray(x))
|
|
350
|
+
return x.map(walk);
|
|
351
|
+
const o = Object.fromEntries(Object.entries(x).map(([k, v]) => [k, walk(v)]));
|
|
352
|
+
return typeof o.e === 'string' ? f(o) : o;
|
|
353
|
+
};
|
|
354
|
+
return walk(e);
|
|
355
|
+
}
|
|
356
|
+
function defaultValue(t, host, span) {
|
|
357
|
+
switch (t.k) {
|
|
358
|
+
case 'nat':
|
|
359
|
+
case 'int': return 0;
|
|
360
|
+
case 'bool':
|
|
361
|
+
case 'frame': return false;
|
|
362
|
+
case 'string': return '';
|
|
363
|
+
case 'unit':
|
|
364
|
+
case 'option': return null;
|
|
365
|
+
case 'list':
|
|
366
|
+
case 'multiset': return [];
|
|
367
|
+
case 'move': return 'stay';
|
|
368
|
+
case 'enum': return t.ctors[0];
|
|
369
|
+
case 'record': return Object.fromEntries(t.fields.map(f => [f.name, defaultValue(f.type, host, span)]));
|
|
370
|
+
default: return host.fail(`a local of type ${t.k} cannot be kept in memory`, span);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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.js";
|
|
6
|
+
import { Program } from "./program.js";
|
|
7
|
+
/** Resolve data through its package, both in the workspace and in an npm installation. */
|
|
8
|
+
export const stdlibDir = join(dirname(createRequire(import.meta.url).resolve('@bramas/simulant-stdlib/package.json')), 'sim') + sep;
|
|
9
|
+
/** A host reading files from disk; `std/...` imports resolve in `stdlib`. */
|
|
10
|
+
export function nodeHost(stdlib = stdlibDir) {
|
|
11
|
+
return {
|
|
12
|
+
read: p => {
|
|
13
|
+
try {
|
|
14
|
+
return readFileSync(p, 'utf8');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
resolve: (from, spec) => spec.startsWith('std/') ? join(stdlib, spec.slice(4)) : resolve(dirname(from), spec),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function loadProgram(entry, stdlib = stdlibDir) {
|
|
24
|
+
return new Program(nodeHost(stdlib), entry);
|
|
25
|
+
}
|
|
26
|
+
/** An elaborator over a module of the standard library (`models.sim` imports all components and sensors). */
|
|
27
|
+
export function stdlibElaborator(module = 'models.sim') {
|
|
28
|
+
return new Elaborator(loadProgram(join(stdlibDir, module)));
|
|
29
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { LexError } from "../parse/lexer.js";
|
|
2
|
+
import { parse, ParseError } from "../parse/parser.js";
|
|
3
|
+
export class Program {
|
|
4
|
+
host;
|
|
5
|
+
entry;
|
|
6
|
+
modules = new Map();
|
|
7
|
+
diagnostics = [];
|
|
8
|
+
/** Every file read, including those that failed: watched for changes. */
|
|
9
|
+
files = new Set();
|
|
10
|
+
constructor(host, entry) {
|
|
11
|
+
this.host = host;
|
|
12
|
+
this.entry = entry;
|
|
13
|
+
this.load(entry);
|
|
14
|
+
}
|
|
15
|
+
load(path, importedAt) {
|
|
16
|
+
if (this.modules.has(path) || this.files.has(path))
|
|
17
|
+
return;
|
|
18
|
+
this.files.add(path);
|
|
19
|
+
const source = this.host.read(path);
|
|
20
|
+
if (source === undefined) {
|
|
21
|
+
this.diagnostics.push({ message: `cannot read module ${path}`, span: importedAt });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
let file;
|
|
25
|
+
try {
|
|
26
|
+
file = parse(source, path);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
if (e instanceof ParseError || e instanceof LexError) {
|
|
30
|
+
this.diagnostics.push({ message: e.message, span: e.span });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
this.modules.set(path, file);
|
|
36
|
+
for (const d of file.decls) {
|
|
37
|
+
if (d.d === 'import')
|
|
38
|
+
this.load(this.host.resolve(path, d.path), d.span);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** All declarations of the program, entry module last. */
|
|
42
|
+
decls() {
|
|
43
|
+
return [...this.modules.values()].reverse().flatMap(f => f.decls);
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/ir.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Simulant intermediate representation (plan, Sections 3 and 6.1).
|
|
3
|
+
*
|
|
4
|
+
* Everything here is plain JSON data: the builder (and later the parser)
|
|
5
|
+
* produce it, the engine interprets it, and the Lean generator translates it.
|
|
6
|
+
* Topologies and rounds are not written in the IR: they belong to the
|
|
7
|
+
* semantic core, and the IR only names them.
|
|
8
|
+
*/
|
|
9
|
+
export const IR_VERSION = 1;
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { resolvePub, T } from "./types.js";
|
|
2
|
+
export const MODEL = 'model';
|
|
3
|
+
export function layout(exp) {
|
|
4
|
+
const errors = [];
|
|
5
|
+
const unique = (what, items) => {
|
|
6
|
+
const seen = new Map();
|
|
7
|
+
for (const item of items) {
|
|
8
|
+
const other = seen.get(item.name);
|
|
9
|
+
if (other !== undefined)
|
|
10
|
+
errors.push(`${what} '${item.name}' is declared by both ${other} and ${item.owner}`);
|
|
11
|
+
seen.set(item.name, item.owner);
|
|
12
|
+
}
|
|
13
|
+
return items;
|
|
14
|
+
};
|
|
15
|
+
const owned = (owner, items) => (items ?? []).map(x => ({ ...x, owner }));
|
|
16
|
+
const { model, algorithm } = exp;
|
|
17
|
+
const world = unique('world field', model.components.flatMap(c => owned(c.name, c.world)));
|
|
18
|
+
const agentFields = unique('agent field', model.components.flatMap(c => owned(c.name, c.agent)));
|
|
19
|
+
const flags = unique('flag', model.components.flatMap(c => owned(c.name, c.flags)));
|
|
20
|
+
const sensors = unique('sensor', [...owned(MODEL, model.sensors), ...model.components.flatMap(c => owned(c.name, c.sensors))])
|
|
21
|
+
.map(s => ({ ...s, type: resolvePub(s.type, algorithm.pub) }));
|
|
22
|
+
const actions = unique('action', model.components.flatMap(c => owned(c.name, c.actions)));
|
|
23
|
+
const reserved = new Set(['pos', 'pub', 'mem']);
|
|
24
|
+
for (const f of [...agentFields, ...flags]) {
|
|
25
|
+
if (reserved.has(f.name))
|
|
26
|
+
errors.push(`agent field '${f.name}' of ${f.owner} is reserved`);
|
|
27
|
+
}
|
|
28
|
+
for (const f of agentFields) {
|
|
29
|
+
if (flags.some(g => g.name === f.name))
|
|
30
|
+
errors.push(`'${f.name}' is both an agent field and a flag`);
|
|
31
|
+
}
|
|
32
|
+
for (const a of actions) {
|
|
33
|
+
if (['move', 'pub', 'mem'].includes(a.name))
|
|
34
|
+
errors.push(`action '${a.name}' of ${a.owner} is reserved`);
|
|
35
|
+
}
|
|
36
|
+
const view = [];
|
|
37
|
+
for (const name of algorithm.uses) {
|
|
38
|
+
const sensor = sensors.find(s => s.name === name);
|
|
39
|
+
if (sensor)
|
|
40
|
+
view.push({ name, type: sensor.type });
|
|
41
|
+
else
|
|
42
|
+
errors.push(`algorithm ${algorithm.name} uses sensor '${name}', which the model does not provide`);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
world, agentFields, flags, sensors, actions, errors,
|
|
46
|
+
worldType: T.record(world.map(({ name, type }) => ({ name, type }))),
|
|
47
|
+
pub: algorithm.pub,
|
|
48
|
+
mem: algorithm.mem,
|
|
49
|
+
view: T.record(view),
|
|
50
|
+
decision: T.record([
|
|
51
|
+
{ name: 'move', type: T.move }, { name: 'pub', type: algorithm.pub }, { name: 'mem', type: algorithm.mem },
|
|
52
|
+
...actions.map(({ name, type }) => ({ name, type })),
|
|
53
|
+
]),
|
|
54
|
+
defs: model.defs ?? [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexer of `.sim` sources. Tokens keep their position and whether they start
|
|
3
|
+
* a line: the parser uses columns for layout, as Lean does.
|
|
4
|
+
*/
|
|
5
|
+
export const KEYWORDS = new Set([
|
|
6
|
+
'import', 'def', 'structure', 'inductive', 'sensor', 'task', 'goal', 'algorithm', 'experiment', 'where', 'let',
|
|
7
|
+
'if', 'then', 'else', 'match', 'with', 'fun', 'choose', 'such', 'that', 'labels', 'param', 'flag', 'uses',
|
|
8
|
+
'true', 'false', 'raise', 'nothing', 'action',
|
|
9
|
+
]);
|
|
10
|
+
/** Operators, longest first. Unicode and ASCII spellings are both accepted. */
|
|
11
|
+
const OPERATORS = [
|
|
12
|
+
':=', '=>', '->', '<-', '++', '==', '!=', '<=', '>=', '&&', '||',
|
|
13
|
+
'→', '←', '≠', '≤', '≥', '∧', '∨', '¬', '·', '×',
|
|
14
|
+
'=', '<', '>', '+', '-', '*', '/', '%', '!', '|', ',', ':', ';', '(', ')', '[', ']', '{', '}', '@',
|
|
15
|
+
];
|
|
16
|
+
const NORMALIZE = {
|
|
17
|
+
'->': '→', '<-': '←', '!=': '≠', '<=': '≤', '>=': '≥', '&&': '∧', '||': '∨', '==': '=',
|
|
18
|
+
};
|
|
19
|
+
export class LexError extends Error {
|
|
20
|
+
span;
|
|
21
|
+
constructor(message, span) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.span = span;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const identStart = /[A-Za-z_À-ɏͰ-Ͽ]/;
|
|
27
|
+
const identPart = /[A-Za-z0-9_'À-ɏͰ-Ͽ₀-₉]/;
|
|
28
|
+
export function lex(source, file = '<input>') {
|
|
29
|
+
const tokens = [];
|
|
30
|
+
let i = 0, line = 1, col = 1;
|
|
31
|
+
let lineStart = true, spaced = true;
|
|
32
|
+
const pos = () => ({ line, col, offset: i });
|
|
33
|
+
const advance = (n = 1) => {
|
|
34
|
+
for (let j = 0; j < n; j++) {
|
|
35
|
+
if (source[i] === '\n') {
|
|
36
|
+
line++;
|
|
37
|
+
col = 1;
|
|
38
|
+
}
|
|
39
|
+
else
|
|
40
|
+
col++;
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const fail = (msg, start) => { throw new LexError(msg, { file, start, end: pos() }); };
|
|
45
|
+
while (i < source.length) {
|
|
46
|
+
const c = source[i];
|
|
47
|
+
if (c === '\n') {
|
|
48
|
+
advance();
|
|
49
|
+
lineStart = true;
|
|
50
|
+
spaced = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (c === ' ' || c === '\t' || c === '\r') {
|
|
54
|
+
advance();
|
|
55
|
+
spaced = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (source.startsWith('--', i)) {
|
|
59
|
+
while (i < source.length && source[i] !== '\n')
|
|
60
|
+
advance();
|
|
61
|
+
spaced = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (source.startsWith('/-', i)) {
|
|
65
|
+
const start = pos();
|
|
66
|
+
let depth = 0;
|
|
67
|
+
do {
|
|
68
|
+
if (source.startsWith('/-', i)) {
|
|
69
|
+
depth++;
|
|
70
|
+
advance(2);
|
|
71
|
+
}
|
|
72
|
+
else if (source.startsWith('-/', i)) {
|
|
73
|
+
depth--;
|
|
74
|
+
advance(2);
|
|
75
|
+
}
|
|
76
|
+
else if (i >= source.length)
|
|
77
|
+
fail('unterminated comment', start);
|
|
78
|
+
else
|
|
79
|
+
advance();
|
|
80
|
+
} while (depth > 0);
|
|
81
|
+
spaced = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const start = pos();
|
|
85
|
+
const push = (kind, text) => {
|
|
86
|
+
tokens.push({ kind, text, start, end: pos(), lineStart, spaced });
|
|
87
|
+
lineStart = false;
|
|
88
|
+
spaced = false;
|
|
89
|
+
};
|
|
90
|
+
const prev = tokens[tokens.length - 1];
|
|
91
|
+
const afterTerm = !!prev && !spaced && (prev.kind === 'ident' || prev.kind === 'field' || prev.kind === 'dotIdent'
|
|
92
|
+
|| prev.kind === 'number' || prev.kind === 'string' || [')', ']', '}'].includes(prev.text) || prev.text === '·');
|
|
93
|
+
if (c === '.' && identStart.test(source[i + 1] ?? '')) {
|
|
94
|
+
// `.x` right after a term is a projection; otherwise a constructor.
|
|
95
|
+
advance();
|
|
96
|
+
const s = i;
|
|
97
|
+
while (i < source.length && identPart.test(source[i]))
|
|
98
|
+
advance();
|
|
99
|
+
push(afterTerm ? 'field' : 'dotIdent', source.slice(s, i));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (identStart.test(c)) {
|
|
103
|
+
const s = i;
|
|
104
|
+
while (i < source.length && identPart.test(source[i]))
|
|
105
|
+
advance();
|
|
106
|
+
// A dotted name, as in Lean: `Hazards.BlackHole` or `C.world.site`; name resolution
|
|
107
|
+
// decides which parts are namespaces and which are projections.
|
|
108
|
+
while (source[i] === '.' && identStart.test(source[i + 1] ?? '')) {
|
|
109
|
+
advance();
|
|
110
|
+
while (i < source.length && identPart.test(source[i]))
|
|
111
|
+
advance();
|
|
112
|
+
}
|
|
113
|
+
const text = source.slice(s, i);
|
|
114
|
+
push(KEYWORDS.has(text) ? 'keyword' : 'ident', text);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (/[0-9]/.test(c)) {
|
|
118
|
+
const s = i;
|
|
119
|
+
while (i < source.length && /[0-9]/.test(source[i]))
|
|
120
|
+
advance();
|
|
121
|
+
push('number', source.slice(s, i));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (c === '"') {
|
|
125
|
+
advance();
|
|
126
|
+
let text = '';
|
|
127
|
+
while (source[i] !== '"') {
|
|
128
|
+
if (i >= source.length || source[i] === '\n')
|
|
129
|
+
fail('unterminated string', start);
|
|
130
|
+
if (source[i] === '\\') {
|
|
131
|
+
advance();
|
|
132
|
+
text += source[i] === 'n' ? '\n' : source[i];
|
|
133
|
+
advance();
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
text += source[i];
|
|
137
|
+
advance();
|
|
138
|
+
}
|
|
139
|
+
advance();
|
|
140
|
+
push('string', text);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const op = OPERATORS.find(o => source.startsWith(o, i));
|
|
144
|
+
if (!op)
|
|
145
|
+
fail(`unexpected character '${c}'`, start);
|
|
146
|
+
advance(op.length);
|
|
147
|
+
push('op', NORMALIZE[op] ?? op);
|
|
148
|
+
}
|
|
149
|
+
tokens.push({ kind: 'eof', text: '', start: pos(), end: pos(), lineStart: true, spaced: true });
|
|
150
|
+
return tokens;
|
|
151
|
+
}
|