@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.
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Signatures of the builtin functions of the IR. The engine implements the
3
+ * same names (packages/engine/src/builtins.ts).
4
+ */
5
+ import type { Type } from './ir';
6
+ import { isCollection, isData, showType, T, typeEq } from './types';
7
+
8
+ /** Where an expression is evaluated. */
9
+ export type Context = 'init' | 'domain' | 'sensor' | 'hook' | 'beforeMove' | 'task' | 'algorithm' | 'def';
10
+
11
+ export type Signature = {
12
+ /** Number of arguments; `undefined` means at least one. */
13
+ arity?: number;
14
+ /** Parameter types of the lambda passed at a position, from the arguments before it. */
15
+ lambdas?: Record<number, (before: Type[]) => Type[] | string>;
16
+ ret: (args: Type[]) => Type | string;
17
+ /** Contexts where the builtin is allowed (default: all). */
18
+ only?: Context[];
19
+ /** Reads the configuration in scope. */
20
+ config?: boolean;
21
+ /** Breaks equivariance under rotations (absolute node numbers). */
22
+ absolute?: boolean;
23
+ };
24
+
25
+ const err = (msg: string) => msg;
26
+
27
+ function mentionsAgent(t: Type): boolean {
28
+ switch (t.k) {
29
+ case 'agent': return true;
30
+ case 'option': case 'list': case 'multiset': return mentionsAgent(t.of);
31
+ case 'record': return t.fields.some(f => mentionsAgent(f.type));
32
+ default: return false;
33
+ }
34
+ }
35
+
36
+ function same(name: string) {
37
+ return ([a, b]: Type[]) => typeEq(a, b) ? (isData(a) ? T.bool : err(`${name}: cannot compare ${showType(a)}`))
38
+ : err(`${name}: ${showType(a)} and ${showType(b)} differ`);
39
+ }
40
+
41
+ function numeric(name: string, ret?: Type) {
42
+ return (args: Type[]) => {
43
+ const t = args[0];
44
+ if (t.k !== 'nat' && t.k !== 'int') return err(`${name}: expected Nat or Int, got ${showType(t)}`);
45
+ if (!args.every(a => typeEq(a, t))) return err(`${name}: arguments must have the same type`);
46
+ return ret ?? t;
47
+ };
48
+ }
49
+
50
+ /** Numbers, or lists of Bool or Nat in lexicographic order (`false < true`, a prefix first), as in Lean. */
51
+ function ordered(name: string) {
52
+ return (args: Type[]) => {
53
+ const [a, b] = args;
54
+ if (a.k === 'list' && (a.of.k === 'bool' || a.of.k === 'nat')) {
55
+ return typeEq(a, b) ? T.bool : err(`${name}: ${showType(a)} and ${showType(b)} differ`);
56
+ }
57
+ return numeric(name, T.bool)(args);
58
+ };
59
+ }
60
+
61
+ function bools(name: string) {
62
+ return (args: Type[]) => args.every(a => a.k === 'bool') ? T.bool : err(`${name}: expected Bool arguments`);
63
+ }
64
+
65
+ function collection(name: string, t: Type) {
66
+ return isCollection(t) ? undefined : err(`${name}: expected a List or Multiset, got ${showType(t)}`);
67
+ }
68
+
69
+ const elem = (name: string) => ([l]: Type[]) => collection(name, l) ?? [(l as { of: Type }).of];
70
+
71
+ function predicate(name: string, ret: (l: Type) => Type) {
72
+ return {
73
+ arity: 2,
74
+ lambdas: { 1: elem(name) },
75
+ ret: ([l, f]: Type[]) => collection(name, l)
76
+ ?? (f.k === 'fn' && f.ret.k === 'bool' ? ret(l) : err(`${name}: expected a predicate`)),
77
+ } satisfies Signature;
78
+ }
79
+
80
+ const CONFIG_CONTEXTS: Context[] = ['init', 'sensor', 'hook', 'beforeMove', 'task', 'def'];
81
+
82
+ export const BUILTINS: Record<string, Signature> = {
83
+ // Logic and comparison
84
+ not: { arity: 1, ret: bools('not') },
85
+ and: { ret: bools('and') },
86
+ or: { ret: bools('or') },
87
+ implies: { arity: 2, ret: bools('implies') },
88
+ eq: { arity: 2, ret: same('eq') },
89
+ ne: { arity: 2, ret: same('ne') },
90
+ lt: { arity: 2, ret: ordered('lt') },
91
+ le: { arity: 2, ret: ordered('le') },
92
+ gt: { arity: 2, ret: ordered('gt') },
93
+ ge: { arity: 2, ret: ordered('ge') },
94
+
95
+ // Arithmetic, following Lean: subtraction and division on Nat are truncated.
96
+ add: { arity: 2, ret: numeric('add') },
97
+ sub: { arity: 2, ret: numeric('sub') },
98
+ mul: { arity: 2, ret: numeric('mul') },
99
+ div: { arity: 2, ret: numeric('div') },
100
+ mod: { arity: 2, ret: numeric('mod') },
101
+ min: { arity: 2, ret: numeric('min') },
102
+ max: { arity: 2, ret: numeric('max') },
103
+ toInt: { arity: 1, ret: ([a]) => a.k === 'nat' ? T.int : err('toInt: expected Nat') },
104
+
105
+ // Options
106
+ some: { arity: 1, ret: ([a]) => T.option(a) },
107
+ isSome: { arity: 1, ret: ([o]) => o.k === 'option' ? T.bool : err('isSome: expected an Option') },
108
+ isNone: { arity: 1, ret: ([o]) => o.k === 'option' ? T.bool : err('isNone: expected an Option') },
109
+ getD: {
110
+ arity: 2,
111
+ ret: ([o, d]) => o.k === 'option' && typeEq(o.of, d) ? d : err('getD: expected an Option and a default of its type'),
112
+ },
113
+
114
+ // Collections (List and Multiset)
115
+ length: { arity: 1, ret: ([l]) => collection('length', l) ?? T.nat },
116
+ isEmpty: { arity: 1, ret: ([l]) => collection('isEmpty', l) ?? T.bool },
117
+ filter: predicate('filter', l => l),
118
+ any: predicate('any', () => T.bool),
119
+ all: predicate('all', () => T.bool),
120
+ count: predicate('count', () => T.nat),
121
+ map: {
122
+ arity: 2,
123
+ lambdas: { 1: elem('map') },
124
+ ret: ([l, f]) => collection('map', l) ?? (f.k === 'fn' ? { k: (l as { k: 'list' | 'multiset' }).k, of: f.ret } as Type : err('map: expected a function')),
125
+ },
126
+ sum: { arity: 1, ret: ([l]) => isCollection(l) && l.of.k === 'nat' ? T.nat : err('sum: expected a collection of Nat') },
127
+ mem: { arity: 2, ret: ([x, l]) => collection('mem', l) ?? (typeEq(x, (l as { of: Type }).of) ? T.bool : err('mem: element type differs')) },
128
+ range: { arity: 1, ret: ([n]) => n.k === 'nat' ? T.list(T.nat) : err('range: expected Nat') },
129
+ toMultiset: { arity: 1, ret: ([l]) => l.k === 'list' ? T.multiset(l.of) : err('toMultiset: expected a List') },
130
+ /** The elements in canonical order; not for agents, which have no order. */
131
+ toList: { arity: 1, ret: ([l]) => l.k !== 'multiset' ? err('toList: expected a Multiset') : mentionsAgent(l.of) ? err('toList: anonymous agents have no order') : T.list(l.of) },
132
+ /** All sub-collections, smallest first. */
133
+ subsets: { arity: 1, ret: ([l]) => collection('subsets', l) ?? T.list(l) },
134
+ /** Sub-collections with at most `k` elements. */
135
+ subsetsUpTo: {
136
+ arity: 2,
137
+ ret: ([l, k]) => collection('subsetsUpTo', l) ?? (k.k === 'nat' ? T.list(l) : err('subsetsUpTo: expected a Nat bound')),
138
+ },
139
+ /** Removes repeated elements. */
140
+ dedup: { arity: 1, ret: ([l]) => collection('dedup', l) ?? l },
141
+ /** Multiset difference. */
142
+ diff: { arity: 2, ret: ([a, b]) => a.k === 'multiset' && typeEq(a, b) ? a : err('diff: expected two Multisets of the same type') },
143
+ /** `[a, b, c]` (an empty list is a literal). */
144
+ list: { ret: args => args.every(a => typeEq(a, args[0])) ? T.list(args[0]) : err('list: elements of different types') },
145
+ at: { arity: 2, ret: ([l, i]) => l.k === 'list' && i.k === 'nat' ? l.of : err('at: expected a List and a Nat') },
146
+ append: { arity: 2, ret: ([a, b]) => isCollection(a) && typeEq(a, b) ? a : err('append: expected two collections of the same type') },
147
+ ofFn: {
148
+ arity: 2,
149
+ lambdas: { 1: () => [T.nat] },
150
+ ret: ([n, f]) => n.k === 'nat' && f.k === 'fn' ? T.list(f.ret) : err('ofFn: expected a Nat and a function'),
151
+ },
152
+
153
+ // Labels
154
+ show: { arity: 1, ret: () => T.string },
155
+ concat: { ret: args => args.every(a => a.k === 'string') ? T.string : err('concat: expected strings') },
156
+
157
+ // Configurations and agents
158
+ /** A multiset: anonymous agents have no order. */
159
+ agents: { arity: 1, config: true, only: CONFIG_CONTEXTS, ret: ([c]) => c.k === 'config' ? T.multiset(T.agent) : err('agents: expected a Config') },
160
+ nodes: { arity: 1, config: true, only: CONFIG_CONTEXTS, ret: ([c]) => c.k === 'config' ? T.list(T.node) : err('nodes: expected a Config') },
161
+ size: { arity: 1, config: true, only: CONFIG_CONTEXTS, ret: ([c]) => c.k === 'config' ? T.nat : err('size: expected a Config') },
162
+ /** No flag is raised. */
163
+ correct: { arity: 1, config: true, ret: ([a]) => a.k === 'agent' ? T.bool : err('correct: expected an Agent') },
164
+ /** No hidden flag is raised. */
165
+ present: { arity: 1, config: true, ret: ([a]) => a.k === 'agent' ? T.bool : err('present: expected an Agent') },
166
+ /** The decision of an agent this round; `decided` is false for agents that did not decide (then `move` is `stay`). */
167
+ decisionOf: { arity: 1, config: true, only: ['beforeMove'], ret: ([a]) => a.k === 'agent' ? T.decision : err('decisionOf: expected an Agent') },
168
+
169
+ // Topology
170
+ /** The neighbour in a global direction (whether or not the edge is present). */
171
+ target: { arity: 2, config: true, ret: ([n, m]) => n.k === 'node' && m.k === 'move' ? T.node : err('target: expected a Node and a Move') },
172
+ /** The node reached from a node after `i` steps in a global direction (edges present or not). */
173
+ advance: {
174
+ arity: 3, config: true,
175
+ ret: ([n, m, i]) => n.k === 'node' && m.k === 'move' && i.k === 'nat' ? T.node : err('advance: expected a Node, a Move and a Nat'),
176
+ },
177
+ /** Whether the edge from a node in a global direction is present in this round. */
178
+ edgePresent: { arity: 2, config: true, ret: ([n, m]) => n.k === 'node' && m.k === 'move' ? T.bool : err('edgePresent: expected a Node and a Move') },
179
+ /** The node seen at local offset `i` from a node, in a frame. */
180
+ offset: {
181
+ arity: 3, config: true,
182
+ ret: ([n, f, i]) => n.k === 'node' && f.k === 'frame' && i.k === 'nat' ? T.node : err('offset: expected a Node, a Frame and a Nat'),
183
+ },
184
+ /** A local move, mapped to a global one through a frame. */
185
+ toGlobal: { arity: 2, ret: ([f, m]) => f.k === 'frame' && m.k === 'move' ? T.move : err('toGlobal: expected a Frame and a Move') },
186
+ opposite: { arity: 1, ret: ([m]) => m.k === 'move' ? T.move : err('opposite: expected a Move') },
187
+ /** Every frame of the topology. */
188
+ frames: { arity: 0, ret: () => T.list(T.frame) },
189
+ /** Node number `n` (modulo the size). Only in initialisation. */
190
+ node: { arity: 1, config: true, absolute: true, only: ['init', 'domain'], ret: ([n]) => n.k === 'nat' ? T.node : err('node: expected a Nat') },
191
+ };