@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
package/src/ir.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
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
|
+
|
|
10
|
+
export const IR_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Types
|
|
14
|
+
|
|
15
|
+
export type Type =
|
|
16
|
+
| { k: 'unit' }
|
|
17
|
+
| { k: 'bool' }
|
|
18
|
+
| { k: 'nat' }
|
|
19
|
+
| { k: 'int' }
|
|
20
|
+
| { k: 'string' } // labels only
|
|
21
|
+
| { k: 'node' } // a node of the topology
|
|
22
|
+
| { k: 'move' } // stay | cw | ccw
|
|
23
|
+
| { k: 'frame' } // an agent's frame (on a ring: mirrored or not)
|
|
24
|
+
| { k: 'agent' } // an agent of the configuration in scope
|
|
25
|
+
| { k: 'config' } // the configuration in scope
|
|
26
|
+
| { k: 'effect' } // what a hook returns
|
|
27
|
+
| { k: 'decision' } // a decision, as seen by `beforeMove` hooks
|
|
28
|
+
| { k: 'verdict' } // success | failure | inProgress
|
|
29
|
+
| { k: 'pub' } // the algorithm's public state, in library pieces
|
|
30
|
+
| { k: 'infer' } // a literal's type before checking (front end only)
|
|
31
|
+
| { k: 'enum'; name: string; ctors: string[] }
|
|
32
|
+
| { k: 'option'; of: Type }
|
|
33
|
+
| { k: 'record'; fields: Field[] }
|
|
34
|
+
| { k: 'list'; of: Type }
|
|
35
|
+
| { k: 'multiset'; of: Type } // unordered: no order of anonymous agents leaks
|
|
36
|
+
| { k: 'fn'; params: Type[]; ret: Type }; // only as arguments of builtins
|
|
37
|
+
|
|
38
|
+
export type Field = { name: string; type: Type };
|
|
39
|
+
|
|
40
|
+
/** JSON encoding of literal values: numbers, booleans, strings (enum
|
|
41
|
+
* constructors, moves), `null` (unit, `none`), arrays, and objects (records). */
|
|
42
|
+
export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Expressions
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `at` (display only, like `Algorithm.source`): the source line of the expression, set on the
|
|
49
|
+
* algorithm's step and definitions of `.sim` programs, for the explanation of a step. It has no
|
|
50
|
+
* meaning for the semantics and is ignored by the checker and the Lean generator.
|
|
51
|
+
*/
|
|
52
|
+
export type Expr = ExprNode & { at?: number };
|
|
53
|
+
|
|
54
|
+
type ExprNode =
|
|
55
|
+
| { e: 'lit'; type: Type; value: Json }
|
|
56
|
+
| { e: 'var'; name: string }
|
|
57
|
+
| { e: 'let'; name: string; value: Expr; body: Expr }
|
|
58
|
+
| { e: 'if'; cond: Expr; then: Expr; else: Expr }
|
|
59
|
+
/** Case analysis on an enum, a move or a verdict. */
|
|
60
|
+
| { e: 'match'; on: Expr; cases: Record<string, Expr>; else?: Expr }
|
|
61
|
+
/** A lambda, only as an argument of a builtin; parameter types are inferred from it. */
|
|
62
|
+
| { e: 'fn'; params: { name: string; type?: Type }[]; body: Expr }
|
|
63
|
+
/** A builtin (see builtins.ts) or a definition of the model. */
|
|
64
|
+
| { e: 'call'; fn: string; args: Expr[] }
|
|
65
|
+
| { e: 'record'; fields: Record<string, Expr> }
|
|
66
|
+
| { e: 'with'; on: Expr; fields: Record<string, Expr> }
|
|
67
|
+
| { e: 'get'; on: Expr; field: string }
|
|
68
|
+
/**
|
|
69
|
+
* An adversary choice among the elements of `from` (a list or a multiset),
|
|
70
|
+
* restricted to those satisfying `such`. `site` names the choice in
|
|
71
|
+
* witnesses; `label` describes the chosen element.
|
|
72
|
+
*/
|
|
73
|
+
| { e: 'choose'; site: string; from: Expr; such?: Expr; label?: Expr }
|
|
74
|
+
/** A value supplied by the experiment. Only allowed in initialisation. */
|
|
75
|
+
| { e: 'param'; name: string }
|
|
76
|
+
/** What a hook does: new values of its component's world fields, and flags to raise. */
|
|
77
|
+
| { e: 'effect'; world?: Record<string, Expr>; raise?: Record<string, Expr> };
|
|
78
|
+
|
|
79
|
+
/** A named function. Definitions are not recursive. */
|
|
80
|
+
export type Def = { name: string; params: Field[]; ret: Type; body: Expr };
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Model pieces
|
|
84
|
+
|
|
85
|
+
export type Flag = {
|
|
86
|
+
name: string;
|
|
87
|
+
/** A hidden agent disappears from every sensor. */
|
|
88
|
+
visibility: 'visible' | 'hidden';
|
|
89
|
+
/** `never`: no longer decides or moves; `adversary`: the adversary decides for it. */
|
|
90
|
+
acts: 'never' | 'adversary';
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A sensor: a function of the configuration `C`, the observing agent `me`
|
|
95
|
+
* and its frame `frame`. The sensors of a model are exactly what its
|
|
96
|
+
* algorithms can know.
|
|
97
|
+
*/
|
|
98
|
+
export type Sensor = { name: string; type: Type; body: Expr; equivariance?: Equivariance };
|
|
99
|
+
|
|
100
|
+
/** Which automorphisms of the topology a piece commutes with (Section 3.11). */
|
|
101
|
+
export type Equivariance = { rotations: boolean; reflections: boolean };
|
|
102
|
+
|
|
103
|
+
/** An action other than a move: a field of every decision, with a default. */
|
|
104
|
+
export type Action = { name: string; type: Type; default: Json };
|
|
105
|
+
|
|
106
|
+
export type Component = {
|
|
107
|
+
name: string;
|
|
108
|
+
/** Parameters of the component family (`BlackHole policy`), in scope in all its expressions. */
|
|
109
|
+
params?: Field[];
|
|
110
|
+
/** Their values in this instance. */
|
|
111
|
+
args?: Record<string, Json>;
|
|
112
|
+
/** World fields owned by the component. */
|
|
113
|
+
world?: Field[];
|
|
114
|
+
/** Agent fields owned by the component. */
|
|
115
|
+
agent?: Field[];
|
|
116
|
+
flags?: Flag[];
|
|
117
|
+
sensors?: Sensor[];
|
|
118
|
+
actions?: Action[];
|
|
119
|
+
/** Initial values: world fields may use `param`; agent fields may use the agent index `i`. */
|
|
120
|
+
init?: { world?: Record<string, Expr>; agent?: Record<string, Expr> };
|
|
121
|
+
/** Hook bodies: `C` is in scope; they return an effect. */
|
|
122
|
+
beforeLook?: Expr;
|
|
123
|
+
/** Also sees the decisions, through `decisionOf(a)`. */
|
|
124
|
+
beforeMove?: Expr;
|
|
125
|
+
afterMove?: Expr;
|
|
126
|
+
defs?: Def[];
|
|
127
|
+
equivariance?: Equivariance;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Topologies are part of the semantic core; the IR names one and its parameters. */
|
|
131
|
+
export type Topology =
|
|
132
|
+
| { kind: 'ring'; size: Expr }
|
|
133
|
+
/** A ring where the adversary removes up to `k` edges at the start of every round. */
|
|
134
|
+
| { kind: 'dynamicRing'; size: Expr; k: number };
|
|
135
|
+
|
|
136
|
+
export type FrameModel =
|
|
137
|
+
| { kind: 'common' }
|
|
138
|
+
| { kind: 'dynamicReflection' }
|
|
139
|
+
/** The frame is stored in an agent field of type `frame`, owned by a component. */
|
|
140
|
+
| { kind: 'persistent'; field: string };
|
|
141
|
+
|
|
142
|
+
export type Model = {
|
|
143
|
+
name: string;
|
|
144
|
+
topology: Topology;
|
|
145
|
+
sensors: Sensor[];
|
|
146
|
+
memory: 'persistent' | 'oblivious';
|
|
147
|
+
frame: FrameModel;
|
|
148
|
+
/** `ssync`: a nonempty eligible subset; `sequential`: one eligible agent. Both require scheduler fairness. */
|
|
149
|
+
round: 'fsync' | 'ssync' | 'sequential';
|
|
150
|
+
movement: 'rigid';
|
|
151
|
+
components: Component[];
|
|
152
|
+
symmetries: 'derived' | 'none';
|
|
153
|
+
defs?: Def[];
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Algorithms, tasks, experiments
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* An algorithm as an explicit state machine. `step` sees `mem`, `pub` and
|
|
161
|
+
* `view` (a record with one field per sensor in `uses`), and returns a
|
|
162
|
+
* record `{ move, pub, mem, ...actions }`. Moves are local to the agent's frame.
|
|
163
|
+
*/
|
|
164
|
+
export type Algorithm = {
|
|
165
|
+
name: string;
|
|
166
|
+
uses: string[];
|
|
167
|
+
pub: Type;
|
|
168
|
+
mem: Type;
|
|
169
|
+
/** Initial public state and memory; the agent index `i` is in scope. */
|
|
170
|
+
init: { pub: Expr; mem: Expr };
|
|
171
|
+
step: Expr;
|
|
172
|
+
/** The line to highlight, from the memory. */
|
|
173
|
+
line?: Expr;
|
|
174
|
+
/** Interpreter-only escape hatch: a TypeScript step function registered under this name. */
|
|
175
|
+
native?: string;
|
|
176
|
+
/**
|
|
177
|
+
* Where the algorithm is written, for display (the line table refers to this file), and, for
|
|
178
|
+
* each program-counter value inside procedure calls, the lines of those calls (outermost first).
|
|
179
|
+
*/
|
|
180
|
+
source?: {
|
|
181
|
+
file: string; line: number; endLine: number; calls?: Record<string, number[]>;
|
|
182
|
+
/** Where the definitions of `defs` are written. */
|
|
183
|
+
defs?: { name: string; file: string; line: number; endLine: number }[];
|
|
184
|
+
};
|
|
185
|
+
defs?: Def[];
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export type Goal =
|
|
189
|
+
/** No reachable `failure` verdict, and every execution reaches `success`. */
|
|
190
|
+
| 'eventually'
|
|
191
|
+
/** No reachable `failure` verdict, and every execution ends in `success` forever. */
|
|
192
|
+
| 'eventuallyAlways'
|
|
193
|
+
/** Only: no reachable `failure` verdict. */
|
|
194
|
+
| 'safety';
|
|
195
|
+
|
|
196
|
+
/** A verdict on configurations (`C` in scope) and a goal over executions. */
|
|
197
|
+
export type Task = { name: string; verdict: Expr; goal: Goal; equivariance?: Equivariance; defs?: Def[] };
|
|
198
|
+
|
|
199
|
+
/** A parameter, with the list of its values for `check-all` (earlier parameters in scope). */
|
|
200
|
+
export type Param = { name: string; type: Type; domain?: Expr };
|
|
201
|
+
|
|
202
|
+
/** Where a declaration is written (display only). */
|
|
203
|
+
export type DeclSource = {
|
|
204
|
+
kind: 'experiment' | 'model' | 'component' | 'sensor' | 'algorithm' | 'task' | 'goal' | 'definition';
|
|
205
|
+
name: string; file: string; line: number; endLine: number;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export type Experiment = {
|
|
209
|
+
irVersion: number;
|
|
210
|
+
name: string;
|
|
211
|
+
model: Model;
|
|
212
|
+
algorithm: Algorithm;
|
|
213
|
+
task: Task;
|
|
214
|
+
params: Param[];
|
|
215
|
+
/** Agents: their number and positions (index `i` in scope); parameters in scope. */
|
|
216
|
+
agents: { count: Expr; pos: Expr };
|
|
217
|
+
limits?: { depth?: number; states?: number };
|
|
218
|
+
/**
|
|
219
|
+
* Display only, like `Algorithm.source`: where the experiment is written, and every declaration
|
|
220
|
+
* it is built from (the experiment first, then in order of use), for the page's model view.
|
|
221
|
+
*/
|
|
222
|
+
source?: { file: string; line: number; endLine: number; uses: DeclSource[] };
|
|
223
|
+
};
|
package/src/layout.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composed state of an experiment: which fields, flags, sensors and
|
|
3
|
+
* actions exist, and which component owns each of them.
|
|
4
|
+
*/
|
|
5
|
+
import type { Action, Def, Experiment, Field, Flag, Sensor, Type } from './ir';
|
|
6
|
+
import { resolvePub, T } from './types';
|
|
7
|
+
|
|
8
|
+
export type Owned<X> = X & { owner: string };
|
|
9
|
+
|
|
10
|
+
export type Layout = {
|
|
11
|
+
world: Owned<Field>[];
|
|
12
|
+
agentFields: Owned<Field>[];
|
|
13
|
+
flags: Owned<Flag>[];
|
|
14
|
+
sensors: Owned<Sensor>[];
|
|
15
|
+
actions: Owned<Action>[];
|
|
16
|
+
worldType: Type;
|
|
17
|
+
pub: Type;
|
|
18
|
+
mem: Type;
|
|
19
|
+
/** What `step` receives as `view`: one field per sensor the algorithm uses. */
|
|
20
|
+
view: Type;
|
|
21
|
+
/** What `step` returns. */
|
|
22
|
+
decision: Type;
|
|
23
|
+
/** Definitions visible from every piece: the model's. */
|
|
24
|
+
defs: Def[];
|
|
25
|
+
errors: string[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const MODEL = 'model';
|
|
29
|
+
|
|
30
|
+
export function layout(exp: Experiment): Layout {
|
|
31
|
+
const errors: string[] = [];
|
|
32
|
+
const unique = <X extends { name: string }>(what: string, items: Owned<X>[]) => {
|
|
33
|
+
const seen = new Map<string, string>();
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
const other = seen.get(item.name);
|
|
36
|
+
if (other !== undefined) errors.push(`${what} '${item.name}' is declared by both ${other} and ${item.owner}`);
|
|
37
|
+
seen.set(item.name, item.owner);
|
|
38
|
+
}
|
|
39
|
+
return items;
|
|
40
|
+
};
|
|
41
|
+
const owned = <X>(owner: string, items: X[] | undefined) => (items ?? []).map(x => ({ ...x, owner }));
|
|
42
|
+
const { model, algorithm } = exp;
|
|
43
|
+
const world = unique('world field', model.components.flatMap(c => owned(c.name, c.world)));
|
|
44
|
+
const agentFields = unique('agent field', model.components.flatMap(c => owned(c.name, c.agent)));
|
|
45
|
+
const flags = unique('flag', model.components.flatMap(c => owned(c.name, c.flags)));
|
|
46
|
+
const sensors = unique('sensor', [...owned(MODEL, model.sensors), ...model.components.flatMap(c => owned(c.name, c.sensors))])
|
|
47
|
+
.map(s => ({ ...s, type: resolvePub(s.type, algorithm.pub) }));
|
|
48
|
+
const actions = unique('action', model.components.flatMap(c => owned(c.name, c.actions)));
|
|
49
|
+
const reserved = new Set(['pos', 'pub', 'mem']);
|
|
50
|
+
for (const f of [...agentFields, ...flags]) {
|
|
51
|
+
if (reserved.has(f.name)) errors.push(`agent field '${f.name}' of ${f.owner} is reserved`);
|
|
52
|
+
}
|
|
53
|
+
for (const f of agentFields) {
|
|
54
|
+
if (flags.some(g => g.name === f.name)) errors.push(`'${f.name}' is both an agent field and a flag`);
|
|
55
|
+
}
|
|
56
|
+
for (const a of actions) {
|
|
57
|
+
if (['move', 'pub', 'mem'].includes(a.name)) errors.push(`action '${a.name}' of ${a.owner} is reserved`);
|
|
58
|
+
}
|
|
59
|
+
const view: Field[] = [];
|
|
60
|
+
for (const name of algorithm.uses) {
|
|
61
|
+
const sensor = sensors.find(s => s.name === name);
|
|
62
|
+
if (sensor) view.push({ name, type: sensor.type });
|
|
63
|
+
else errors.push(`algorithm ${algorithm.name} uses sensor '${name}', which the model does not provide`);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
world, agentFields, flags, sensors, actions, errors,
|
|
67
|
+
worldType: T.record(world.map(({ name, type }) => ({ name, type }))),
|
|
68
|
+
pub: algorithm.pub,
|
|
69
|
+
mem: algorithm.mem,
|
|
70
|
+
view: T.record(view),
|
|
71
|
+
decision: T.record([
|
|
72
|
+
{ name: 'move', type: T.move }, { name: 'pub', type: algorithm.pub }, { name: 'mem', type: algorithm.mem },
|
|
73
|
+
...actions.map(({ name, type }) => ({ name, type })),
|
|
74
|
+
]),
|
|
75
|
+
defs: model.defs ?? [],
|
|
76
|
+
};
|
|
77
|
+
}
|
package/src/parse/ast.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** The surface syntax of `.sim` files, with source spans. */
|
|
2
|
+
import type { Span } from './lexer';
|
|
3
|
+
|
|
4
|
+
export type SType =
|
|
5
|
+
| { t: 'name'; name: string; args: SType[]; span: Span }
|
|
6
|
+
| { t: 'record'; fields: SFieldType[]; span: Span };
|
|
7
|
+
|
|
8
|
+
export type SFieldType = { name: string; type: SType; default?: SExpr; span: Span };
|
|
9
|
+
|
|
10
|
+
export type SArg = { name?: string; value: SExpr };
|
|
11
|
+
|
|
12
|
+
export type SPat =
|
|
13
|
+
| { t: 'ctor'; name: string; span: Span } // .cw or Move.cw
|
|
14
|
+
| { t: 'wild'; span: Span }
|
|
15
|
+
| { t: 'none'; span: Span }
|
|
16
|
+
| { t: 'some'; name: string; span: Span }
|
|
17
|
+
| { t: 'bool'; value: boolean; span: Span };
|
|
18
|
+
|
|
19
|
+
export type SRecordItem =
|
|
20
|
+
| { t: 'field'; name: string; value?: SExpr; span: Span } // `f := e`, or `f` (punning)
|
|
21
|
+
| { t: 'raise'; flag: string; value: SExpr; span: Span }; // `raise dead := S`, in effects
|
|
22
|
+
|
|
23
|
+
export type SExpr = { span: Span } & (
|
|
24
|
+
| { t: 'num'; value: number }
|
|
25
|
+
| { t: 'str'; value: string }
|
|
26
|
+
| { t: 'bool'; value: boolean }
|
|
27
|
+
| { t: 'name'; name: string } // possibly dotted: C.world.site
|
|
28
|
+
| { t: 'ctor'; name: string } // .cw
|
|
29
|
+
| { t: 'proj'; on: SExpr; field: string } // (e).f
|
|
30
|
+
| { t: 'app'; fn: SExpr; args: SArg[] }
|
|
31
|
+
| { t: 'binop'; op: string; l: SExpr; r: SExpr }
|
|
32
|
+
| { t: 'unop'; op: string; e: SExpr }
|
|
33
|
+
| { t: 'if'; cond: SExpr; then: SExpr; else: SExpr }
|
|
34
|
+
| { t: 'let'; name: string; type?: SType; value: SExpr; body: SExpr }
|
|
35
|
+
| { t: 'match'; on: SExpr; cases: { pat: SPat; body: SExpr }[] }
|
|
36
|
+
| { t: 'fun'; params: { name: string; type?: SType }[]; body: SExpr }
|
|
37
|
+
| { t: 'list'; items: SExpr[] }
|
|
38
|
+
| { t: 'record'; base?: SExpr; items: SRecordItem[] }
|
|
39
|
+
| { t: 'choose'; from: SExpr; such?: SExpr; labels?: SExpr }
|
|
40
|
+
| { t: 'param'; name: string }
|
|
41
|
+
| { t: 'ascribe'; e: SExpr; type: SType }
|
|
42
|
+
| { t: 'nothing' }
|
|
43
|
+
| { t: 'unit' } // ()
|
|
44
|
+
| { t: 'hole' } // `·` in a section, before desugaring
|
|
45
|
+
| { t: 'flag'; name: string; visibility: 'visible' | 'hidden'; acts: 'never' | 'adversary' }
|
|
46
|
+
| { t: 'action'; name: string; type: SType; default: SExpr }
|
|
47
|
+
| { t: 'typeValue'; type: SType } // a type in a `where` block: `World := { site : Node }`
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export type SBinder = { name: string; type: SType; default?: SExpr; span: Span };
|
|
51
|
+
|
|
52
|
+
/** Statements of structured algorithms (plan 3.8). `move`, `stay` and `halt` end the activation. */
|
|
53
|
+
export type SStmt = { span: Span } & (
|
|
54
|
+
| { s: 'move'; dir: SExpr; actions?: SRecordItem[] } // `move d with { pebble := .drop }`
|
|
55
|
+
| { s: 'stay'; actions?: SRecordItem[] }
|
|
56
|
+
| { s: 'halt'; actions?: SRecordItem[] }
|
|
57
|
+
| { s: 'wait'; count: SExpr } // `wait n`: stay n activations
|
|
58
|
+
| { s: 'repeat'; body: SStmt[] }
|
|
59
|
+
| { s: 'while'; cond: SExpr; body: SStmt[] }
|
|
60
|
+
| { s: 'if'; cond: SExpr; then: SStmt[]; else?: SStmt[] }
|
|
61
|
+
| { s: 'let'; name: string; type?: SType; value: SExpr }
|
|
62
|
+
| { s: 'assign'; name: string; value: SExpr }
|
|
63
|
+
| { s: 'call'; name: string; args: SExpr[] }
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
/** An item of a `where` block: `name args := value`, or `param N : T := domain` in experiments. */
|
|
67
|
+
export type SWhereItem = {
|
|
68
|
+
name: string; args: string[]; value: SExpr; span: Span;
|
|
69
|
+
/** `param N : T := domain`, in experiments. */
|
|
70
|
+
param?: { type: SType };
|
|
71
|
+
/** `def p (x : T) := statements` and `main := statements`, in algorithms. */
|
|
72
|
+
proc?: { params: SBinder[]; body: SStmt[] };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type SAttribute = { name: string; args: string[] };
|
|
76
|
+
|
|
77
|
+
export type SDecl = { span: Span; doc?: string; attrs: SAttribute[] } & (
|
|
78
|
+
| { d: 'import'; path: string }
|
|
79
|
+
| { d: 'inductive'; name: string; ctors: string[] }
|
|
80
|
+
| { d: 'structure'; name: string; fields: SFieldType[] }
|
|
81
|
+
| { d: 'def'; name: string; params: SBinder[]; type?: SType; body?: SExpr; items?: SWhereItem[] }
|
|
82
|
+
| { d: 'sensor'; name: string; params: SBinder[]; type: SType; body: SExpr }
|
|
83
|
+
| { d: 'task'; name: string; params: SBinder[]; type: SType; body: SExpr }
|
|
84
|
+
| { d: 'goal'; name: string; goal: 'eventually' | 'eventuallyAlways' | 'safety' }
|
|
85
|
+
| { d: 'algorithm'; name: string; uses: string[]; items: SWhereItem[] }
|
|
86
|
+
| { d: 'experiment'; name: string; items: SWhereItem[] }
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
export type SFile = { file: string; decls: SDecl[] };
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
|
|
6
|
+
export type Pos = { line: number; col: number; offset: number };
|
|
7
|
+
export type Span = { file: string; start: Pos; end: Pos };
|
|
8
|
+
|
|
9
|
+
export type TokenKind =
|
|
10
|
+
| 'ident' // name, possibly qualified: Hazards.BlackHole
|
|
11
|
+
| 'dotIdent' // .cw, a constructor whose type is inferred
|
|
12
|
+
| 'field' // .pos right after an expression: a projection
|
|
13
|
+
| 'number'
|
|
14
|
+
| 'string'
|
|
15
|
+
| 'op' // punctuation and operators
|
|
16
|
+
| 'keyword'
|
|
17
|
+
| 'eof';
|
|
18
|
+
|
|
19
|
+
export type Token = {
|
|
20
|
+
kind: TokenKind;
|
|
21
|
+
text: string;
|
|
22
|
+
start: Pos;
|
|
23
|
+
end: Pos;
|
|
24
|
+
/** First token of its line. */
|
|
25
|
+
lineStart: boolean;
|
|
26
|
+
/** Preceded by whitespace or a comment. */
|
|
27
|
+
spaced: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const KEYWORDS = new Set([
|
|
31
|
+
'import', 'def', 'structure', 'inductive', 'sensor', 'task', 'goal', 'algorithm', 'experiment', 'where', 'let',
|
|
32
|
+
'if', 'then', 'else', 'match', 'with', 'fun', 'choose', 'such', 'that', 'labels', 'param', 'flag', 'uses',
|
|
33
|
+
'true', 'false', 'raise', 'nothing', 'action',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/** Operators, longest first. Unicode and ASCII spellings are both accepted. */
|
|
37
|
+
const OPERATORS = [
|
|
38
|
+
':=', '=>', '->', '<-', '++', '==', '!=', '<=', '>=', '&&', '||',
|
|
39
|
+
'→', '←', '≠', '≤', '≥', '∧', '∨', '¬', '·', '×',
|
|
40
|
+
'=', '<', '>', '+', '-', '*', '/', '%', '!', '|', ',', ':', ';', '(', ')', '[', ']', '{', '}', '@',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const NORMALIZE: Record<string, string> = {
|
|
44
|
+
'->': '→', '<-': '←', '!=': '≠', '<=': '≤', '>=': '≥', '&&': '∧', '||': '∨', '==': '=',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export class LexError extends Error {
|
|
48
|
+
constructor(message: string, readonly span: Span) {
|
|
49
|
+
super(message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const identStart = /[A-Za-z_À-ɏͰ-Ͽ]/;
|
|
54
|
+
const identPart = /[A-Za-z0-9_'À-ɏͰ-Ͽ₀-₉]/;
|
|
55
|
+
|
|
56
|
+
export function lex(source: string, file = '<input>'): Token[] {
|
|
57
|
+
const tokens: Token[] = [];
|
|
58
|
+
let i = 0, line = 1, col = 1;
|
|
59
|
+
let lineStart = true, spaced = true;
|
|
60
|
+
const pos = (): Pos => ({ line, col, offset: i });
|
|
61
|
+
const advance = (n = 1) => {
|
|
62
|
+
for (let j = 0; j < n; j++) {
|
|
63
|
+
if (source[i] === '\n') { line++; col = 1; } else col++;
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const fail = (msg: string, start: Pos): never => { throw new LexError(msg, { file, start, end: pos() }); };
|
|
68
|
+
|
|
69
|
+
while (i < source.length) {
|
|
70
|
+
const c = source[i];
|
|
71
|
+
if (c === '\n') { advance(); lineStart = true; spaced = true; continue; }
|
|
72
|
+
if (c === ' ' || c === '\t' || c === '\r') { advance(); spaced = true; continue; }
|
|
73
|
+
if (source.startsWith('--', i)) {
|
|
74
|
+
while (i < source.length && source[i] !== '\n') advance();
|
|
75
|
+
spaced = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (source.startsWith('/-', i)) {
|
|
79
|
+
const start = pos();
|
|
80
|
+
let depth = 0;
|
|
81
|
+
do {
|
|
82
|
+
if (source.startsWith('/-', i)) { depth++; advance(2); }
|
|
83
|
+
else if (source.startsWith('-/', i)) { depth--; advance(2); }
|
|
84
|
+
else if (i >= source.length) fail('unterminated comment', start);
|
|
85
|
+
else advance();
|
|
86
|
+
} while (depth > 0);
|
|
87
|
+
spaced = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const start = pos();
|
|
91
|
+
const push = (kind: TokenKind, text: string) => {
|
|
92
|
+
tokens.push({ kind, text, start, end: pos(), lineStart, spaced });
|
|
93
|
+
lineStart = false;
|
|
94
|
+
spaced = false;
|
|
95
|
+
};
|
|
96
|
+
const prev = tokens[tokens.length - 1];
|
|
97
|
+
const afterTerm = !!prev && !spaced && (prev.kind === 'ident' || prev.kind === 'field' || prev.kind === 'dotIdent'
|
|
98
|
+
|| prev.kind === 'number' || prev.kind === 'string' || [')', ']', '}'].includes(prev.text) || prev.text === '·');
|
|
99
|
+
if (c === '.' && identStart.test(source[i + 1] ?? '')) {
|
|
100
|
+
// `.x` right after a term is a projection; otherwise a constructor.
|
|
101
|
+
advance();
|
|
102
|
+
const s = i;
|
|
103
|
+
while (i < source.length && identPart.test(source[i])) advance();
|
|
104
|
+
push(afterTerm ? 'field' : 'dotIdent', source.slice(s, i));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (identStart.test(c)) {
|
|
108
|
+
const s = i;
|
|
109
|
+
while (i < source.length && identPart.test(source[i])) advance();
|
|
110
|
+
// A dotted name, as in Lean: `Hazards.BlackHole` or `C.world.site`; name resolution
|
|
111
|
+
// decides which parts are namespaces and which are projections.
|
|
112
|
+
while (source[i] === '.' && identStart.test(source[i + 1] ?? '')) {
|
|
113
|
+
advance();
|
|
114
|
+
while (i < source.length && identPart.test(source[i])) advance();
|
|
115
|
+
}
|
|
116
|
+
const text = source.slice(s, i);
|
|
117
|
+
push(KEYWORDS.has(text) ? 'keyword' : 'ident', text);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (/[0-9]/.test(c)) {
|
|
121
|
+
const s = i;
|
|
122
|
+
while (i < source.length && /[0-9]/.test(source[i])) advance();
|
|
123
|
+
push('number', source.slice(s, i));
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (c === '"') {
|
|
127
|
+
advance();
|
|
128
|
+
let text = '';
|
|
129
|
+
while (source[i] !== '"') {
|
|
130
|
+
if (i >= source.length || source[i] === '\n') fail('unterminated string', start);
|
|
131
|
+
if (source[i] === '\\') { advance(); text += source[i] === 'n' ? '\n' : source[i]; advance(); continue; }
|
|
132
|
+
text += source[i];
|
|
133
|
+
advance();
|
|
134
|
+
}
|
|
135
|
+
advance();
|
|
136
|
+
push('string', text);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const op = OPERATORS.find(o => source.startsWith(o, i));
|
|
140
|
+
if (!op) fail(`unexpected character '${c}'`, start);
|
|
141
|
+
advance(op!.length);
|
|
142
|
+
push('op', NORMALIZE[op!] ?? op!);
|
|
143
|
+
}
|
|
144
|
+
tokens.push({ kind: 'eof', text: '', start: pos(), end: pos(), lineStart: true, spaced: true });
|
|
145
|
+
return tokens;
|
|
146
|
+
}
|