@bramas/simulant-lang 0.0.0-stage → 0.1.0
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
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { T } from "./types.js";
|
|
2
|
+
export function resolveTypes(exp, resolutions, onCopy) {
|
|
3
|
+
const copy = (x) => {
|
|
4
|
+
if (!x || typeof x !== 'object')
|
|
5
|
+
return x;
|
|
6
|
+
if (Array.isArray(x))
|
|
7
|
+
return x.map(copy);
|
|
8
|
+
const o = x;
|
|
9
|
+
if (typeof o.e === 'string') {
|
|
10
|
+
const e = o;
|
|
11
|
+
let out;
|
|
12
|
+
if (e.e === 'lit' && e.type.k === 'infer') {
|
|
13
|
+
const t = resolutions.get(e);
|
|
14
|
+
if (!t)
|
|
15
|
+
throw new Error(`a literal was not resolved: ${JSON.stringify(e.value)}`);
|
|
16
|
+
out = { e: 'lit', type: t, value: e.value };
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
out = Object.fromEntries(Object.entries(o).map(([k, v]) => [k, copy(v)]));
|
|
20
|
+
if (out.e === 'call' && out.fn === 'list' && out.args.every(a => a.e === 'lit')) {
|
|
21
|
+
const items = out.args;
|
|
22
|
+
out = { e: 'lit', type: T.list(items[0].type), value: items.map(a => a.value) };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
onCopy?.(e, out);
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
return Object.fromEntries(Object.entries(o).map(([k, v]) => [k, copy(v)]));
|
|
29
|
+
};
|
|
30
|
+
return copy(exp);
|
|
31
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks and resolves a component on its own, in the smallest experiment
|
|
3
|
+
* that contains it. The Lean library is generated from components resolved
|
|
4
|
+
* this way, independently of any model.
|
|
5
|
+
*/
|
|
6
|
+
import { check } from "./check.js";
|
|
7
|
+
import { IR_VERSION } from "./ir.js";
|
|
8
|
+
import { resolveTypes } from "./resolve.js";
|
|
9
|
+
import { T } from "./types.js";
|
|
10
|
+
/** Experiment parameters read by the component's initial values (`site := param site`), with their types. */
|
|
11
|
+
function inputs(c) {
|
|
12
|
+
const found = new Map();
|
|
13
|
+
const fields = [...(c.world ?? []), ...(c.agent ?? [])];
|
|
14
|
+
for (const f of fields) {
|
|
15
|
+
const init = c.init?.world?.[f.name] ?? c.init?.agent?.[f.name];
|
|
16
|
+
if (init?.e === 'param')
|
|
17
|
+
found.set(init.name, f.type);
|
|
18
|
+
}
|
|
19
|
+
return [...found].map(([name, type]) => ({ name, type }));
|
|
20
|
+
}
|
|
21
|
+
export function standaloneExperiment(c) {
|
|
22
|
+
const lit = (type, value) => ({ e: 'lit', type, value });
|
|
23
|
+
return {
|
|
24
|
+
irVersion: IR_VERSION,
|
|
25
|
+
name: `${c.name} alone`,
|
|
26
|
+
model: {
|
|
27
|
+
name: c.name, topology: { kind: 'ring', size: { e: 'param', name: 'N' } }, sensors: [], memory: 'persistent',
|
|
28
|
+
frame: { kind: 'common' }, round: 'fsync', movement: 'rigid', components: [c], symmetries: 'derived',
|
|
29
|
+
},
|
|
30
|
+
algorithm: {
|
|
31
|
+
name: 'idle', uses: [], pub: T.unit, mem: T.unit, init: { pub: lit(T.unit, null), mem: lit(T.unit, null) },
|
|
32
|
+
step: { e: 'record', fields: { move: lit(T.move, 'stay'), pub: { e: 'var', name: 'pub' }, mem: { e: 'var', name: 'mem' } } },
|
|
33
|
+
},
|
|
34
|
+
task: { name: 'none', goal: 'safety', verdict: lit(T.verdict, 'inProgress') },
|
|
35
|
+
params: [{ name: 'N', type: T.nat }, ...inputs(c)],
|
|
36
|
+
agents: { count: lit(T.nat, 1), pos: lit(T.node, 0) },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function resolveComponent(c) {
|
|
40
|
+
const exp = standaloneExperiment(c);
|
|
41
|
+
const checked = check(exp);
|
|
42
|
+
if (checked.errors.length)
|
|
43
|
+
throw new Error(`${c.name}:\n${checked.errors.join('\n')}`);
|
|
44
|
+
return resolveTypes(exp, checked.resolutions).model.components[0];
|
|
45
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export const T = {
|
|
2
|
+
unit: { k: 'unit' },
|
|
3
|
+
bool: { k: 'bool' },
|
|
4
|
+
nat: { k: 'nat' },
|
|
5
|
+
int: { k: 'int' },
|
|
6
|
+
string: { k: 'string' },
|
|
7
|
+
node: { k: 'node' },
|
|
8
|
+
move: { k: 'move' },
|
|
9
|
+
frame: { k: 'frame' },
|
|
10
|
+
agent: { k: 'agent' },
|
|
11
|
+
config: { k: 'config' },
|
|
12
|
+
effect: { k: 'effect' },
|
|
13
|
+
decision: { k: 'decision' },
|
|
14
|
+
verdict: { k: 'verdict' },
|
|
15
|
+
pub: { k: 'pub' },
|
|
16
|
+
infer: { k: 'infer' },
|
|
17
|
+
enum: (name, ctors) => ({ k: 'enum', name, ctors }),
|
|
18
|
+
option: (of) => ({ k: 'option', of }),
|
|
19
|
+
record: (fields) => ({ k: 'record', fields: Array.isArray(fields) ? fields : Object.entries(fields).map(([name, type]) => ({ name, type })) }),
|
|
20
|
+
list: (of) => ({ k: 'list', of }),
|
|
21
|
+
multiset: (of) => ({ k: 'multiset', of }),
|
|
22
|
+
fn: (params, ret) => ({ k: 'fn', params, ret }),
|
|
23
|
+
};
|
|
24
|
+
export const MOVES = ['stay', 'cw', 'ccw'];
|
|
25
|
+
export const VERDICTS = ['success', 'failure', 'inProgress'];
|
|
26
|
+
export function typeEq(a, b) {
|
|
27
|
+
if (a.k !== b.k)
|
|
28
|
+
return false;
|
|
29
|
+
switch (a.k) {
|
|
30
|
+
case 'enum': {
|
|
31
|
+
const o = b;
|
|
32
|
+
return a.name === o.name && a.ctors.length === o.ctors.length && a.ctors.every((c, i) => c === o.ctors[i]);
|
|
33
|
+
}
|
|
34
|
+
case 'option':
|
|
35
|
+
case 'list':
|
|
36
|
+
case 'multiset':
|
|
37
|
+
return typeEq(a.of, b.of);
|
|
38
|
+
case 'record': {
|
|
39
|
+
const o = b;
|
|
40
|
+
// Fields are matched by name, as in Lean structure instances.
|
|
41
|
+
return a.fields.length === o.fields.length
|
|
42
|
+
&& a.fields.every(f => o.fields.some(g => g.name === f.name && typeEq(f.type, g.type)));
|
|
43
|
+
}
|
|
44
|
+
case 'fn': {
|
|
45
|
+
const o = b;
|
|
46
|
+
return a.params.length === o.params.length && a.params.every((p, i) => typeEq(p, o.params[i])) && typeEq(a.ret, o.ret);
|
|
47
|
+
}
|
|
48
|
+
default:
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function showType(t) {
|
|
53
|
+
switch (t.k) {
|
|
54
|
+
case 'enum': return t.name;
|
|
55
|
+
case 'option': return `Option ${wrap(t.of)}`;
|
|
56
|
+
case 'list': return `List ${wrap(t.of)}`;
|
|
57
|
+
case 'multiset': return `Multiset ${wrap(t.of)}`;
|
|
58
|
+
case 'record': return `{ ${t.fields.map(f => `${f.name} : ${showType(f.type)}`).join(', ')} }`;
|
|
59
|
+
case 'fn': return `(${t.params.map(showType).join(' → ')} → ${showType(t.ret)})`;
|
|
60
|
+
default: return t.k[0].toUpperCase() + t.k.slice(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function wrap(t) {
|
|
64
|
+
const s = showType(t);
|
|
65
|
+
return /\s/.test(s) && !s.startsWith('{') ? `(${s})` : s;
|
|
66
|
+
}
|
|
67
|
+
/** Types whose values can be compared, stored in configurations and encoded. */
|
|
68
|
+
export function isData(t) {
|
|
69
|
+
switch (t.k) {
|
|
70
|
+
case 'fn':
|
|
71
|
+
case 'config':
|
|
72
|
+
case 'effect':
|
|
73
|
+
case 'decision': return false;
|
|
74
|
+
case 'option':
|
|
75
|
+
case 'list':
|
|
76
|
+
case 'multiset': return isData(t.of);
|
|
77
|
+
case 'record': return t.fields.every(f => isData(f.type));
|
|
78
|
+
default: return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Data that can be kept in a configuration: agents are only references within a round. */
|
|
82
|
+
export function isStorable(t) {
|
|
83
|
+
switch (t.k) {
|
|
84
|
+
case 'agent': return false;
|
|
85
|
+
case 'option':
|
|
86
|
+
case 'list':
|
|
87
|
+
case 'multiset': return isStorable(t.of);
|
|
88
|
+
case 'record': return t.fields.every(f => isStorable(f.type));
|
|
89
|
+
default: return isData(t);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function isCollection(t) {
|
|
93
|
+
return t.k === 'list' || t.k === 'multiset';
|
|
94
|
+
}
|
|
95
|
+
export function fieldType(t, name) {
|
|
96
|
+
return t.k === 'record' ? t.fields.find(f => f.name === name)?.type : undefined;
|
|
97
|
+
}
|
|
98
|
+
/** Replaces the `pub` placeholder by the public state of the composition. */
|
|
99
|
+
export function resolvePub(t, pub) {
|
|
100
|
+
switch (t.k) {
|
|
101
|
+
case 'pub': return pub;
|
|
102
|
+
case 'option':
|
|
103
|
+
case 'list':
|
|
104
|
+
case 'multiset': return { k: t.k, of: resolvePub(t.of, pub) };
|
|
105
|
+
case 'record': return { k: 'record', fields: t.fields.map(f => ({ name: f.name, type: resolvePub(f.type, pub) })) };
|
|
106
|
+
case 'fn': return { k: 'fn', params: t.params.map(p => resolvePub(p, pub)), ret: resolvePub(t.ret, pub) };
|
|
107
|
+
default: return t;
|
|
108
|
+
}
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bramas/simulant-lang",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "CECILL-B",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./*": {
|
|
9
|
+
"types": "./src/*.ts",
|
|
10
|
+
"default": "./dist/*.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"registry": "https://registry.npmjs.org",
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"dist",
|
|
20
|
+
"LICENCE.txt"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@bramas/simulant-stdlib": "0.1.0"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/Bramas/simulant.git",
|
|
28
|
+
"directory": "packages/lang"
|
|
29
|
+
}
|
|
6
30
|
}
|
package/src/builder.ts
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript API for constructing IR independently of the `.sim` frontend.
|
|
3
|
+
* Used by conformance and unit tests, including the hand-written standard
|
|
4
|
+
* library counterparts in packages/stdlib/test/helpers (see packages/stdlib/README.md).
|
|
5
|
+
*
|
|
6
|
+
* Expressions are wrapped in proxies: reading a property builds a field
|
|
7
|
+
* access (`a.pos`), and the methods below build builtin calls
|
|
8
|
+
* (`C.agents.filter(b => b.pos.eq(me.pos))`).
|
|
9
|
+
*/
|
|
10
|
+
import type {
|
|
11
|
+
Action, Algorithm, Component, Equivariance, Experiment, Expr, Field, Flag, FrameModel, Goal, Json, Model, Param,
|
|
12
|
+
Sensor, Task, Topology, Type,
|
|
13
|
+
} from './ir';
|
|
14
|
+
import { IR_VERSION } from './ir';
|
|
15
|
+
import { T } from './types';
|
|
16
|
+
|
|
17
|
+
export type Arg = X | boolean | number;
|
|
18
|
+
|
|
19
|
+
/** An expression under construction. Unknown properties are field accesses. */
|
|
20
|
+
export interface X {
|
|
21
|
+
readonly ir: Expr;
|
|
22
|
+
readonly [field: string]: any;
|
|
23
|
+
// logic and comparison
|
|
24
|
+
not(): X;
|
|
25
|
+
and(...xs: Arg[]): X;
|
|
26
|
+
or(...xs: Arg[]): X;
|
|
27
|
+
implies(x: Arg): X;
|
|
28
|
+
eq(x: Arg): X;
|
|
29
|
+
ne(x: Arg): X;
|
|
30
|
+
lt(x: Arg): X;
|
|
31
|
+
le(x: Arg): X;
|
|
32
|
+
gt(x: Arg): X;
|
|
33
|
+
ge(x: Arg): X;
|
|
34
|
+
// arithmetic
|
|
35
|
+
add(x: Arg): X;
|
|
36
|
+
sub(x: Arg): X;
|
|
37
|
+
mul(x: Arg): X;
|
|
38
|
+
div(x: Arg): X;
|
|
39
|
+
mod(x: Arg): X;
|
|
40
|
+
// options
|
|
41
|
+
isSome(): X;
|
|
42
|
+
isNone(): X;
|
|
43
|
+
getD(d: Arg): X;
|
|
44
|
+
// collections
|
|
45
|
+
length(): X;
|
|
46
|
+
isEmpty(): X;
|
|
47
|
+
filter(f: (x: X) => Arg): X;
|
|
48
|
+
map(f: (x: X) => Arg): X;
|
|
49
|
+
any(f: (x: X) => Arg): X;
|
|
50
|
+
all(f: (x: X) => Arg): X;
|
|
51
|
+
count(f: (x: X) => Arg): X;
|
|
52
|
+
sum(): X;
|
|
53
|
+
has(x: Arg): X;
|
|
54
|
+
at(i: Arg): X;
|
|
55
|
+
append(x: Arg): X;
|
|
56
|
+
diff(x: Arg): X;
|
|
57
|
+
toMultiset(): X;
|
|
58
|
+
toList(): X;
|
|
59
|
+
dedup(): X;
|
|
60
|
+
// records
|
|
61
|
+
with(fields: Record<string, Arg>): X;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Nesting depth of lambdas under construction: parameter names depend only on it, so the IR is reproducible. */
|
|
65
|
+
let depth = 0;
|
|
66
|
+
|
|
67
|
+
export function ir(x: Arg): Expr {
|
|
68
|
+
if (typeof x === 'boolean') return { e: 'lit', type: T.bool, value: x };
|
|
69
|
+
if (typeof x === 'number') {
|
|
70
|
+
if (!Number.isInteger(x)) throw new Error(`not an integer: ${x}`);
|
|
71
|
+
return { e: 'lit', type: x < 0 ? T.int : T.nat, value: x };
|
|
72
|
+
}
|
|
73
|
+
return x.ir;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const call = (fn: string, ...args: Arg[]): X => wrap({ e: 'call', fn, args: args.map(ir) });
|
|
77
|
+
|
|
78
|
+
/** A lambda; parameter types are inferred by the checker. */
|
|
79
|
+
function lambda(arity: number, f: (...xs: X[]) => Arg): Expr {
|
|
80
|
+
const params = Array.from({ length: arity }, (_, i) => ({ name: arity === 1 ? `x${depth}` : `x${depth}_${i}` }));
|
|
81
|
+
depth++;
|
|
82
|
+
try {
|
|
83
|
+
return { e: 'fn', params, body: ir(f(...params.map(p => v(p.name)))) };
|
|
84
|
+
} finally {
|
|
85
|
+
depth--;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const methods: Record<string, (self: Expr, ...args: any[]) => Expr> = {
|
|
90
|
+
not: self => ({ e: 'call', fn: 'not', args: [self] }),
|
|
91
|
+
and: (self, ...xs: Arg[]) => ({ e: 'call', fn: 'and', args: [self, ...xs.map(ir)] }),
|
|
92
|
+
or: (self, ...xs: Arg[]) => ({ e: 'call', fn: 'or', args: [self, ...xs.map(ir)] }),
|
|
93
|
+
length: self => ({ e: 'call', fn: 'length', args: [self] }),
|
|
94
|
+
isEmpty: self => ({ e: 'call', fn: 'isEmpty', args: [self] }),
|
|
95
|
+
isSome: self => ({ e: 'call', fn: 'isSome', args: [self] }),
|
|
96
|
+
isNone: self => ({ e: 'call', fn: 'isNone', args: [self] }),
|
|
97
|
+
sum: self => ({ e: 'call', fn: 'sum', args: [self] }),
|
|
98
|
+
toMultiset: self => ({ e: 'call', fn: 'toMultiset', args: [self] }),
|
|
99
|
+
toList: self => ({ e: 'call', fn: 'toList', args: [self] }),
|
|
100
|
+
dedup: self => ({ e: 'call', fn: 'dedup', args: [self] }),
|
|
101
|
+
has: (self, x: Arg) => ({ e: 'call', fn: 'mem', args: [ir(x), self] }),
|
|
102
|
+
with: (self, fields: Record<string, Arg>) => ({ e: 'with', on: self, fields: mapValues(fields, ir) }),
|
|
103
|
+
};
|
|
104
|
+
for (const fn of ['implies', 'eq', 'ne', 'lt', 'le', 'gt', 'ge', 'add', 'sub', 'mul', 'div', 'mod', 'getD', 'at', 'append', 'diff']) {
|
|
105
|
+
methods[fn] = (self, x: Arg) => ({ e: 'call', fn, args: [self, ir(x)] });
|
|
106
|
+
}
|
|
107
|
+
for (const fn of ['filter', 'map', 'any', 'all', 'count']) {
|
|
108
|
+
methods[fn] = (self, f: (x: X) => Arg) => ({ e: 'call', fn, args: [self, lambda(1, f)] });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function wrap(expr: Expr): X {
|
|
112
|
+
return new Proxy({ ir: expr } as X, {
|
|
113
|
+
get(target, prop) {
|
|
114
|
+
if (prop === 'ir') return expr;
|
|
115
|
+
if (typeof prop !== 'string') return (target as any)[prop];
|
|
116
|
+
const m = methods[prop];
|
|
117
|
+
if (m) return (...args: unknown[]) => wrap(m(expr, ...args));
|
|
118
|
+
return wrap({ e: 'get', on: expr, field: prop });
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mapValues<A, B>(o: Record<string, A>, f: (a: A) => B): Record<string, B> {
|
|
124
|
+
return Object.fromEntries(Object.entries(o).map(([k, a]) => [k, f(a)]));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Expressions
|
|
129
|
+
|
|
130
|
+
export const v = (name: string): X => wrap({ e: 'var', name });
|
|
131
|
+
export const lit = (type: Type, value: Json): X => wrap({ e: 'lit', type, value });
|
|
132
|
+
export const nat = (n: number) => lit(T.nat, n);
|
|
133
|
+
export const int = (n: number) => lit(T.int, n);
|
|
134
|
+
export const str = (s: string) => lit(T.string, s);
|
|
135
|
+
export const move = (m: 'stay' | 'cw' | 'ccw') => lit(T.move, m);
|
|
136
|
+
export const verdict = (s: 'success' | 'failure' | 'inProgress') => lit(T.verdict, s);
|
|
137
|
+
export const ctor = (type: Type, name: string) => lit(type, name);
|
|
138
|
+
export const none = (of: Type) => lit(T.option(of), null);
|
|
139
|
+
export const unit = () => lit(T.unit, null);
|
|
140
|
+
export const emptyList = (of: Type) => lit(T.list(of), []);
|
|
141
|
+
export const some = (x: Arg) => call('some', x);
|
|
142
|
+
export const param = (name: string): X => wrap({ e: 'param', name });
|
|
143
|
+
|
|
144
|
+
export const not = (x: Arg) => call('not', x);
|
|
145
|
+
export const and = (...xs: Arg[]) => call('and', ...xs);
|
|
146
|
+
export const or = (...xs: Arg[]) => call('or', ...xs);
|
|
147
|
+
export const min = (a: Arg, b: Arg) => call('min', a, b);
|
|
148
|
+
export const max = (a: Arg, b: Arg) => call('max', a, b);
|
|
149
|
+
|
|
150
|
+
export function ite(cond: Arg, then: Arg, otherwise: Arg): X {
|
|
151
|
+
return wrap({ e: 'if', cond: ir(cond), then: ir(then), else: ir(otherwise) });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function match(on: Arg, cases: Record<string, Arg>, otherwise?: Arg): X {
|
|
155
|
+
return wrap({ e: 'match', on: ir(on), cases: mapValues(cases, ir), ...(otherwise === undefined ? {} : { else: ir(otherwise) }) });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** `let name := value; body`. */
|
|
159
|
+
export function let_(name: string, value: Arg, body: (x: X) => Arg): X {
|
|
160
|
+
return wrap({ e: 'let', name, value: ir(value), body: ir(body(v(name))) });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function rec(fields: Record<string, Arg>): X {
|
|
164
|
+
return wrap({ e: 'record', fields: mapValues(fields, ir) });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** An adversary choice; `site` names it in witnesses. */
|
|
168
|
+
export function choose(site: string, from: Arg, opts: { such?: (x: X) => Arg; label?: (x: X) => Arg } = {}): X {
|
|
169
|
+
return wrap({
|
|
170
|
+
e: 'choose', site, from: ir(from),
|
|
171
|
+
...(opts.such ? { such: lambda(1, opts.such) } : {}),
|
|
172
|
+
...(opts.label ? { label: lambda(1, opts.label) } : {}),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A hook's result: raise a flag on a collection of agents, or on those satisfying a predicate. */
|
|
177
|
+
export function effect(e: { world?: Record<string, Arg>; raise?: Record<string, X | ((a: X) => Arg)> } = {}): X {
|
|
178
|
+
return wrap({
|
|
179
|
+
e: 'effect',
|
|
180
|
+
...(e.world ? { world: mapValues(e.world, ir) } : {}),
|
|
181
|
+
...(e.raise ? { raise: mapValues(e.raise, r => typeof r === 'function' ? lambda(1, r as (a: X) => Arg) : ir(r as X)) } : {}),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const nothing = () => effect();
|
|
186
|
+
|
|
187
|
+
/** A call to a definition of the model or of the piece. */
|
|
188
|
+
export const callDef = (name: string, ...args: Arg[]) => call(name, ...args);
|
|
189
|
+
|
|
190
|
+
// Builtins that are not methods
|
|
191
|
+
export const agents = (C: X) => call('agents', C);
|
|
192
|
+
export const nodes = (C: X) => call('nodes', C);
|
|
193
|
+
export const size = (C: X) => call('size', C);
|
|
194
|
+
export const correct = (a: X) => call('correct', a);
|
|
195
|
+
export const present = (a: X) => call('present', a);
|
|
196
|
+
export const decisionOf = (a: X) => call('decisionOf', a);
|
|
197
|
+
export const target = (n: Arg, m: Arg) => call('target', n, m);
|
|
198
|
+
export const edgePresent = (n: Arg, m: Arg) => call('edgePresent', n, m);
|
|
199
|
+
export const offset = (n: Arg, frame: Arg, i: Arg) => call('offset', n, frame, i);
|
|
200
|
+
export const toGlobal = (frame: Arg, m: Arg) => call('toGlobal', frame, m);
|
|
201
|
+
export const opposite = (m: Arg) => call('opposite', m);
|
|
202
|
+
export const frames = () => call('frames');
|
|
203
|
+
export const node = (n: Arg) => call('node', n);
|
|
204
|
+
export const range = (n: Arg) => call('range', n);
|
|
205
|
+
export const subsets = (l: Arg) => call('subsets', l);
|
|
206
|
+
export const subsetsUpTo = (l: Arg, k: Arg) => call('subsetsUpTo', l, k);
|
|
207
|
+
export const ofFn = (n: Arg, f: (i: X) => Arg) => wrap({ e: 'call', fn: 'ofFn', args: [ir(n), lambda(1, f)] });
|
|
208
|
+
export const show = (x: Arg) => call('show', x);
|
|
209
|
+
export const concat = (...xs: (Arg | string)[]) => call('concat', ...xs.map(x => typeof x === 'string' ? str(x) : x));
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Pieces
|
|
213
|
+
|
|
214
|
+
const fields = (o?: Record<string, Type>): Field[] | undefined =>
|
|
215
|
+
o && Object.entries(o).map(([name, type]) => ({ name, type }));
|
|
216
|
+
|
|
217
|
+
export function flag(name: string, visibility: Flag['visibility'], acts: Flag['acts']): Flag {
|
|
218
|
+
return { name, visibility, acts };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** A sensor: `C`, `me` and `frame` are in scope. */
|
|
222
|
+
export function sensor(name: string, type: Type, body: (s: { C: X; me: X; frame: X }) => Arg, equivariance?: Equivariance): Sensor {
|
|
223
|
+
return { name, type, body: ir(body({ C: v('C'), me: v('me'), frame: v('frame') })), ...(equivariance ? { equivariance } : {}) };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function def(name: string, params: Record<string, Type>, ret: Type, body: (args: Record<string, X>) => Arg) {
|
|
227
|
+
const ps = fields(params)!;
|
|
228
|
+
return { name, params: ps, ret, body: ir(body(Object.fromEntries(ps.map(p => [p.name, v(p.name)])))) };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The parameters of a component, as variables. */
|
|
232
|
+
export type Params = Record<string, X>;
|
|
233
|
+
|
|
234
|
+
export type ComponentSpec = {
|
|
235
|
+
name: string;
|
|
236
|
+
/** Parameters of the family, and their values in this instance. */
|
|
237
|
+
params?: Record<string, Type>;
|
|
238
|
+
args?: Record<string, Json>;
|
|
239
|
+
world?: Record<string, Type>;
|
|
240
|
+
agent?: Record<string, Type>;
|
|
241
|
+
flags?: Flag[];
|
|
242
|
+
sensors?: Sensor[];
|
|
243
|
+
actions?: Action[];
|
|
244
|
+
init?: { world?: Record<string, Arg | ((p: Params) => Arg)>; agent?: Record<string, (i: X, p: Params) => Arg> };
|
|
245
|
+
beforeLook?: (C: X, p: Params) => Arg;
|
|
246
|
+
beforeMove?: (C: X, p: Params) => Arg;
|
|
247
|
+
afterMove?: (C: X, p: Params) => Arg;
|
|
248
|
+
defs?: Component['defs'];
|
|
249
|
+
equivariance?: Equivariance;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export function component(spec: ComponentSpec): Component {
|
|
253
|
+
const p: Params = Object.fromEntries(Object.keys(spec.params ?? {}).map(name => [name, v(name)]));
|
|
254
|
+
const hook = (h?: (C: X, p: Params) => Arg) => h && ir(h(v('C'), p));
|
|
255
|
+
const c: Component = {
|
|
256
|
+
name: spec.name,
|
|
257
|
+
params: fields(spec.params),
|
|
258
|
+
args: spec.args,
|
|
259
|
+
world: fields(spec.world),
|
|
260
|
+
agent: fields(spec.agent),
|
|
261
|
+
flags: spec.flags,
|
|
262
|
+
sensors: spec.sensors,
|
|
263
|
+
actions: spec.actions,
|
|
264
|
+
init: spec.init && dropUndefined({
|
|
265
|
+
world: spec.init.world && mapValues(spec.init.world, x => ir(typeof x === 'function' ? x(p) : x)),
|
|
266
|
+
agent: spec.init.agent && mapValues(spec.init.agent, f => ir(f(v('i'), p))),
|
|
267
|
+
}),
|
|
268
|
+
beforeLook: hook(spec.beforeLook),
|
|
269
|
+
beforeMove: hook(spec.beforeMove),
|
|
270
|
+
afterMove: hook(spec.afterMove),
|
|
271
|
+
defs: spec.defs,
|
|
272
|
+
equivariance: spec.equivariance,
|
|
273
|
+
};
|
|
274
|
+
return dropUndefined(c);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export type ModelSpec = {
|
|
278
|
+
name: string;
|
|
279
|
+
topology: Topology;
|
|
280
|
+
sensors: Sensor[];
|
|
281
|
+
memory?: Model['memory'];
|
|
282
|
+
frame?: FrameModel;
|
|
283
|
+
round?: Model['round'];
|
|
284
|
+
components?: Component[];
|
|
285
|
+
symmetries?: Model['symmetries'];
|
|
286
|
+
defs?: Model['defs'];
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export function model(spec: ModelSpec): Model {
|
|
290
|
+
return dropUndefined({
|
|
291
|
+
name: spec.name,
|
|
292
|
+
topology: spec.topology,
|
|
293
|
+
sensors: spec.sensors,
|
|
294
|
+
memory: spec.memory ?? 'persistent',
|
|
295
|
+
frame: spec.frame ?? { kind: 'common' },
|
|
296
|
+
round: spec.round ?? 'fsync',
|
|
297
|
+
movement: 'rigid',
|
|
298
|
+
components: spec.components ?? [],
|
|
299
|
+
symmetries: spec.symmetries ?? 'derived',
|
|
300
|
+
defs: spec.defs,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export const ring = (size: Arg): Topology => ({ kind: 'ring', size: ir(size) });
|
|
305
|
+
export const dynamicRing = (size: Arg, k = 1): Topology => ({ kind: 'dynamicRing', size: ir(size), k });
|
|
306
|
+
|
|
307
|
+
export type AlgorithmSpec = {
|
|
308
|
+
name: string;
|
|
309
|
+
uses: string[];
|
|
310
|
+
pub: Type;
|
|
311
|
+
mem?: Type;
|
|
312
|
+
init: { pub: (i: X) => Arg; mem?: (i: X) => Arg };
|
|
313
|
+
/** Returns `{ move, pub, mem, ...actions }`; missing `pub` and `mem` are kept unchanged. */
|
|
314
|
+
step?: (s: { mem: X; pub: X; view: X }) => Record<string, Arg>;
|
|
315
|
+
line?: (mem: X) => Arg;
|
|
316
|
+
native?: string;
|
|
317
|
+
defs?: Algorithm['defs'];
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export function algorithm(spec: AlgorithmSpec): Algorithm {
|
|
321
|
+
const mem = spec.mem ?? T.unit;
|
|
322
|
+
const s = { mem: v('mem'), pub: v('pub'), view: v('view') };
|
|
323
|
+
const step = spec.step ? spec.step(s) : { move: move('stay') };
|
|
324
|
+
return dropUndefined({
|
|
325
|
+
name: spec.name,
|
|
326
|
+
uses: spec.uses,
|
|
327
|
+
pub: spec.pub,
|
|
328
|
+
mem,
|
|
329
|
+
init: { pub: ir(spec.init.pub(v('i'))), mem: ir(spec.init.mem ? spec.init.mem(v('i')) : unit()) },
|
|
330
|
+
step: rec({ pub: s.pub, mem: s.mem, ...step }).ir,
|
|
331
|
+
line: spec.line && ir(spec.line(v('mem'))),
|
|
332
|
+
native: spec.native,
|
|
333
|
+
defs: spec.defs,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function task(spec: { name: string; goal: Goal; verdict: (C: X) => Arg; equivariance?: Equivariance; defs?: Task['defs'] }): Task {
|
|
338
|
+
return dropUndefined({ name: spec.name, goal: spec.goal, verdict: ir(spec.verdict(v('C'))), equivariance: spec.equivariance, defs: spec.defs });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function experiment(spec: {
|
|
342
|
+
name: string; model: Model; algorithm: Algorithm; task: Task;
|
|
343
|
+
params?: { name: string; type: Type; domain?: Arg }[];
|
|
344
|
+
agents: { count: Arg; pos: (i: X) => Arg };
|
|
345
|
+
limits?: Experiment['limits'];
|
|
346
|
+
}): Experiment {
|
|
347
|
+
const params: Param[] = (spec.params ?? []).map(p => dropUndefined({ name: p.name, type: p.type, domain: p.domain === undefined ? undefined : ir(p.domain) }));
|
|
348
|
+
return dropUndefined({
|
|
349
|
+
irVersion: IR_VERSION, name: spec.name, model: spec.model, algorithm: spec.algorithm, task: spec.task, params,
|
|
350
|
+
agents: { count: ir(spec.agents.count), pos: ir(spec.agents.pos(v('i'))) },
|
|
351
|
+
limits: spec.limits,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Keeps the IR free of `undefined`, so that it round-trips through JSON unchanged. */
|
|
356
|
+
function dropUndefined<O extends object>(o: O): O {
|
|
357
|
+
return Object.fromEntries(Object.entries(o).filter(([, x]) => x !== undefined)) as O;
|
|
358
|
+
}
|