@bramas/simulant-engine 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/check.js +162 -0
- package/dist/compile.js +640 -0
- package/dist/conformance.js +21 -0
- package/dist/explorer.js +303 -0
- package/dist/symmetry.js +111 -0
- package/dist/system.js +607 -0
- package/dist/topology.js +82 -0
- package/dist/values.js +129 -0
- package/package.json +27 -3
- package/src/check.ts +210 -0
- package/src/compile.ts +596 -0
- package/src/conformance.ts +38 -0
- package/src/explorer.ts +330 -0
- package/src/symmetry.ts +119 -0
- package/src/system.ts +641 -0
- package/src/topology.ts +120 -0
- package/src/values.ts +148 -0
- package/README.md +0 -3
package/src/compile.ts
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiles IR expressions to JavaScript closures.
|
|
3
|
+
*
|
|
4
|
+
* A deterministic expression becomes `(frame) => value`. An expression that
|
|
5
|
+
* contains a choice becomes `(frame, k) => void`, which calls `k` once per
|
|
6
|
+
* outcome, in the order of the choices (continuation-passing style). While
|
|
7
|
+
* `k` runs, `rt.witness` holds the choices that led to the outcome.
|
|
8
|
+
*
|
|
9
|
+
* Variables live in slots of a frame array, allocated statically. Because
|
|
10
|
+
* evaluation is synchronous and continuations finish before the next outcome
|
|
11
|
+
* is produced, a slot can simply be overwritten for each outcome.
|
|
12
|
+
*/
|
|
13
|
+
import { BUILTINS } from '@bramas/simulant-lang/builtins';
|
|
14
|
+
import type { Def, Expr, Type } from '@bramas/simulant-lang/ir';
|
|
15
|
+
import type { Layout } from '@bramas/simulant-lang/layout';
|
|
16
|
+
import { fieldType, isCollection, resolvePub, showType, T } from '@bramas/simulant-lang/types';
|
|
17
|
+
import { opposite, type Move, type TopologyRuntime } from './topology';
|
|
18
|
+
import { canonicalSort, encoder, equality, show, type V } from './values';
|
|
19
|
+
|
|
20
|
+
export type Choice = { site: string; index: number; label: string };
|
|
21
|
+
|
|
22
|
+
export type AgentV = { pos: number; pub: V; mem: V; f: Record<string, V> };
|
|
23
|
+
export type Config = { agents: AgentV[]; world: Record<string, V>; idx?: number[] };
|
|
24
|
+
export type DecisionV = { decided: boolean; move: Move; pub: V; mem: V; [action: string]: V };
|
|
25
|
+
/** New values of world fields, and per flag, which agents raise it. */
|
|
26
|
+
export type Effect = { world: Record<string, V>; raise: Record<string, boolean[]> };
|
|
27
|
+
|
|
28
|
+
/** A call to a definition during a traced evaluation: its arguments, result and the line it returned from. */
|
|
29
|
+
export type TraceCall = { fn: string; args: string[]; result: string; line?: number; calls: TraceCall[] };
|
|
30
|
+
/**
|
|
31
|
+
* A traced evaluation (explanations of a step, never during exploration): the line of the last
|
|
32
|
+
* expression that produced a value (`Expr.at`), and the calls of each definition being evaluated.
|
|
33
|
+
*/
|
|
34
|
+
export type Trace = { line?: number; stack: TraceCall[][]; count: number };
|
|
35
|
+
/** At most this many calls are recorded (definitions called in lambdas over a whole ring add up). */
|
|
36
|
+
const MAX_TRACED_CALLS = 2000;
|
|
37
|
+
|
|
38
|
+
export class Runtime {
|
|
39
|
+
params: Record<string, V> = {};
|
|
40
|
+
topo!: TopologyRuntime;
|
|
41
|
+
topoState: V = null;
|
|
42
|
+
decisions: DecisionV[] = [];
|
|
43
|
+
witness: Choice[] = [];
|
|
44
|
+
/** Set only while an explanation evaluates (by a compiler built with `traced`). */
|
|
45
|
+
trace?: Trace;
|
|
46
|
+
private readonly flagNames: string[];
|
|
47
|
+
private readonly hiddenFlags: string[];
|
|
48
|
+
private readonly neverFlags: string[];
|
|
49
|
+
private readonly adversaryFlags: string[];
|
|
50
|
+
|
|
51
|
+
constructor(readonly lay: Layout) {
|
|
52
|
+
this.flagNames = lay.flags.map(f => f.name);
|
|
53
|
+
this.hiddenFlags = lay.flags.filter(f => f.visibility === 'hidden').map(f => f.name);
|
|
54
|
+
this.neverFlags = lay.flags.filter(f => f.acts === 'never').map(f => f.name);
|
|
55
|
+
this.adversaryFlags = lay.flags.filter(f => f.acts === 'adversary').map(f => f.name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
correct(a: AgentV) {
|
|
59
|
+
for (const f of this.flagNames) if (a.f[f]) return false;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
present(a: AgentV) {
|
|
64
|
+
for (const f of this.hiddenFlags) if (a.f[f]) return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Who decides for the agent: never > adversary > algorithm. */
|
|
69
|
+
acts(a: AgentV): 'never' | 'adversary' | 'algorithm' {
|
|
70
|
+
for (const f of this.neverFlags) if (a.f[f]) return 'never';
|
|
71
|
+
for (const f of this.adversaryFlags) if (a.f[f]) return 'adversary';
|
|
72
|
+
return 'algorithm';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type Frame = V[];
|
|
77
|
+
export type Det = (fr: Frame) => V;
|
|
78
|
+
export type ND = (fr: Frame, k: (v: V) => void) => void;
|
|
79
|
+
type Compiled = { t: Type; det?: Det; nd?: ND };
|
|
80
|
+
type Scope = { vars: Map<string, { slot: number; t: Type }>; config?: number };
|
|
81
|
+
type Root = { n: number };
|
|
82
|
+
|
|
83
|
+
const toND = (c: Compiled): ND => c.nd ?? ((fr, k) => k(c.det!(fr)));
|
|
84
|
+
|
|
85
|
+
/** Evaluates expressions in order, possibly with choices, then calls `k` with their values. */
|
|
86
|
+
function sequence(cs: Compiled[]): (fr: Frame, k: (vs: V[]) => void) => void {
|
|
87
|
+
const nds = cs.map(toND);
|
|
88
|
+
const vals: V[] = new Array(cs.length);
|
|
89
|
+
const step = (i: number, fr: Frame, k: (vs: V[]) => void): void => {
|
|
90
|
+
if (i === nds.length) return k(vals.slice());
|
|
91
|
+
nds[i](fr, v => { vals[i] = v; step(i + 1, fr, k); });
|
|
92
|
+
};
|
|
93
|
+
return (fr, k) => step(0, fr, k);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class CompileError extends Error {}
|
|
97
|
+
|
|
98
|
+
/** A compiled root expression: a function of its variables, deterministic or not. */
|
|
99
|
+
export type RootFn = { t: Type; nslots: number; det?: Det; nd?: ND };
|
|
100
|
+
|
|
101
|
+
export class Compiler {
|
|
102
|
+
private readonly defFns = new Map<Def, { t: Type; nslots: number; det?: Det; nd?: ND }>();
|
|
103
|
+
|
|
104
|
+
constructor(
|
|
105
|
+
readonly rt: Runtime,
|
|
106
|
+
readonly lay: Layout,
|
|
107
|
+
readonly paramTypes: Map<string, Type>,
|
|
108
|
+
/** Records, in `rt.trace`, the lines of values and the calls of definitions (explanations only). */
|
|
109
|
+
readonly traced = false,
|
|
110
|
+
) {}
|
|
111
|
+
|
|
112
|
+
/** Compiles `expr` with variables in slots 0, 1, ... in the order given. */
|
|
113
|
+
root(expr: Expr, vars: [string, Type][], defs: Map<string, Def>): RootFn {
|
|
114
|
+
const rc: Root = { n: vars.length };
|
|
115
|
+
const scope: Scope = { vars: new Map() };
|
|
116
|
+
vars.forEach(([name, t], slot) => {
|
|
117
|
+
scope.vars.set(name, { slot, t: this.resolve(t) });
|
|
118
|
+
if (t.k === 'config') scope.config = slot;
|
|
119
|
+
});
|
|
120
|
+
const c = this.compile(expr, scope, rc, defs);
|
|
121
|
+
return { t: c.t, nslots: rc.n, det: c.det, nd: c.nd };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private resolve(t: Type) {
|
|
125
|
+
return resolvePub(t, this.lay.pub);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private fail(msg: string): never {
|
|
129
|
+
throw new CompileError(msg);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private compile(expr: Expr, scope: Scope, rc: Root, defs: Map<string, Def>): Compiled {
|
|
133
|
+
const out = this.compileNode(expr, scope, rc, defs);
|
|
134
|
+
const at = expr.at;
|
|
135
|
+
// A traced value remembers where it came from; conditionals and lets pass on their branch's.
|
|
136
|
+
if (!this.traced || at === undefined || expr.e === 'if' || expr.e === 'match' || expr.e === 'let') return out;
|
|
137
|
+
const rt = this.rt;
|
|
138
|
+
const { det, nd } = out;
|
|
139
|
+
return {
|
|
140
|
+
t: out.t,
|
|
141
|
+
det: det && (fr => { const v = det(fr); if (rt.trace) rt.trace.line = at; return v; }),
|
|
142
|
+
nd: nd && ((fr, k) => nd(fr, v => { if (rt.trace) rt.trace.line = at; k(v); })),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private compileNode(expr: Expr, scope: Scope, rc: Root, defs: Map<string, Def>): Compiled {
|
|
147
|
+
const rt = this.rt;
|
|
148
|
+
const c = (e: Expr, s: Scope = scope) => this.compile(e, s, rc, defs);
|
|
149
|
+
switch (expr.e) {
|
|
150
|
+
case 'lit': {
|
|
151
|
+
const t = this.resolve(expr.type);
|
|
152
|
+
const value = t.k === 'multiset' ? canonicalSort(t.of)(expr.value as V[]) : expr.value;
|
|
153
|
+
return { t, det: () => value };
|
|
154
|
+
}
|
|
155
|
+
case 'var': {
|
|
156
|
+
const v = scope.vars.get(expr.name) ?? this.fail(`unknown variable ${expr.name}`);
|
|
157
|
+
const slot = v.slot;
|
|
158
|
+
return { t: v.t, det: fr => fr[slot] };
|
|
159
|
+
}
|
|
160
|
+
case 'param': {
|
|
161
|
+
const t = this.paramTypes.get(expr.name) ?? this.fail(`unknown parameter ${expr.name}`);
|
|
162
|
+
const name = expr.name;
|
|
163
|
+
return { t, det: () => rt.params[name] };
|
|
164
|
+
}
|
|
165
|
+
case 'let': {
|
|
166
|
+
const v = c(expr.value);
|
|
167
|
+
const slot = rc.n++;
|
|
168
|
+
const inner: Scope = { vars: new Map(scope.vars).set(expr.name, { slot, t: v.t }), config: v.t.k === 'config' ? slot : scope.config };
|
|
169
|
+
const b = c(expr.body, inner);
|
|
170
|
+
if (v.det && b.det) {
|
|
171
|
+
const vd = v.det, bd = b.det;
|
|
172
|
+
return { t: b.t, det: fr => { fr[slot] = vd(fr); return bd(fr); } };
|
|
173
|
+
}
|
|
174
|
+
const vn = toND(v), bn = toND(b);
|
|
175
|
+
return { t: b.t, nd: (fr, k) => vn(fr, x => { fr[slot] = x; bn(fr, k); }) };
|
|
176
|
+
}
|
|
177
|
+
case 'if': {
|
|
178
|
+
const [cond, a, b] = [c(expr.cond), c(expr.then), c(expr.else)];
|
|
179
|
+
if (cond.det && a.det && b.det) {
|
|
180
|
+
const cd = cond.det, ad = a.det, bd = b.det;
|
|
181
|
+
return { t: a.t, det: fr => cd(fr) ? ad(fr) : bd(fr) };
|
|
182
|
+
}
|
|
183
|
+
const cn = toND(cond), an = toND(a), bn = toND(b);
|
|
184
|
+
return { t: a.t, nd: (fr, k) => cn(fr, v => (v ? an : bn)(fr, k)) };
|
|
185
|
+
}
|
|
186
|
+
case 'match': {
|
|
187
|
+
const on = c(expr.on);
|
|
188
|
+
const cases = Object.fromEntries(Object.entries(expr.cases).map(([ctor, e]) => [ctor, c(e)]));
|
|
189
|
+
const otherwise = expr.else ? c(expr.else) : undefined;
|
|
190
|
+
const branches = [...Object.values(cases), ...(otherwise ? [otherwise] : [])];
|
|
191
|
+
const t = branches[0].t;
|
|
192
|
+
if (on.det && branches.every(b => b.det)) {
|
|
193
|
+
const od = on.det;
|
|
194
|
+
const table = new Map(Object.entries(cases).map(([ctor, b]) => [ctor, b.det!]));
|
|
195
|
+
const dflt = otherwise?.det;
|
|
196
|
+
return { t, det: fr => (table.get(od(fr)) ?? dflt!)(fr) };
|
|
197
|
+
}
|
|
198
|
+
const onN = toND(on);
|
|
199
|
+
const table = new Map(Object.entries(cases).map(([ctor, b]) => [ctor, toND(b)]));
|
|
200
|
+
const dflt = otherwise && toND(otherwise);
|
|
201
|
+
return { t, nd: (fr, k) => onN(fr, v => (table.get(v) ?? dflt!)(fr, k)) };
|
|
202
|
+
}
|
|
203
|
+
case 'fn':
|
|
204
|
+
return this.fail('a function is only allowed as an argument of a builtin');
|
|
205
|
+
case 'record': {
|
|
206
|
+
const names = Object.keys(expr.fields);
|
|
207
|
+
const cs = names.map(n => c(expr.fields[n]));
|
|
208
|
+
const t = T.record(names.map((name, i) => ({ name, type: cs[i].t })));
|
|
209
|
+
const build = (vs: V[]) => {
|
|
210
|
+
const o: Record<string, V> = {};
|
|
211
|
+
for (let i = 0; i < names.length; i++) o[names[i]] = vs[i];
|
|
212
|
+
return o;
|
|
213
|
+
};
|
|
214
|
+
return this.lift(t, cs, build);
|
|
215
|
+
}
|
|
216
|
+
case 'with': {
|
|
217
|
+
const on = c(expr.on);
|
|
218
|
+
const names = Object.keys(expr.fields);
|
|
219
|
+
const cs = [on, ...names.map(n => c(expr.fields[n]))];
|
|
220
|
+
return this.lift(on.t, cs, ([base, ...vs]) => {
|
|
221
|
+
const o = { ...base };
|
|
222
|
+
for (let i = 0; i < names.length; i++) o[names[i]] = vs[i];
|
|
223
|
+
return o;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
case 'get':
|
|
227
|
+
return this.get(c(expr.on), expr.field, scope);
|
|
228
|
+
case 'choose':
|
|
229
|
+
return this.choose(expr, scope, rc, defs);
|
|
230
|
+
case 'effect':
|
|
231
|
+
return this.effect(expr, scope, rc, defs);
|
|
232
|
+
case 'call':
|
|
233
|
+
return BUILTINS[expr.fn] ? this.builtin(expr, scope, rc, defs) : this.callDef(expr, scope, rc, defs);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Applies a deterministic function to the values of `cs`, which may contain choices. */
|
|
238
|
+
private lift(t: Type, cs: Compiled[], f: (vs: V[]) => V): Compiled {
|
|
239
|
+
if (cs.every(x => x.det)) {
|
|
240
|
+
const ds = cs.map(x => x.det!);
|
|
241
|
+
if (ds.length === 1) { const d = ds[0]; return { t, det: fr => f([d(fr)]) }; }
|
|
242
|
+
return { t, det: fr => f(ds.map(d => d(fr))) };
|
|
243
|
+
}
|
|
244
|
+
const seq = sequence(cs);
|
|
245
|
+
return { t, nd: (fr, k) => seq(fr, vs => k(f(vs))) };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private configSlot(scope: Scope, what: string): number {
|
|
249
|
+
if (scope.config === undefined) this.fail(`${what} needs a configuration in scope`);
|
|
250
|
+
return scope.config;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private get(on: Compiled, field: string, scope: Scope): Compiled {
|
|
254
|
+
const t = on.t;
|
|
255
|
+
switch (t.k) {
|
|
256
|
+
case 'record': {
|
|
257
|
+
const ft = fieldType(t, field) ?? this.fail(`${showType(t)} has no field ${field}`);
|
|
258
|
+
return this.lift(ft, [on], ([v]) => v[field]);
|
|
259
|
+
}
|
|
260
|
+
case 'config':
|
|
261
|
+
if (field === 'world') return this.lift(this.lay.worldType, [on], ([C]) => C.world);
|
|
262
|
+
if (field === 'agents') return this.lift(T.multiset(T.agent), [on], ([C]) => agentIndices(C));
|
|
263
|
+
return this.fail(`a configuration has no field ${field}`);
|
|
264
|
+
case 'agent': {
|
|
265
|
+
const cs = this.configSlot(scope, `reading ${field} of an agent`);
|
|
266
|
+
let ft: Type;
|
|
267
|
+
let read: (a: AgentV) => V;
|
|
268
|
+
if (field === 'pos') { ft = T.node; read = a => a.pos; }
|
|
269
|
+
else if (field === 'pub') { ft = this.lay.pub; read = a => a.pub; }
|
|
270
|
+
else if (field === 'mem') { ft = this.lay.mem; read = a => a.mem; }
|
|
271
|
+
else if (this.lay.flags.some(f => f.name === field)) { ft = T.bool; read = a => !!a.f[field]; }
|
|
272
|
+
else {
|
|
273
|
+
ft = this.lay.agentFields.find(f => f.name === field)?.type ?? this.fail(`agents have no field ${field}`);
|
|
274
|
+
read = a => a.f[field];
|
|
275
|
+
}
|
|
276
|
+
if (on.det) {
|
|
277
|
+
const d = on.det;
|
|
278
|
+
return { t: ft, det: fr => read(fr[cs].agents[d(fr)]) };
|
|
279
|
+
}
|
|
280
|
+
const n = toND(on);
|
|
281
|
+
return { t: ft, nd: (fr, k) => n(fr, i => k(read(fr[cs].agents[i]))) };
|
|
282
|
+
}
|
|
283
|
+
case 'decision': {
|
|
284
|
+
const ft = field === 'move' ? T.move : field === 'decided' ? T.bool
|
|
285
|
+
: this.lay.actions.find(a => a.name === field)?.type ?? this.fail(`decisions have no action ${field}`);
|
|
286
|
+
return this.lift(ft, [on], ([d]) => d[field]);
|
|
287
|
+
}
|
|
288
|
+
default:
|
|
289
|
+
return this.fail(`${showType(t)} has no fields`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** A deterministic lambda: `(fr, ...args) => value`. */
|
|
294
|
+
private lambda(e: Expr, params: Type[], scope: Scope, rc: Root, defs: Map<string, Def>): { t: Type; fn: (fr: Frame, ...xs: V[]) => V } {
|
|
295
|
+
if (e.e !== 'fn') this.fail('expected a function');
|
|
296
|
+
const vars = new Map(scope.vars);
|
|
297
|
+
const slots = e.params.map((p, i) => {
|
|
298
|
+
const slot = rc.n++;
|
|
299
|
+
vars.set(p.name, { slot, t: this.resolve(params[i]) });
|
|
300
|
+
return slot;
|
|
301
|
+
});
|
|
302
|
+
const body = this.compile(e.body, { vars, config: scope.config }, rc, defs);
|
|
303
|
+
if (!body.det) this.fail('a function passed to a builtin cannot contain choices');
|
|
304
|
+
const d = body.det;
|
|
305
|
+
if (slots.length === 1) {
|
|
306
|
+
const s = slots[0];
|
|
307
|
+
return { t: body.t, fn: (fr, x) => { fr[s] = x; return d(fr); } };
|
|
308
|
+
}
|
|
309
|
+
return { t: body.t, fn: (fr, ...xs) => { slots.forEach((s, i) => { fr[s] = xs[i]; }); return d(fr); } };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private choose(expr: Extract<Expr, { e: 'choose' }>, scope: Scope, rc: Root, defs: Map<string, Def>): Compiled {
|
|
313
|
+
const rt = this.rt;
|
|
314
|
+
const from = this.compile(expr.from, scope, rc, defs);
|
|
315
|
+
if (!isCollection(from.t)) this.fail('choose: expected a collection');
|
|
316
|
+
const elem = from.t.of;
|
|
317
|
+
const such = expr.such && this.lambda(expr.such, [elem], scope, rc, defs).fn;
|
|
318
|
+
const label = expr.label ? this.lambda(expr.label, [elem], scope, rc, defs).fn : undefined;
|
|
319
|
+
const showElem = show(elem);
|
|
320
|
+
const fromN = toND(from);
|
|
321
|
+
const site = expr.site;
|
|
322
|
+
return {
|
|
323
|
+
t: elem,
|
|
324
|
+
nd: (fr, k) => fromN(fr, (xs: V[]) => {
|
|
325
|
+
for (let i = 0; i < xs.length; i++) {
|
|
326
|
+
const x = xs[i];
|
|
327
|
+
if (such && !such(fr, x)) continue;
|
|
328
|
+
rt.witness.push({ site, index: i, label: label ? label(fr, x) : showElem(x) });
|
|
329
|
+
k(x);
|
|
330
|
+
rt.witness.pop();
|
|
331
|
+
}
|
|
332
|
+
}),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private effect(expr: Extract<Expr, { e: 'effect' }>, scope: Scope, rc: Root, defs: Map<string, Def>): Compiled {
|
|
337
|
+
const worldNames = Object.keys(expr.world ?? {});
|
|
338
|
+
const world = worldNames.map(n => this.compile(expr.world![n], scope, rc, defs));
|
|
339
|
+
const raiseNames = Object.keys(expr.raise ?? {});
|
|
340
|
+
const cs = raiseNames.length ? this.configSlot(scope, 'raising flags') : -1;
|
|
341
|
+
// Each raise becomes a function from the frame to "which agents raise the flag".
|
|
342
|
+
const raises: Compiled[] = raiseNames.map(n => {
|
|
343
|
+
const e = expr.raise![n];
|
|
344
|
+
if (e.e === 'fn') {
|
|
345
|
+
const pred = this.lambda(e, [T.agent], scope, rc, defs).fn;
|
|
346
|
+
return { t: T.unit, det: fr => fr[cs].agents.map((_: AgentV, i: number) => !!pred(fr, i)) };
|
|
347
|
+
}
|
|
348
|
+
return this.lift(T.unit, [this.compile(e, scope, rc, defs)], ([idx]) => indexMarks(idx));
|
|
349
|
+
});
|
|
350
|
+
const all = [...world, ...raises];
|
|
351
|
+
return this.lift(T.effect, all, vs => {
|
|
352
|
+
const eff: Effect = { world: {}, raise: {} };
|
|
353
|
+
worldNames.forEach((n, i) => { eff.world[n] = vs[i]; });
|
|
354
|
+
raiseNames.forEach((n, j) => { eff.raise[n] = vs[worldNames.length + j]; });
|
|
355
|
+
return eff;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private callDef(expr: Extract<Expr, { e: 'call' }>, scope: Scope, rc: Root, defs: Map<string, Def>): Compiled {
|
|
360
|
+
const def = defs.get(expr.fn) ?? this.fail(`unknown function ${expr.fn}`);
|
|
361
|
+
let fn = this.defFns.get(def);
|
|
362
|
+
if (!fn) {
|
|
363
|
+
fn = this.root(def.body, def.params.map(p => [p.name, p.type]), defs);
|
|
364
|
+
this.defFns.set(def, fn);
|
|
365
|
+
}
|
|
366
|
+
const { nslots, det, nd } = fn;
|
|
367
|
+
const args = expr.args.map(a => this.compile(a, scope, rc, defs));
|
|
368
|
+
if (det && args.every(a => a.det)) {
|
|
369
|
+
const ds = args.map(a => a.det!);
|
|
370
|
+
const run = this.traced ? this.tracedCall(def.name, args.map(a => show(a.t, true)), show(this.resolve(def.ret), true), det) : det;
|
|
371
|
+
return {
|
|
372
|
+
t: this.resolve(def.ret),
|
|
373
|
+
det: fr => {
|
|
374
|
+
const f2 = new Array(nslots);
|
|
375
|
+
for (let i = 0; i < ds.length; i++) f2[i] = ds[i](fr);
|
|
376
|
+
return run(f2);
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const seq = sequence(args);
|
|
381
|
+
const body = nd ?? ((f2: Frame, k: (v: V) => void) => k(det!(f2)));
|
|
382
|
+
return {
|
|
383
|
+
t: this.resolve(def.ret),
|
|
384
|
+
nd: (fr, k) => seq(fr, vs => {
|
|
385
|
+
const f2 = new Array(nslots);
|
|
386
|
+
vs.forEach((v, i) => { f2[i] = v; });
|
|
387
|
+
body(f2, k);
|
|
388
|
+
}),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** A definition's body that records its call in `rt.trace` (arguments, result, line it returned from). */
|
|
393
|
+
private tracedCall(fn: string, showArgs: ((v: V) => string)[], showRet: (v: V) => string, body: Det): Det {
|
|
394
|
+
const rt = this.rt;
|
|
395
|
+
return f2 => {
|
|
396
|
+
const trace = rt.trace;
|
|
397
|
+
if (!trace) return body(f2);
|
|
398
|
+
const call: TraceCall = { fn, args: showArgs.map((s, i) => s(f2[i])), result: '', calls: [] };
|
|
399
|
+
trace.stack.push(call.calls);
|
|
400
|
+
let v: V;
|
|
401
|
+
try {
|
|
402
|
+
v = body(f2);
|
|
403
|
+
} finally {
|
|
404
|
+
trace.stack.pop();
|
|
405
|
+
}
|
|
406
|
+
call.result = showRet(v);
|
|
407
|
+
call.line = trace.line;
|
|
408
|
+
if (trace.count++ < MAX_TRACED_CALLS) trace.stack[trace.stack.length - 1].push(call);
|
|
409
|
+
return v;
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private builtin(expr: Extract<Expr, { e: 'call' }>, scope: Scope, rc: Root, defs: Map<string, Def>): Compiled {
|
|
414
|
+
if (expr.fn === 'correct' || expr.fn === 'present') {
|
|
415
|
+
// Like a field of the agent: read in the configuration in scope.
|
|
416
|
+
const cs = this.configSlot(scope, expr.fn);
|
|
417
|
+
const test = expr.fn === 'correct' ? (a: AgentV) => this.rt.correct(a) : (a: AgentV) => this.rt.present(a);
|
|
418
|
+
const a = this.compile(expr.args[0], scope, rc, defs);
|
|
419
|
+
if (a.det) { const d = a.det; return { t: T.bool, det: fr => test(fr[cs].agents[d(fr)]) }; }
|
|
420
|
+
const n = toND(a);
|
|
421
|
+
return { t: T.bool, nd: (fr, k) => n(fr, i => k(test(fr[cs].agents[i]))) };
|
|
422
|
+
}
|
|
423
|
+
const sig = BUILTINS[expr.fn];
|
|
424
|
+
const types: Type[] = [];
|
|
425
|
+
const parts: ({ kind: 'value'; c: Compiled } | { kind: 'fn'; fn: (fr: Frame, ...xs: V[]) => V })[] = [];
|
|
426
|
+
expr.args.forEach((arg, i) => {
|
|
427
|
+
const lam = sig.lambdas?.[i];
|
|
428
|
+
if (lam) {
|
|
429
|
+
const params = lam(types);
|
|
430
|
+
if (typeof params === 'string') this.fail(params);
|
|
431
|
+
const l = this.lambda(arg, params, scope, rc, defs);
|
|
432
|
+
types.push(T.fn(params, l.t));
|
|
433
|
+
parts.push({ kind: 'fn', fn: l.fn });
|
|
434
|
+
} else {
|
|
435
|
+
const c = this.compile(arg, scope, rc, defs);
|
|
436
|
+
types.push(c.t);
|
|
437
|
+
parts.push({ kind: 'value', c });
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
const t = sig.ret(types);
|
|
441
|
+
if (typeof t === 'string') this.fail(t);
|
|
442
|
+
const impl = this.impl(expr.fn, types, t, scope);
|
|
443
|
+
const values = parts.filter(p => p.kind === 'value').map(p => (p as { c: Compiled }).c);
|
|
444
|
+
const fns = parts.map(p => p.kind === 'fn' ? p.fn : undefined);
|
|
445
|
+
const hasFns = fns.some(f => f);
|
|
446
|
+
// Lambdas receive the current frame; values are evaluated first, in order.
|
|
447
|
+
const call = (fr: Frame, vs: V[]) => {
|
|
448
|
+
if (!hasFns) return impl(vs);
|
|
449
|
+
let j = 0;
|
|
450
|
+
const args = fns.map(f => f ? (...xs: V[]) => f(fr, ...xs) : vs[j++]);
|
|
451
|
+
return impl(args);
|
|
452
|
+
};
|
|
453
|
+
if (values.every(v => v.det)) {
|
|
454
|
+
const ds = values.map(v => v.det!);
|
|
455
|
+
if (expr.fn === 'and') return { t, det: fr => { for (const d of ds) if (!d(fr)) return false; return true; } };
|
|
456
|
+
if (expr.fn === 'or') return { t, det: fr => { for (const d of ds) if (d(fr)) return true; return false; } };
|
|
457
|
+
if (ds.length === 1) { const d = ds[0]; return { t, det: fr => call(fr, [d(fr)]) }; }
|
|
458
|
+
if (ds.length === 2) { const [d0, d1] = ds; return { t, det: fr => call(fr, [d0(fr), d1(fr)]) }; }
|
|
459
|
+
return { t, det: fr => call(fr, ds.map(d => d(fr))) };
|
|
460
|
+
}
|
|
461
|
+
const seq = sequence(values);
|
|
462
|
+
return { t, nd: (fr, k) => seq(fr, vs => k(call(fr, vs))) };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** The implementation of a builtin, specialised to its argument types. */
|
|
466
|
+
private impl(fn: string, types: Type[], ret: Type, scope: Scope): (args: V[]) => V {
|
|
467
|
+
const rt = this.rt;
|
|
468
|
+
const elemOf = (t: Type) => (t as { of: Type }).of;
|
|
469
|
+
const isMultiset = types[0]?.k === 'multiset';
|
|
470
|
+
const nat = types[0]?.k === 'nat';
|
|
471
|
+
switch (fn) {
|
|
472
|
+
case 'not': return ([x]) => !x;
|
|
473
|
+
case 'and': return xs => xs.every(Boolean);
|
|
474
|
+
case 'or': return xs => xs.some(Boolean);
|
|
475
|
+
case 'implies': return ([a, b]) => !a || b;
|
|
476
|
+
case 'eq': { const eq = equality(types[0]); return ([a, b]) => eq(a, b); }
|
|
477
|
+
case 'ne': { const eq = equality(types[0]); return ([a, b]) => !eq(a, b); }
|
|
478
|
+
case 'lt': return types[0].k === 'list' ? ([a, b]) => lexCompare(a, b) < 0 : ([a, b]) => a < b;
|
|
479
|
+
case 'le': return types[0].k === 'list' ? ([a, b]) => lexCompare(a, b) <= 0 : ([a, b]) => a <= b;
|
|
480
|
+
case 'gt': return types[0].k === 'list' ? ([a, b]) => lexCompare(a, b) > 0 : ([a, b]) => a > b;
|
|
481
|
+
case 'ge': return types[0].k === 'list' ? ([a, b]) => lexCompare(a, b) >= 0 : ([a, b]) => a >= b;
|
|
482
|
+
case 'add': return ([a, b]) => a + b;
|
|
483
|
+
case 'sub': return nat ? ([a, b]) => Math.max(0, a - b) : ([a, b]) => a - b;
|
|
484
|
+
case 'mul': return ([a, b]) => a * b;
|
|
485
|
+
// Lean: division by zero is zero; on Int, `/` and `%` are Euclidean.
|
|
486
|
+
case 'div': return nat ? ([a, b]) => b === 0 ? 0 : Math.floor(a / b) : ([a, b]) => b === 0 ? 0 : Math.sign(b) * Math.floor(a / Math.abs(b));
|
|
487
|
+
case 'mod': return nat ? ([a, b]) => b === 0 ? a : a % b : ([a, b]) => b === 0 ? a : ((a % Math.abs(b)) + Math.abs(b)) % Math.abs(b);
|
|
488
|
+
case 'min': return ([a, b]) => Math.min(a, b);
|
|
489
|
+
case 'max': return ([a, b]) => Math.max(a, b);
|
|
490
|
+
case 'toInt': return ([a]) => a;
|
|
491
|
+
case 'some': return ([x]) => x;
|
|
492
|
+
case 'isSome': return ([o]) => o !== null;
|
|
493
|
+
case 'isNone': return ([o]) => o === null;
|
|
494
|
+
case 'getD': return ([o, d]) => o === null ? d : o;
|
|
495
|
+
case 'length': return ([l]) => l.length;
|
|
496
|
+
case 'isEmpty': return ([l]) => l.length === 0;
|
|
497
|
+
case 'filter': return ([l, p]) => l.filter((x: V) => p(x));
|
|
498
|
+
case 'any': return ([l, p]) => l.some((x: V) => p(x));
|
|
499
|
+
case 'all': return ([l, p]) => l.every((x: V) => p(x));
|
|
500
|
+
case 'count': return ([l, p]) => { let n = 0; for (const x of l) if (p(x)) n++; return n; };
|
|
501
|
+
case 'map': {
|
|
502
|
+
if (!isMultiset) return ([l, f]) => l.map((x: V) => f(x));
|
|
503
|
+
const sort = canonicalSort(elemOf(ret));
|
|
504
|
+
return ([l, f]) => sort(l.map((x: V) => f(x)));
|
|
505
|
+
}
|
|
506
|
+
case 'sum': return ([l]) => l.reduce((a: number, b: number) => a + b, 0);
|
|
507
|
+
case 'mem': { const eq = equality(types[0]); return ([x, l]) => l.some((y: V) => eq(x, y)); }
|
|
508
|
+
case 'range': return ([n]) => Array.from({ length: n }, (_, i) => i);
|
|
509
|
+
case 'toMultiset': { const sort = canonicalSort(elemOf(types[0])); return ([l]) => sort(l); }
|
|
510
|
+
case 'toList': return ([l]) => l.slice();
|
|
511
|
+
case 'subsets': return ([l]) => subsetsOf(l, l.length, types[0]);
|
|
512
|
+
case 'subsetsUpTo': return ([l, k]) => subsetsOf(l, k, types[0]);
|
|
513
|
+
case 'dedup': {
|
|
514
|
+
const enc = encoder(elemOf(types[0]));
|
|
515
|
+
return ([l]) => { const seen = new Set<string>(); return l.filter((x: V) => { const e = enc(x); if (seen.has(e)) return false; seen.add(e); return true; }); };
|
|
516
|
+
}
|
|
517
|
+
case 'diff': {
|
|
518
|
+
const enc = encoder(elemOf(types[0]));
|
|
519
|
+
return ([a, b]) => {
|
|
520
|
+
const remove = new Map<string, number>();
|
|
521
|
+
for (const x of b) { const e = enc(x); remove.set(e, (remove.get(e) ?? 0) + 1); }
|
|
522
|
+
return a.filter((x: V) => {
|
|
523
|
+
const e = enc(x);
|
|
524
|
+
const n = remove.get(e);
|
|
525
|
+
if (!n) return true;
|
|
526
|
+
remove.set(e, n - 1);
|
|
527
|
+
return false;
|
|
528
|
+
});
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
case 'list': return xs => xs;
|
|
532
|
+
case 'at': return ([l, i]) => { if (i >= l.length) throw new Error(`at: index ${i} out of range`); return l[i]; };
|
|
533
|
+
case 'append': {
|
|
534
|
+
if (!isMultiset) return ([a, b]) => a.concat(b);
|
|
535
|
+
const sort = canonicalSort(elemOf(types[0]));
|
|
536
|
+
return ([a, b]) => sort(a.concat(b));
|
|
537
|
+
}
|
|
538
|
+
case 'ofFn': return ([n, f]) => Array.from({ length: n }, (_, i) => f(i));
|
|
539
|
+
case 'show': { const s = show(types[0]); return ([x]) => s(x); }
|
|
540
|
+
case 'concat': return xs => xs.join('');
|
|
541
|
+
case 'agents': return ([C]) => agentIndices(C);
|
|
542
|
+
case 'nodes': return () => Array.from({ length: rt.topo.size }, (_, i) => i);
|
|
543
|
+
case 'size': return () => rt.topo.size;
|
|
544
|
+
case 'decisionOf': return ([a]) => rt.decisions[a];
|
|
545
|
+
case 'target': return ([n, m]) => rt.topo.target(n, m);
|
|
546
|
+
case 'advance': return ([n, m, i]) => {
|
|
547
|
+
const N = rt.topo.size;
|
|
548
|
+
return m === 'cw' ? (n + i) % N : m === 'ccw' ? (n + N - i % N) % N : n;
|
|
549
|
+
};
|
|
550
|
+
case 'edgePresent': return ([n, m]) => rt.topo.canMove(rt.topoState, n, m);
|
|
551
|
+
case 'offset': return ([n, f, i]) => rt.topo.offset(n, f, i);
|
|
552
|
+
case 'toGlobal': return ([f, m]) => rt.topo.toGlobal(f, m);
|
|
553
|
+
case 'opposite': return ([m]) => opposite(m);
|
|
554
|
+
case 'frames': return () => rt.topo.frames;
|
|
555
|
+
case 'node': return ([n]) => n % rt.topo.size;
|
|
556
|
+
}
|
|
557
|
+
return this.fail(`builtin ${fn} is not implemented`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Lexicographic order on lists of Bool or Nat (`false < true`, a prefix first), as Lean's `List.lt`. */
|
|
563
|
+
function lexCompare(a: (boolean | number)[], b: (boolean | number)[]): number {
|
|
564
|
+
const n = Math.min(a.length, b.length);
|
|
565
|
+
for (let i = 0; i < n; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
|
|
566
|
+
return a.length - b.length;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function agentIndices(C: Config): number[] {
|
|
570
|
+
return C.idx ??= C.agents.map((_, i) => i);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function indexMarks(idx: number[]): boolean[] {
|
|
574
|
+
const marks: boolean[] = [];
|
|
575
|
+
for (const i of idx) marks[i] = true;
|
|
576
|
+
return marks;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** Sub-collections with at most `k` elements, smallest first, then in index order. */
|
|
580
|
+
function subsetsOf(l: V[], k: number, t: Type): V[][] {
|
|
581
|
+
const out: V[][] = [];
|
|
582
|
+
const pick = (size: number, from: number, current: V[]) => {
|
|
583
|
+
if (current.length === size) { out.push(current.slice()); return; }
|
|
584
|
+
for (let i = from; i < l.length; i++) {
|
|
585
|
+
current.push(l[i]);
|
|
586
|
+
pick(size, i + 1, current);
|
|
587
|
+
current.pop();
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
for (let size = 0; size <= Math.min(k, l.length); size++) pick(size, 0, []);
|
|
591
|
+
if (t.k !== 'multiset') return out;
|
|
592
|
+
// Equal elements of a multiset give equal sub-multisets: keep one of each.
|
|
593
|
+
const enc = encoder(t);
|
|
594
|
+
const seen = new Set<string>();
|
|
595
|
+
return out.filter(s => { const e = enc(s); if (seen.has(e)) return false; seen.add(e); return true; });
|
|
596
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IR conformance fixtures (plan, Section 6.1): an experiment's IR, its
|
|
3
|
+
* parameters, and the beginning of its unreduced state graph in BFS order,
|
|
4
|
+
* with each edge's witness. Any interpreter of the IR must reproduce them.
|
|
5
|
+
*/
|
|
6
|
+
import type { Experiment } from '@bramas/simulant-lang/ir';
|
|
7
|
+
import { Explorer } from './explorer';
|
|
8
|
+
import { System, type Config } from './system';
|
|
9
|
+
import type { V } from './values';
|
|
10
|
+
|
|
11
|
+
export type ConformanceFixture = {
|
|
12
|
+
name: string;
|
|
13
|
+
experiment: Experiment;
|
|
14
|
+
params: Record<string, V>;
|
|
15
|
+
maxStates: number;
|
|
16
|
+
states: { config: Config; verdict: string }[];
|
|
17
|
+
/** Edges of the expanded states, in successor order. */
|
|
18
|
+
edges: { from: number; to: number; witness: string[]; activated: number[] }[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function conformanceGraph(name: string, experiment: Experiment, params: Record<string, V>, maxStates = 40): ConformanceFixture {
|
|
22
|
+
const system = new System(experiment, params);
|
|
23
|
+
const explorer = new Explorer(system, { symmetry: false, maxStates });
|
|
24
|
+
explorer.run();
|
|
25
|
+
const ids = new Map(explorer.states.map(s => [s.key, s.id]));
|
|
26
|
+
const edges: ConformanceFixture['edges'] = [];
|
|
27
|
+
for (const s of explorer.states) {
|
|
28
|
+
if (!s.succ) continue;
|
|
29
|
+
for (const t of system.successors(s.config)) {
|
|
30
|
+
edges.push({ from: s.id, to: ids.get(t.key)!, witness: t.witness.map(w => w.label), activated: [...t.activated].sort() });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
name, experiment, params, maxStates,
|
|
35
|
+
states: explorer.states.map(s => ({ config: { agents: s.config.agents, world: s.config.world }, verdict: s.verdict })),
|
|
36
|
+
edges,
|
|
37
|
+
};
|
|
38
|
+
}
|