@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/topology.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topologies of the semantic core (plan, Section 3.6): nodes, moves, frames,
|
|
3
|
+
* the per-round state chosen by the adversary, and the automorphisms.
|
|
4
|
+
*/
|
|
5
|
+
import type { Action, V } from './values';
|
|
6
|
+
|
|
7
|
+
export type Move = 'stay' | 'cw' | 'ccw';
|
|
8
|
+
|
|
9
|
+
export type Automorphism = Action & {
|
|
10
|
+
label: string;
|
|
11
|
+
/** Rotations keep orientation; reflections reverse it. */
|
|
12
|
+
reflects: boolean;
|
|
13
|
+
inverse: () => Automorphism;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface TopologyRuntime {
|
|
17
|
+
readonly kind: string;
|
|
18
|
+
readonly size: number;
|
|
19
|
+
/** The states the adversary may choose at the start of a round; one state for a static topology. */
|
|
20
|
+
states(): { state: V; label: string }[];
|
|
21
|
+
/** The neighbour of `n` in global direction `m`. */
|
|
22
|
+
target(n: number, m: Move): number;
|
|
23
|
+
/** Whether the edge used by moving from `n` in direction `m` is present in `state`. */
|
|
24
|
+
canMove(state: V, n: number, m: Move): boolean;
|
|
25
|
+
/** The node seen at local offset `i` from `n`, in frame `f`. */
|
|
26
|
+
offset(n: number, f: V, i: number): number;
|
|
27
|
+
toGlobal(f: V, m: Move): Move;
|
|
28
|
+
readonly frames: V[];
|
|
29
|
+
readonly identityFrame: V;
|
|
30
|
+
automorphisms(): Automorphism[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const flip = (m: Move): Move => m === 'cw' ? 'ccw' : m === 'ccw' ? 'cw' : 'stay';
|
|
34
|
+
|
|
35
|
+
export function opposite(m: Move): Move {
|
|
36
|
+
return flip(m);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class Ring implements TopologyRuntime {
|
|
40
|
+
readonly kind: string = 'ring';
|
|
41
|
+
readonly frames = [false, true];
|
|
42
|
+
readonly identityFrame = false;
|
|
43
|
+
|
|
44
|
+
constructor(readonly size: number) {
|
|
45
|
+
if (!Number.isInteger(size) || size < 2) throw new Error(`a ring needs at least 2 nodes, got ${size}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
states(): { state: V; label: string }[] {
|
|
49
|
+
return [{ state: null, label: '' }];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
target(n: number, m: Move) {
|
|
53
|
+
const N = this.size;
|
|
54
|
+
return m === 'cw' ? (n + 1) % N : m === 'ccw' ? (n + N - 1) % N : n;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
canMove(_state: V, _n: number, _m: Move) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
offset(n: number, f: V, i: number) {
|
|
62
|
+
const N = this.size;
|
|
63
|
+
return f ? ((n - i) % N + N) % N : (n + i) % N;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
toGlobal(f: V, m: Move): Move {
|
|
67
|
+
return f ? flip(m) : m;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
automorphisms(): Automorphism[] {
|
|
71
|
+
const N = this.size;
|
|
72
|
+
const make = (k: number, reflects: boolean): Automorphism => ({
|
|
73
|
+
label: reflects ? `reflection ${k}` : `rotation ${k}`,
|
|
74
|
+
reflects,
|
|
75
|
+
node: reflects ? n => ((k - n) % N + N) % N : n => (n + k) % N,
|
|
76
|
+
frame: reflects ? f => !f : f => f,
|
|
77
|
+
inverse: () => reflects ? make(k, true) : make((N - k) % N, false),
|
|
78
|
+
});
|
|
79
|
+
return [...Array.from({ length: N }, (_, k) => make(k, false)), ...Array.from({ length: N }, (_, k) => make(k, true))];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A ring where, at the start of every round, the adversary removes up to `k`
|
|
85
|
+
* edges. Edge `i` joins nodes `i` and `i + 1`. The state is the sorted list
|
|
86
|
+
* of missing edges; it is not kept between rounds.
|
|
87
|
+
*/
|
|
88
|
+
export class DynamicRing extends Ring {
|
|
89
|
+
readonly kind = 'dynamicRing';
|
|
90
|
+
private readonly choices: { state: number[]; label: string }[];
|
|
91
|
+
|
|
92
|
+
constructor(size: number, readonly k: number) {
|
|
93
|
+
super(size);
|
|
94
|
+
const N = size;
|
|
95
|
+
const sets: number[][] = [[]];
|
|
96
|
+
const extend = (from: number, current: number[]) => {
|
|
97
|
+
for (let e = from; e < N; e++) {
|
|
98
|
+
const next = [...current, e];
|
|
99
|
+
if (next.length > k) continue;
|
|
100
|
+
sets.push(next);
|
|
101
|
+
extend(e + 1, next);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
extend(0, []);
|
|
105
|
+
this.choices = sets.map(state => ({
|
|
106
|
+
state,
|
|
107
|
+
label: state.length ? `missing ${state.map(e => `u${e}–u${(e + 1) % N}`).join(', ')}` : 'all edges present',
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
states() {
|
|
112
|
+
return this.choices;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
canMove(state: number[], n: number, m: Move) {
|
|
116
|
+
if (m === 'stay' || !state.length) return true;
|
|
117
|
+
const edge = m === 'cw' ? n : (n + this.size - 1) % this.size;
|
|
118
|
+
return !state.includes(edge);
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/values.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime values (plan, Section 6.4). Values are immutable JavaScript data:
|
|
3
|
+
*
|
|
4
|
+
* Unit, none null
|
|
5
|
+
* Bool boolean
|
|
6
|
+
* Nat, Int, Node number
|
|
7
|
+
* Agent number (index in the configuration of the round)
|
|
8
|
+
* Frame (ring) boolean (mirrored)
|
|
9
|
+
* Move, enums, Verdict string (constructor name)
|
|
10
|
+
* String string
|
|
11
|
+
* Option T null or the value (options of options are not supported)
|
|
12
|
+
* records plain objects
|
|
13
|
+
* List arrays
|
|
14
|
+
* Multiset arrays sorted by encoding, so no order leaks
|
|
15
|
+
*
|
|
16
|
+
* Every type has a canonical, injective string encoding, used for state
|
|
17
|
+
* equality and hashing.
|
|
18
|
+
*/
|
|
19
|
+
import type { Type } from '@bramas/simulant-lang/ir';
|
|
20
|
+
|
|
21
|
+
export type V = any;
|
|
22
|
+
export type Encoder = (v: V) => string;
|
|
23
|
+
|
|
24
|
+
const encoders = new WeakMap<Type, Encoder>();
|
|
25
|
+
|
|
26
|
+
export function encoder(t: Type): Encoder {
|
|
27
|
+
let e = encoders.get(t);
|
|
28
|
+
if (!e) {
|
|
29
|
+
e = makeEncoder(t);
|
|
30
|
+
encoders.set(t, e);
|
|
31
|
+
}
|
|
32
|
+
return e;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeEncoder(t: Type): Encoder {
|
|
36
|
+
switch (t.k) {
|
|
37
|
+
case 'unit': return () => '';
|
|
38
|
+
case 'bool': return v => v ? '1' : '0';
|
|
39
|
+
case 'frame': return v => v ? 'm' : 'i';
|
|
40
|
+
case 'nat': case 'int': case 'node': case 'agent': return v => String(v);
|
|
41
|
+
case 'move': case 'enum': case 'verdict': return v => v;
|
|
42
|
+
case 'string': return v => JSON.stringify(v);
|
|
43
|
+
case 'option': {
|
|
44
|
+
const inner = encoder(t.of);
|
|
45
|
+
return v => v === null ? '-' : `+${inner(v)}`;
|
|
46
|
+
}
|
|
47
|
+
case 'list': case 'multiset': {
|
|
48
|
+
const inner = encoder(t.of);
|
|
49
|
+
return v => {
|
|
50
|
+
let s = '[';
|
|
51
|
+
for (let i = 0; i < v.length; i++) s += (i ? ',' : '') + inner(v[i]);
|
|
52
|
+
return s + ']';
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
case 'record': {
|
|
56
|
+
const fields = t.fields.map(f => [f.name, encoder(f.type)] as const);
|
|
57
|
+
return v => {
|
|
58
|
+
let s = '{';
|
|
59
|
+
for (let i = 0; i < fields.length; i++) s += (i ? ',' : '') + fields[i][1](v[fields[i][0]]);
|
|
60
|
+
return s + '}';
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
return () => { throw new Error(`values of type ${t.k} cannot be encoded`); };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isAtomic(t: Type) {
|
|
69
|
+
return !['option', 'list', 'multiset', 'record'].includes(t.k);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function equality(t: Type): (a: V, b: V) => boolean {
|
|
73
|
+
if (isAtomic(t)) return (a, b) => a === b;
|
|
74
|
+
if (t.k === 'option' && isAtomic(t.of)) return (a, b) => a === b;
|
|
75
|
+
const enc = encoder(t);
|
|
76
|
+
return (a, b) => a === b || enc(a) === enc(b);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Sorts the elements of a multiset into canonical order. */
|
|
80
|
+
export function canonicalSort(elem: Type): (xs: V[]) => V[] {
|
|
81
|
+
if (elem.k === 'nat' || elem.k === 'int' || elem.k === 'node' || elem.k === 'agent') {
|
|
82
|
+
return xs => xs.slice().sort((a, b) => a - b);
|
|
83
|
+
}
|
|
84
|
+
const enc = encoder(elem);
|
|
85
|
+
return xs => xs.map(x => [enc(x), x] as const).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0).map(p => p[1]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `show`, or, with `compact` (explanations of steps, for reading), lists of booleans as one
|
|
90
|
+
* word: `[XX.X.]` for `[true, true, false, true, false]`.
|
|
91
|
+
*/
|
|
92
|
+
export function show(t: Type, compact = false): (v: V) => string {
|
|
93
|
+
switch (t.k) {
|
|
94
|
+
case 'unit': return () => '()';
|
|
95
|
+
case 'agent': return v => `a${v}`;
|
|
96
|
+
case 'node': return v => `u${v}`;
|
|
97
|
+
case 'frame': return v => v ? 'mirrored' : 'as is';
|
|
98
|
+
case 'string': return v => v;
|
|
99
|
+
case 'option': {
|
|
100
|
+
const inner = show(t.of, compact);
|
|
101
|
+
return v => v === null ? 'none' : `some ${inner(v)}`;
|
|
102
|
+
}
|
|
103
|
+
case 'list': case 'multiset': {
|
|
104
|
+
if (compact && t.of.k === 'bool') return v => `[${v.map((b: boolean) => b ? 'X' : '.').join('')}]`;
|
|
105
|
+
const inner = show(t.of, compact);
|
|
106
|
+
return v => `[${v.map(inner).join(', ')}]`;
|
|
107
|
+
}
|
|
108
|
+
case 'record': {
|
|
109
|
+
const fields = t.fields.map(f => [f.name, show(f.type, compact)] as const);
|
|
110
|
+
return v => `{ ${fields.map(([n, s]) => `${n} := ${s(v[n])}`).join(', ')} }`;
|
|
111
|
+
}
|
|
112
|
+
default: return v => String(v);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** How an automorphism acts on the values of a type: nodes are mapped, frames composed. */
|
|
117
|
+
export type Action = { node: (n: number) => number; frame: (f: V) => V };
|
|
118
|
+
|
|
119
|
+
/** The action on values of type `t`, or `undefined` when it is the identity. */
|
|
120
|
+
export function mapper(t: Type): ((v: V, g: Action) => V) | undefined {
|
|
121
|
+
switch (t.k) {
|
|
122
|
+
case 'node': return (v, g) => g.node(v);
|
|
123
|
+
case 'frame': return (v, g) => g.frame(v);
|
|
124
|
+
case 'option': {
|
|
125
|
+
const inner = mapper(t.of);
|
|
126
|
+
return inner && ((v, g) => v === null ? null : inner(v, g));
|
|
127
|
+
}
|
|
128
|
+
case 'list': {
|
|
129
|
+
const inner = mapper(t.of);
|
|
130
|
+
return inner && ((v, g) => v.map((x: V) => inner(x, g)));
|
|
131
|
+
}
|
|
132
|
+
case 'multiset': {
|
|
133
|
+
const inner = mapper(t.of);
|
|
134
|
+
const sort = canonicalSort(t.of);
|
|
135
|
+
return inner && ((v, g) => sort(v.map((x: V) => inner(x, g))));
|
|
136
|
+
}
|
|
137
|
+
case 'record': {
|
|
138
|
+
const fields = t.fields.map(f => [f.name, mapper(f.type)] as const).filter(([, m]) => m);
|
|
139
|
+
if (!fields.length) return undefined;
|
|
140
|
+
return (v, g) => {
|
|
141
|
+
const out = { ...v };
|
|
142
|
+
for (const [n, m] of fields) out[n] = m!(v[n], g);
|
|
143
|
+
return out;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
default: return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
package/README.md
DELETED