@cotal-ai/lang 0.23.0 → 0.25.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/README.md +68 -0
- package/dist/effects.d.ts +19 -0
- package/dist/effects.d.ts.map +1 -1
- package/dist/effects.js +23 -0
- package/dist/effects.js.map +1 -1
- package/dist/engine/bridge.d.ts +71 -0
- package/dist/engine/bridge.d.ts.map +1 -0
- package/dist/engine/bridge.js +277 -0
- package/dist/engine/bridge.js.map +1 -0
- package/dist/engine/ctx.d.ts +140 -0
- package/dist/engine/ctx.d.ts.map +1 -0
- package/dist/engine/ctx.js +834 -0
- package/dist/engine/ctx.js.map +1 -0
- package/dist/engine/frame.d.ts +69 -0
- package/dist/engine/frame.d.ts.map +1 -0
- package/dist/engine/frame.js +105 -0
- package/dist/engine/frame.js.map +1 -0
- package/dist/engine/host.d.ts +77 -0
- package/dist/engine/host.d.ts.map +1 -0
- package/dist/engine/host.js +134 -0
- package/dist/engine/host.js.map +1 -0
- package/dist/engine/worker-entry.d.ts +26 -0
- package/dist/engine/worker-entry.d.ts.map +1 -0
- package/dist/engine/worker-entry.js +175 -0
- package/dist/engine/worker-entry.js.map +1 -0
- package/dist/engine/worker.d.ts +156 -0
- package/dist/engine/worker.d.ts.map +1 -0
- package/dist/engine/worker.js +123 -0
- package/dist/engine/worker.js.map +1 -0
- package/dist/errors.d.ts +92 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +187 -2
- package/dist/errors.js.map +1 -1
- package/dist/grammar.d.ts.map +1 -1
- package/dist/grammar.js +561 -128
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +19 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -6
- package/dist/index.js.map +1 -1
- package/dist/interpret.d.ts +59 -12
- package/dist/interpret.d.ts.map +1 -1
- package/dist/interpret.js +831 -613
- package/dist/interpret.js.map +1 -1
- package/dist/journal.d.ts +218 -9
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +306 -12
- package/dist/journal.js.map +1 -1
- package/dist/keys.d.ts +39 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +61 -0
- package/dist/keys.js.map +1 -1
- package/dist/library.d.ts +65 -0
- package/dist/library.d.ts.map +1 -0
- package/dist/library.js +537 -0
- package/dist/library.js.map +1 -0
- package/dist/notify-fact.d.ts +8 -0
- package/dist/notify-fact.d.ts.map +1 -0
- package/dist/notify-fact.js +69 -0
- package/dist/notify-fact.js.map +1 -0
- package/dist/perform.d.ts +138 -0
- package/dist/perform.d.ts.map +1 -0
- package/dist/perform.js +1052 -0
- package/dist/perform.js.map +1 -0
- package/dist/pins.d.ts +102 -0
- package/dist/pins.d.ts.map +1 -0
- package/dist/pins.js +107 -0
- package/dist/pins.js.map +1 -0
- package/dist/primitives.d.ts +14 -0
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +45 -5
- package/dist/primitives.js.map +1 -1
- package/dist/sim.d.ts +24 -2
- package/dist/sim.d.ts.map +1 -1
- package/dist/sim.js +9 -1
- package/dist/sim.js.map +1 -1
- package/dist/syntax.d.ts +34 -0
- package/dist/syntax.d.ts.map +1 -0
- package/dist/syntax.js +178 -0
- package/dist/syntax.js.map +1 -0
- package/dist/transform/emit.d.ts +23 -0
- package/dist/transform/emit.d.ts.map +1 -0
- package/dist/transform/emit.js +934 -0
- package/dist/transform/emit.js.map +1 -0
- package/dist/transform/index.d.ts +34 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +31 -0
- package/dist/transform/index.js.map +1 -0
- package/dist/transform/scope.d.ts +58 -0
- package/dist/transform/scope.d.ts.map +1 -0
- package/dist/transform/scope.js +500 -0
- package/dist/transform/scope.js.map +1 -0
- package/dist/transform/seam.d.ts +78 -0
- package/dist/transform/seam.d.ts.map +1 -0
- package/dist/transform/seam.js +111 -0
- package/dist/transform/seam.js.map +1 -0
- package/dist/values.d.ts +35 -1
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +0 -0
- package/dist/values.js.map +1 -1
- package/package.json +9 -4
package/dist/interpret.js
CHANGED
|
@@ -15,13 +15,37 @@
|
|
|
15
15
|
* serialized.
|
|
16
16
|
*/
|
|
17
17
|
import { validate } from "./grammar.js";
|
|
18
|
-
import { LangError, LangErrors } from "./errors.js";
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
18
|
+
import { LangError, LangErrors, RunDivergence, RuntimeFault, ScopeBranchMissing, UnwalkableScope, messageOf, } from "./errors.js";
|
|
19
|
+
export { RunDivergence, RuntimeFault, ScopeBranchMissing, UnwalkableScope } from "./errors.js";
|
|
20
|
+
import { KeyScope, digest, programHashOf, requestId, scopePathString, stepKeyString } from "./keys.js";
|
|
21
|
+
import { Journal, JournalAppendRejected, RunClock } from "./journal.js";
|
|
22
|
+
import { NotCrossable, Prng, assertCrossable, birthDepth, born, deepFreeze, setOwn } from "./values.js";
|
|
22
23
|
import { parseDuration } from "./duration.js";
|
|
23
|
-
import { PRIMITIVES } from "./primitives.js";
|
|
24
|
-
import {
|
|
24
|
+
import { PRIMITIVES, VALUE_NAMES } from "./primitives.js";
|
|
25
|
+
import { arrayMethods, builtins, numberMethods, stringMethods } from "./library.js";
|
|
26
|
+
import { notifyFactViolation } from "./notify-fact.js";
|
|
27
|
+
import { bindPins, resolvePins, WALKER_LANGUAGE_VERSION } from "./pins.js";
|
|
28
|
+
import { dispatchPrimitive, freeConstructors, option, performEffect, performScope, runScope, } from "./perform.js";
|
|
29
|
+
import { Cancelled, RunReleased, EffectError, applyCheckpointPolicy, } from "./effects.js";
|
|
30
|
+
/**
|
|
31
|
+
* Every write of a RECORD member goes through here, whatever spelled it: a literal, a spread, a
|
|
32
|
+
* rest pattern, or `o.a = v`. The one member the language refuses is a callable `then`.
|
|
33
|
+
*
|
|
34
|
+
* The host's promise machinery adopts any object that carries one: resolving a promise with such a
|
|
35
|
+
* record calls the record's own `then` with the machinery's continuations instead of delivering the
|
|
36
|
+
* value. Inside this interpreter every function is async, so a program-authored `then` that throws
|
|
37
|
+
* turns that throw into a rejection of a promise nobody owns, which escapes the run as an
|
|
38
|
+
* unhandled rejection and kills the host, while the await that adopted the record never settles
|
|
39
|
+
* and the run hangs behind it (measured: a record returned from a program function did exactly
|
|
40
|
+
* this, host process and all). Refusing the member is refusing the adoption: the language carries
|
|
41
|
+
* no thenable values, the same way it carries no sparse arrays and no `__proto__` fields.
|
|
42
|
+
*/
|
|
43
|
+
function setRecordMember(target, key, value) {
|
|
44
|
+
if (key === "then" && typeof value === "function") {
|
|
45
|
+
throw new RuntimeFault("L4021", "`then` cannot name a function here. To the host's promise machinery any object with a callable `then` is a promise waiting to be adopted, so the record would never arrive as the value this program built: its `then` runs with the machinery's own continuations, a `then` that throws or rejects escapes the run as an unhandled rejection with no owner and kills the host, and the await that adopted it never settles. Name the member something else.");
|
|
46
|
+
}
|
|
47
|
+
setOwn(target, key, value);
|
|
48
|
+
}
|
|
25
49
|
// ---- environments ------------------------------------------------------------------------------
|
|
26
50
|
class Binding {
|
|
27
51
|
value;
|
|
@@ -31,15 +55,49 @@ class Binding {
|
|
|
31
55
|
this.mutable = mutable;
|
|
32
56
|
}
|
|
33
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The value a `let`/`const` binding holds between the top of its block and its declaration: the
|
|
60
|
+
* temporal dead zone, materialized. The validator refuses every straight-line reference into it
|
|
61
|
+
* (L2004), so the only way here at run time is a function called before the declaration executed —
|
|
62
|
+
* which JavaScript answers with a ReferenceError, and this language answers with the same code the
|
|
63
|
+
* static refusal carries.
|
|
64
|
+
*/
|
|
65
|
+
const TDZ = Symbol("cotal-lang temporal dead zone");
|
|
34
66
|
class Env {
|
|
35
67
|
parent;
|
|
68
|
+
depth;
|
|
36
69
|
names = new Map();
|
|
37
|
-
|
|
70
|
+
/**
|
|
71
|
+
* How many CONCURRENT scopes deep this environment was created.
|
|
72
|
+
*
|
|
73
|
+
* L2032's runtime half rests on this. The static rule follows named and inline branches, but a
|
|
74
|
+
* branch the validator cannot resolve to a function node — one that arrives through a parameter
|
|
75
|
+
* or a computed record — is not proven, and banning that shape outright would cost more than the
|
|
76
|
+
* hazard. So the depth travels with the binding: a write from inside a concurrent branch to a
|
|
77
|
+
* binding declared OUTSIDE it is refused where it happens. `conclave` does not raise the depth,
|
|
78
|
+
* because its single body has nothing to race.
|
|
79
|
+
*/
|
|
80
|
+
constructor(parent, depth = parent?.depth ?? 0) {
|
|
38
81
|
this.parent = parent;
|
|
82
|
+
this.depth = depth;
|
|
39
83
|
}
|
|
40
84
|
declare(name, value, mutable) {
|
|
41
85
|
this.names.set(name, new Binding(value, mutable));
|
|
42
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* A fresh environment holding copies of `names` at their current values: JavaScript's
|
|
89
|
+
* per-iteration bindings for a `for (let ...)` loop, so a closure made in one iteration keeps
|
|
90
|
+
* that iteration's value rather than watching the counter move.
|
|
91
|
+
*/
|
|
92
|
+
perIteration(names) {
|
|
93
|
+
const next = new Env(this.parent, this.depth);
|
|
94
|
+
for (const n of names) {
|
|
95
|
+
const b = this.names.get(n);
|
|
96
|
+
if (b !== undefined)
|
|
97
|
+
next.declare(n, b.value, b.mutable);
|
|
98
|
+
}
|
|
99
|
+
return next;
|
|
100
|
+
}
|
|
43
101
|
find(name) {
|
|
44
102
|
for (let e = this; e !== null; e = e.parent) {
|
|
45
103
|
const b = e.names.get(name);
|
|
@@ -48,56 +106,101 @@ class Env {
|
|
|
48
106
|
}
|
|
49
107
|
return undefined;
|
|
50
108
|
}
|
|
109
|
+
owner(name) {
|
|
110
|
+
for (let e = this; e !== null; e = e.parent) {
|
|
111
|
+
if (e.names.has(name))
|
|
112
|
+
return e;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
51
116
|
get(name) {
|
|
52
117
|
const b = this.find(name);
|
|
53
118
|
if (b === undefined)
|
|
54
119
|
throw new RuntimeFault("L2001", `${name} is not defined`);
|
|
120
|
+
if (b.value === TDZ) {
|
|
121
|
+
throw new RuntimeFault("L2004", `${name} is used before its declaration was reached: the binding exists for the whole block, but it holds no value until the \`let\`/\`const\` line runs. Call this function after the declaration, or move the declaration up.`);
|
|
122
|
+
}
|
|
55
123
|
return b.value;
|
|
56
124
|
}
|
|
57
125
|
has(name) {
|
|
58
126
|
return this.find(name) !== undefined;
|
|
59
127
|
}
|
|
60
|
-
set(name, value) {
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
128
|
+
set(name, value, atDepth) {
|
|
129
|
+
const owner = this.owner(name);
|
|
130
|
+
if (owner === undefined)
|
|
63
131
|
throw new RuntimeFault("L2001", `${name} is not defined`);
|
|
132
|
+
const b = owner.names.get(name);
|
|
133
|
+
if (b.value === TDZ) {
|
|
134
|
+
throw new RuntimeFault("L2004", `${name} is assigned before its declaration was reached: the binding exists for the whole block, but it holds no value until the \`let\`/\`const\` line runs.`);
|
|
135
|
+
}
|
|
64
136
|
if (!b.mutable)
|
|
65
137
|
throw new RuntimeFault("L2003", `${name} is declared const`);
|
|
138
|
+
if (owner.depth < atDepth) {
|
|
139
|
+
throw new RuntimeFault("L2032", `${name} is declared outside this concurrent branch and written inside it. Live, the branches write in completion order; on resume the recorded effects return instantly and they write in launch order, so ${name} holds a different value and the run takes a path it never recorded, with no divergence raised. Return the value from the branch and read it out of the combinator's result, or use race, which yields its winner.`);
|
|
140
|
+
}
|
|
66
141
|
b.value = value;
|
|
67
142
|
}
|
|
68
143
|
}
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
144
|
+
/**
|
|
145
|
+
* The message of an arbitrary thrown value.
|
|
146
|
+
*
|
|
147
|
+
* Reading `.message` off `null` throws, and a thrown primitive is legal in a language with `throw`,
|
|
148
|
+
* so every place that has to describe a failure it did not construct goes through here. A recorded
|
|
149
|
+
* entry saying "Cannot read properties of null" describes the recorder, not the run.
|
|
150
|
+
*/
|
|
151
|
+
/** An AST subtree with its source offsets removed: what the code IS, not where it sits. */
|
|
152
|
+
export function stripPositions(node) {
|
|
153
|
+
if (Array.isArray(node))
|
|
154
|
+
return node.map(stripPositions);
|
|
155
|
+
if (node === null || typeof node !== "object")
|
|
156
|
+
return node;
|
|
157
|
+
const out = {};
|
|
158
|
+
for (const [k, v] of Object.entries(node)) {
|
|
159
|
+
if (k === "start" || k === "end" || k === "loc" || k === "range")
|
|
160
|
+
continue;
|
|
161
|
+
out[k] = stripPositions(v);
|
|
76
162
|
}
|
|
163
|
+
return out;
|
|
77
164
|
}
|
|
78
165
|
const NORMAL = { type: "normal" };
|
|
79
166
|
// ---- per-branch execution state ---------------------------------------------------------------------
|
|
167
|
+
/**
|
|
168
|
+
* A branch's cancellation, in two degrees.
|
|
169
|
+
*
|
|
170
|
+
* `cancelled` is the cancellation LAW: a cancelled branch performs no new effect, and every effect
|
|
171
|
+
* boundary refuses it. `cutPure` is the stronger cut a scope applies to an arm that CANNOT WIN any
|
|
172
|
+
* more: its pure work is also abandoned, at the next yield. An arm that could still win keeps
|
|
173
|
+
* running its pure work to a settle, because cutting it there would let the scheduler, and through
|
|
174
|
+
* it the `yieldEvery` pin, decide a race the recorded clocks should decide (see `runScope`).
|
|
175
|
+
* Both degrees flow to child signals, and a signal already cancelled softly can be escalated.
|
|
176
|
+
*/
|
|
80
177
|
class Signal {
|
|
81
178
|
cancelled = false;
|
|
179
|
+
cutPure = false;
|
|
82
180
|
reason;
|
|
83
181
|
listeners = [];
|
|
84
182
|
onCancel(fn) {
|
|
85
183
|
this.listeners.push(fn);
|
|
86
184
|
}
|
|
87
|
-
cancel(reason) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
185
|
+
cancel(reason, opts) {
|
|
186
|
+
const cut = opts?.cutPure ?? true;
|
|
187
|
+
const first = !this.cancelled;
|
|
188
|
+
if (first) {
|
|
189
|
+
this.cancelled = true;
|
|
190
|
+
this.reason = reason;
|
|
191
|
+
}
|
|
192
|
+
const escalated = cut && !this.cutPure;
|
|
193
|
+
if (escalated)
|
|
194
|
+
this.cutPure = true;
|
|
195
|
+
if (first || escalated)
|
|
196
|
+
for (const l of this.listeners)
|
|
197
|
+
l(reason, this.cutPure);
|
|
94
198
|
}
|
|
95
199
|
child() {
|
|
96
200
|
const s = new Signal();
|
|
97
201
|
if (this.cancelled)
|
|
98
|
-
s.cancel(this.reason ?? "parent cancelled");
|
|
99
|
-
|
|
100
|
-
this.onCancel((r) => s.cancel(r));
|
|
202
|
+
s.cancel(this.reason ?? "parent cancelled", { cutPure: this.cutPure });
|
|
203
|
+
this.onCancel((r, cut) => s.cancel(r, { cutPure: cut }));
|
|
101
204
|
return s;
|
|
102
205
|
}
|
|
103
206
|
}
|
|
@@ -111,50 +214,197 @@ class Frame {
|
|
|
111
214
|
keys;
|
|
112
215
|
clock;
|
|
113
216
|
signal;
|
|
114
|
-
|
|
217
|
+
depth;
|
|
218
|
+
constructor(keys, clock, signal,
|
|
219
|
+
/** How many CONCURRENT scopes deep. See {@link Env.depth}: this is L2032's runtime half. */
|
|
220
|
+
depth = 0) {
|
|
115
221
|
this.keys = keys;
|
|
116
222
|
this.clock = clock;
|
|
117
223
|
this.signal = signal;
|
|
224
|
+
this.depth = depth;
|
|
118
225
|
}
|
|
119
226
|
branch(kind, name, occurrence, branchKey) {
|
|
120
|
-
return new Frame(this.keys.branch(kind, name, occurrence, branchKey), this.clock.fork(), this.signal.child()
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
export class RunDivergence extends Error {
|
|
125
|
-
stepKey;
|
|
126
|
-
recordedHash;
|
|
127
|
-
programHash;
|
|
128
|
-
constructor(stepKey, recordedHash, programHash) {
|
|
129
|
-
super(`L5001 Run divergence\n\n step ${stepKey} INPUT CHANGED\n recorded ${recordedHash}\n program ${programHash}\n\nThe recorded result was produced from different inputs, so replaying it would hand the program an answer to a question it is no longer asking.\n\nOptions\n fork(run, "${stepKey}") re-run from this step, keeping everything before it\n revert the inputs keep the recorded result`);
|
|
130
|
-
this.stepKey = stepKey;
|
|
131
|
-
this.recordedHash = recordedHash;
|
|
132
|
-
this.programHash = programHash;
|
|
133
|
-
this.name = "RunDivergence";
|
|
227
|
+
return new Frame(this.keys.branch(kind, name, occurrence, branchKey), this.clock.fork(), this.signal.child(),
|
|
228
|
+
// `conclave` opens a scope but not a RACE: one body, nothing running beside it, so a write
|
|
229
|
+
// from inside it is as ordered as a write anywhere else and the depth does not move.
|
|
230
|
+
kind === "conclave" ? this.depth : this.depth + 1);
|
|
134
231
|
}
|
|
135
232
|
}
|
|
136
233
|
class Interpreter {
|
|
137
234
|
ast;
|
|
138
235
|
options;
|
|
139
236
|
programHash;
|
|
237
|
+
pins;
|
|
140
238
|
journal;
|
|
141
239
|
prng;
|
|
142
|
-
|
|
143
|
-
ceiling;
|
|
240
|
+
effects;
|
|
144
241
|
steps = 0;
|
|
145
242
|
nextYield;
|
|
146
243
|
stepBudget;
|
|
147
244
|
yieldEvery;
|
|
148
|
-
|
|
245
|
+
/** The curated method tables (library.ts). Built once: they close over this interpreter's write check. */
|
|
246
|
+
methods;
|
|
247
|
+
constructor(ast, options, programHash, pins) {
|
|
149
248
|
this.ast = ast;
|
|
150
249
|
this.options = options;
|
|
151
250
|
this.programHash = programHash;
|
|
251
|
+
this.pins = pins;
|
|
252
|
+
// The journal and the run must be the same run. Request ids derive from `options.runId` while
|
|
253
|
+
// recorded results come from the journal, so a mismatch would submit work under one identity and
|
|
254
|
+
// resolve it against another's history.
|
|
255
|
+
if (options.journal !== undefined && options.journal.run !== options.runId)
|
|
256
|
+
throw new RuntimeFault("L5011", `this run is ${options.runId} but it was handed the journal of run ${options.journal.run}; a run resumes only from its own journal`);
|
|
152
257
|
this.journal = options.journal ?? new Journal({ run: options.runId });
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
258
|
+
// EVERY limit comes from the pins, never from a default applied here. A default resolved a
|
|
259
|
+
// second time is a default resolved by whichever interpreter happens to be resuming, which is
|
|
260
|
+
// exactly what pinning exists to stop.
|
|
261
|
+
// THE CEILING IS A RUN BOUND, SO THE COUNT STARTS WHERE THE RUN LEFT OFF. L4009 is named "Run
|
|
262
|
+
// effect ceiling reached" and the run record pins the ceiling — a pin is only worth
|
|
263
|
+
// refusing a mismatch on (L5009) if the thing it pins is enforced. Starting at 0 gave every
|
|
264
|
+
// activation a full allowance, so a runaway loop of effects that crashed or was released
|
|
265
|
+
// periodically never reached the ceiling however much it performed against the world, and the
|
|
266
|
+
// fault text claimed a run-scoped fact from an activation-scoped counter.
|
|
267
|
+
this.prng = new Prng(pins.seed);
|
|
268
|
+
this.effects = { journal: this.journal, options, ceiling: pins.effectCeiling, effectCount: this.journal.dispatchedEffects() };
|
|
269
|
+
this.stepBudget = pins.stepBudget;
|
|
270
|
+
this.yieldEvery = pins.yieldEvery;
|
|
157
271
|
this.nextYield = this.yieldEvery;
|
|
272
|
+
this.methods = {
|
|
273
|
+
array: arrayMethods(this.libraryContext()),
|
|
274
|
+
string: stringMethods(),
|
|
275
|
+
number: numberMethods(),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/** What the library sees of this interpreter. */
|
|
279
|
+
libraryContext() {
|
|
280
|
+
return {
|
|
281
|
+
runId: this.options.runId,
|
|
282
|
+
programHash: this.programHash,
|
|
283
|
+
startedAt: this.pins.startedAt,
|
|
284
|
+
prng: this.prng,
|
|
285
|
+
...(this.options.onLog !== undefined ? { onLog: this.options.onLog } : {}),
|
|
286
|
+
assertWritable: (target, frame) => this.assertWritable(target, frame),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// ---- values: reads and writes ---------------------------------------------------------------
|
|
290
|
+
/**
|
|
291
|
+
* May this frame write into this container? Two refusals, and they are the whole of the value
|
|
292
|
+
* half of freeze-on-share (design D4, §3.4 rule 4):
|
|
293
|
+
*
|
|
294
|
+
* - a FROZEN value crossed an effect boundary, and what crossed is what was recorded (L2031);
|
|
295
|
+
* - a value born OUTSIDE this concurrent branch and written inside it is L2032's defect reached
|
|
296
|
+
* through a value instead of a binding, and just as silent on resume.
|
|
297
|
+
*/
|
|
298
|
+
assertWritable(target, frame) {
|
|
299
|
+
if (Object.isFrozen(target)) {
|
|
300
|
+
throw new RuntimeFault("L2031", "this value crossed an effect boundary and is frozen: what crossed is what the journal recorded, so it cannot change afterwards. Build a new value instead: `{ ...record, field: value }` or `[...list, item]`.");
|
|
301
|
+
}
|
|
302
|
+
if (birthDepth(target) < frame.depth) {
|
|
303
|
+
throw new RuntimeFault("L2032", "this value was built outside this concurrent branch and is written inside it. Two branches writing one value is nondeterministic, and it is silent: live they write in completion order, on resume the recorded effects return instantly and they write in launch order, so the value differs and the run takes a path it never recorded. Build the value inside the branch and return it, and read it out of the combinator's result.");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** The property key a member expression names, as JavaScript would spell it. A computed key is
|
|
307
|
+
* held to the same no-implicit-conversion law as every other coercion site (L4018): `String(k)`
|
|
308
|
+
* on a record would enter the host's ToPrimitive, which calls the value's own `toString` — a
|
|
309
|
+
* program closure invoked without a Frame. Measured before the refusal: the closure's rejection
|
|
310
|
+
* escaped as an unhandled host TypeError AFTER the run returned, and `o[{}] = 1` silently minted
|
|
311
|
+
* the own field `"[object Object]"`. Primitives keep JavaScript's spelling (`o[1]`, `o[true]`). */
|
|
312
|
+
async memberKey(node, env, frame) {
|
|
313
|
+
if (node.computed !== true)
|
|
314
|
+
return node.property.name;
|
|
315
|
+
const k = await this.evaluate(node.property, env, frame);
|
|
316
|
+
if (typeof k === "string")
|
|
317
|
+
return k;
|
|
318
|
+
refuseCoercion("[...]", k);
|
|
319
|
+
return String(k);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Read a member. Records answer their own fields and `undefined` for anything else, so a host
|
|
323
|
+
* prototype is never reached (`o.constructor`, `o.toString` are `undefined`); strings, arrays and
|
|
324
|
+
* numbers answer `length`, an index, or an entry of their method table, and refuse anything else
|
|
325
|
+
* (L4014). Functions and booleans have no members.
|
|
326
|
+
*/
|
|
327
|
+
memberOf(obj, prop, asCallee = false) {
|
|
328
|
+
switch (typeof obj) {
|
|
329
|
+
case "string": {
|
|
330
|
+
if (prop === "length")
|
|
331
|
+
return obj.length;
|
|
332
|
+
const i = arrayIndex(prop);
|
|
333
|
+
if (i !== undefined)
|
|
334
|
+
return obj[i];
|
|
335
|
+
return this.method(this.methods.string, obj, prop, "a string", asCallee);
|
|
336
|
+
}
|
|
337
|
+
case "number":
|
|
338
|
+
return this.method(this.methods.number, obj, prop, "a number", asCallee);
|
|
339
|
+
case "object": {
|
|
340
|
+
if (obj === null)
|
|
341
|
+
throw new RuntimeFault("L4010", `cannot read \`${prop}\` of null`);
|
|
342
|
+
if (Array.isArray(obj)) {
|
|
343
|
+
if (prop === "length")
|
|
344
|
+
return obj.length;
|
|
345
|
+
const i = arrayIndex(prop);
|
|
346
|
+
if (i !== undefined)
|
|
347
|
+
return obj[i];
|
|
348
|
+
return this.method(this.methods.array, obj, prop, "an array", asCallee);
|
|
349
|
+
}
|
|
350
|
+
return Object.prototype.hasOwnProperty.call(obj, prop) ? obj[prop] : undefined;
|
|
351
|
+
}
|
|
352
|
+
case "undefined":
|
|
353
|
+
throw new RuntimeFault("L4010", `cannot read \`${prop}\` of undefined`);
|
|
354
|
+
default:
|
|
355
|
+
throw new RuntimeFault("L4014", `\`${prop}\` is not a member: a ${typeof obj} has no members`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
method(table, receiver, prop, kind, asCallee) {
|
|
359
|
+
const m = table[prop];
|
|
360
|
+
if (m === undefined) {
|
|
361
|
+
throw new RuntimeFault("L4014", `\`${prop}\` is not a member of ${kind}. The members are: length, an index, ${Object.keys(table).join(", ")}.`);
|
|
362
|
+
}
|
|
363
|
+
// A method is looked up at the call and exists nowhere else — a declared difference from
|
|
364
|
+
// JavaScript, where `xs.map` is a value. Handing one out produced everything a bound-function
|
|
365
|
+
// factory produces (measured): `xs.map === xs.map` was false where JavaScript says true, and
|
|
366
|
+
// an extracted `push` wrote to its receiver where strict JavaScript throws. Refusing the read
|
|
367
|
+
// is honest on both counts.
|
|
368
|
+
if (!asCallee) {
|
|
369
|
+
throw new RuntimeFault("L4020", `\`${prop}\` is a method of ${kind}, and a method is not a value here: it is looked up at the call, so it cannot be extracted, compared, or passed. Call it — \`.${prop}(...)\` — or wrap it: \`(...args) => value.${prop}(...args)\`.`);
|
|
370
|
+
}
|
|
371
|
+
return async (frame, args) => await m(frame, receiver, args);
|
|
372
|
+
}
|
|
373
|
+
/** Write a member: `o.a = v`, `xs[i] = v`. Records take any own field; arrays take an index or `length`. */
|
|
374
|
+
writeMember(obj, prop, value, frame) {
|
|
375
|
+
if (obj === null || obj === undefined || typeof obj !== "object") {
|
|
376
|
+
throw new RuntimeFault("L4010", `cannot write \`${prop}\` of ${obj === null ? "null" : typeof obj === "undefined" ? "undefined" : `a ${typeof obj}`}`);
|
|
377
|
+
}
|
|
378
|
+
this.assertWritable(obj, frame);
|
|
379
|
+
if (Array.isArray(obj)) {
|
|
380
|
+
if (prop === "length") {
|
|
381
|
+
// `xs.length = n` truncates, as in JavaScript. A LONGER length is refused: JavaScript would
|
|
382
|
+
// fill the gap with holes, and a hole is a value class this language does not have (its
|
|
383
|
+
// methods do not skip holes, so a program with holes would read differently here and on a
|
|
384
|
+
// real engine). Push what you need instead. `length` is not an own data property that
|
|
385
|
+
// `setOwn` can define, so the write goes to the array itself.
|
|
386
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > obj.length) {
|
|
387
|
+
throw new RuntimeFault("L4017", `\`length\` can only be set to an integer between 0 and the array's current length (${obj.length}), got ${typeof value === "number" ? value : typeof value}: a longer length would create holes, which this language does not have; push the elements instead`);
|
|
388
|
+
}
|
|
389
|
+
obj.length = value;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const i = arrayIndex(prop);
|
|
393
|
+
if (i === undefined) {
|
|
394
|
+
throw new RuntimeFault("L4014", `\`${prop}\` is not a member of an array: an array takes an index or \`length\``);
|
|
395
|
+
}
|
|
396
|
+
// Contiguous or refused: JavaScript would fill the gap with holes, and a hole is a value
|
|
397
|
+
// class this language does not have (measured before the refusal: `xs[2] = 1` on an empty
|
|
398
|
+
// array built a sparse array whose holes then crossed an effect boundary as silent nulls).
|
|
399
|
+
// Writing AT the length appends, which is `push` by another spelling and makes no hole.
|
|
400
|
+
if (i > obj.length) {
|
|
401
|
+
throw new RuntimeFault("L4019", `index ${i} is past the end of this array (length ${obj.length}), and JavaScript would fill the gap with holes, which this language does not have. Write at an existing index, at the length to append, or use \`push\`.`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else if (prop === "__proto__") {
|
|
405
|
+
throw new RuntimeFault("L4014", "`__proto__` names an object's prototype, and there are no prototypes here");
|
|
406
|
+
}
|
|
407
|
+
setRecordMember(obj, prop, value);
|
|
158
408
|
}
|
|
159
409
|
// ---- the fuel ceiling -----------------------------------------------------------------------
|
|
160
410
|
/**
|
|
@@ -167,131 +417,44 @@ class Interpreter {
|
|
|
167
417
|
tick(frame) {
|
|
168
418
|
this.steps += 1;
|
|
169
419
|
if (this.steps > this.stepBudget) {
|
|
170
|
-
throw new RuntimeFault("L4013", `this
|
|
420
|
+
throw new RuntimeFault("L4013", `this walk has taken more than ${this.stepBudget} interpreter steps without finishing, which means a loop that performs no effect is not terminating. The effect ceiling cannot see such a loop, because it performs nothing to count. Add an exit condition, or raise stepBudget if the program legitimately does this much work. (stepBudget bounds ONE WALK, not the run: steps are not recorded, so a resume cannot recover a count the way the effect ceiling can.)`);
|
|
171
421
|
}
|
|
172
422
|
if (this.steps < this.nextYield)
|
|
173
423
|
return null;
|
|
174
424
|
this.nextYield = this.steps + this.yieldEvery;
|
|
175
425
|
return this.breathe(frame);
|
|
176
426
|
}
|
|
177
|
-
/**
|
|
178
|
-
* Hand the macrotask queue back, then notice if this branch was cancelled while we were away.
|
|
179
|
-
*
|
|
180
|
-
* The cancellation check is deliberately HERE and not on every dispatch. Cancellation is
|
|
181
|
-
* otherwise observed only at effect boundaries (see {@link Interpreter.performEffect}), so a race
|
|
182
|
-
* loser that spins without performing an effect never learns it lost and spins forever. Checking
|
|
183
|
-
* at the yield boundary reaches exactly that case and no other: a branch that runs fewer than
|
|
184
|
-
* `yieldEvery` dispatches between two effects never crosses this line, so the cancellation law
|
|
185
|
-
* for ordinary programs is unchanged.
|
|
186
|
-
*/
|
|
187
427
|
get stepCount() {
|
|
188
428
|
return this.steps;
|
|
189
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Hand the macrotask queue back, then abandon this branch's pure work IF IT CAN NO LONGER MATTER.
|
|
432
|
+
*
|
|
433
|
+
* Cancellation is otherwise observed only at effect boundaries (see {@link Interpreter.performEffect}).
|
|
434
|
+
* This line used to cut every cancelled branch, so a `race` loser in a pure tail was abandoned at
|
|
435
|
+
* its next yield, and whether an arm that had already performed its last effect got to settle
|
|
436
|
+
* depended on how many dispatches its tail took against `yieldEvery`: the winner of a live race
|
|
437
|
+
* was a function of a host tuning knob (design §3.4, measured). The cut is now the scope's call
|
|
438
|
+
* (`Signal.cutPure`): an arm that cannot win any more is abandoned here, and an arm that could
|
|
439
|
+
* still win runs its pure work to a settle, so the winner is the recorded clocks and declaration
|
|
440
|
+
* order and nothing else (see `runScope`). An arm that could still win and spins forever is a
|
|
441
|
+
* pure infinite loop, and it ends the way every pure infinite loop ends: on the step budget
|
|
442
|
+
* (L4013), loudly, which is the run's answer rather than the scheduler's.
|
|
443
|
+
*/
|
|
190
444
|
async breathe(frame) {
|
|
191
445
|
await new Promise((resolve) => {
|
|
192
446
|
setTimeout(resolve, 0);
|
|
193
447
|
});
|
|
194
|
-
if (frame.signal.
|
|
448
|
+
if (frame.signal.cutPure)
|
|
195
449
|
throw new Cancelled(frame.signal.reason ?? "cancelled");
|
|
196
450
|
}
|
|
197
451
|
// ---- the effect seam ------------------------------------------------------------------------
|
|
198
452
|
/**
|
|
199
|
-
* Perform one effect, or replay it.
|
|
200
|
-
*
|
|
201
|
-
* Everything durable happens here. A handler is called only in the `miss` and `pending` cases,
|
|
202
|
-
* and in `pending` it is told to re-bind rather than re-issue.
|
|
453
|
+
* Perform one effect, or replay it. The machinery is {@link performEffect} in `perform.ts`,
|
|
454
|
+
* shared with the v2 engine host; this method binds it to this run.
|
|
203
455
|
*/
|
|
204
456
|
async performEffect(kind, name, hashedInput, perform, frame) {
|
|
205
|
-
|
|
206
|
-
const inputHash = digest(hashedInput ?? null);
|
|
207
|
-
const verdict = this.journal.lookup(key, inputHash);
|
|
208
|
-
switch (verdict.verdict) {
|
|
209
|
-
case "replay":
|
|
210
|
-
if (verdict.entry.endedAt !== undefined)
|
|
211
|
-
frame.clock.advance(verdict.entry.endedAt);
|
|
212
|
-
return verdict.entry.result;
|
|
213
|
-
case "replay-failed": {
|
|
214
|
-
if (verdict.entry.endedAt !== undefined)
|
|
215
|
-
frame.clock.advance(verdict.entry.endedAt);
|
|
216
|
-
const e = verdict.entry.error;
|
|
217
|
-
throw new EffectError(e.code, e.kind, e.message, e.detail);
|
|
218
|
-
}
|
|
219
|
-
case "replay-cancelled":
|
|
220
|
-
throw new Cancelled("this branch was cancelled on the recorded run");
|
|
221
|
-
case "diverged":
|
|
222
|
-
throw new RunDivergence(stepKeyString(key), verdict.recordedHash, verdict.programHash);
|
|
223
|
-
case "pending":
|
|
224
|
-
case "miss":
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
// A cancelled branch performs no NEW effects. That is the whole of the cancellation law on
|
|
228
|
-
// this side: work already in flight is another matter, and the handler owns it.
|
|
229
|
-
if (frame.signal.cancelled) {
|
|
230
|
-
throw new Cancelled(frame.signal.reason ?? "cancelled");
|
|
231
|
-
}
|
|
232
|
-
this.effectCount += 1;
|
|
233
|
-
if (this.effectCount > this.ceiling) {
|
|
234
|
-
throw new RuntimeFault("L4009", `this run has performed more than ${this.ceiling} effects, which means a loop is not terminating. Add an exit condition or a permit.`);
|
|
235
|
-
}
|
|
236
|
-
const resume = verdict.verdict === "pending" ? verdict.entry.external : undefined;
|
|
237
|
-
// RECOVERY SUBMITS UNDER THE RECORDED IDENTITY. Re-deriving happens to agree whenever nothing
|
|
238
|
-
// moved, which is exactly why it read as correct: the whole point of writing the id down is
|
|
239
|
-
// the case where it does NOT agree, and a resumed run that re-derives is reissuing under an
|
|
240
|
-
// identity the far side may never have seen. An entry with no recorded id predates this rule.
|
|
241
|
-
const recorded = verdict.verdict === "pending" && verdict.entry.requestId !== undefined ? verdict.entry : undefined;
|
|
242
|
-
const reqId = recorded?.requestId ?? requestId(this.options.runId, key, inputHash);
|
|
243
|
-
// WHICH attempt is open, not merely which id. An id alone cannot say how much of an escalation
|
|
244
|
-
// chain is already spent, and a recovery that cannot tell replays the hop: it mints again under
|
|
245
|
-
// the id the far side already holds and reads that mint's cached expiry back as a fresh
|
|
246
|
-
// observation. An entry written before the index existed reads as attempt 0, which is what it
|
|
247
|
-
// is for every effect that never hops.
|
|
248
|
-
const attempt = recorded?.attempt ?? 0;
|
|
249
|
-
if (verdict.verdict === "miss") {
|
|
250
|
-
this.journal.begin(key, inputHash, this.options.handler.now(), reqId);
|
|
251
|
-
}
|
|
252
|
-
const ctx = {
|
|
253
|
-
key,
|
|
254
|
-
signal: frame.signal,
|
|
255
|
-
// Derived from the run, the step, the inputs and the attempt, and written on the pending
|
|
256
|
-
// entry by `begin` above BEFORE the handler runs. A handler submits under it idempotently,
|
|
257
|
-
// so a resumed run reissues the same id rather than creating a second goal.
|
|
258
|
-
requestId: reqId,
|
|
259
|
-
attempt,
|
|
260
|
-
...(resume !== undefined ? { resume } : {}),
|
|
261
|
-
bind: async (external) => {
|
|
262
|
-
this.journal.bind(key, external);
|
|
263
|
-
},
|
|
264
|
-
};
|
|
265
|
-
try {
|
|
266
|
-
const result = await perform(ctx, inputHash);
|
|
267
|
-
assertCrossable(result, `the result of ${stepKeyString(key)}`);
|
|
268
|
-
const endedAt = this.options.handler.now();
|
|
269
|
-
this.journal.settle(key, { status: "ok", result: deepFreeze(result) }, endedAt);
|
|
270
|
-
frame.clock.advance(endedAt);
|
|
271
|
-
return result;
|
|
272
|
-
}
|
|
273
|
-
catch (e) {
|
|
274
|
-
const endedAt = this.options.handler.now();
|
|
275
|
-
if (e instanceof Cancelled) {
|
|
276
|
-
this.journal.settle(key, { status: "cancelled" }, endedAt);
|
|
277
|
-
throw e;
|
|
278
|
-
}
|
|
279
|
-
// A handler may raise a language code directly, and it survives. The simulator's "unscripted
|
|
280
|
-
// effect" is L6001, and flattening that to a generic handler fault would tell a caller acting
|
|
281
|
-
// on `code` that the handler broke, when what actually happened is that their script is
|
|
282
|
-
// incomplete. Only the L-code shape is honoured: anything else a thrown object happens to
|
|
283
|
-
// call `code` (an errno, an HTTP status) is a handler fault and is recorded as one.
|
|
284
|
-
const raised = e.code;
|
|
285
|
-
const carried = typeof raised === "string" && /^L\d{4}$/.test(raised) ? raised : null;
|
|
286
|
-
const error = e instanceof EffectError
|
|
287
|
-
? { code: e.code, kind: e.kind, message: e.message, ...(e.detail !== undefined ? { detail: e.detail } : {}) }
|
|
288
|
-
: carried !== null
|
|
289
|
-
? { code: carried, kind: "handler-fault", message: e.message }
|
|
290
|
-
: { code: "L4000", kind: "handler-fault", message: e.message };
|
|
291
|
-
this.journal.settle(key, { status: "failed", error }, endedAt);
|
|
292
|
-
frame.clock.advance(endedAt);
|
|
293
|
-
throw e instanceof EffectError ? e : new EffectError(error.code, error.kind, error.message);
|
|
294
|
-
}
|
|
457
|
+
return await performEffect(this.effects, kind, name, hashedInput, perform, frame);
|
|
295
458
|
}
|
|
296
459
|
// ---- expressions ------------------------------------------------------------------------------
|
|
297
460
|
async evaluate(node, env, frame) {
|
|
@@ -309,48 +472,64 @@ class Interpreter {
|
|
|
309
472
|
let out = "";
|
|
310
473
|
for (let i = 0; i < quasis.length; i += 1) {
|
|
311
474
|
out += quasis[i].value.cooked;
|
|
312
|
-
if (i < exprs.length)
|
|
313
|
-
|
|
475
|
+
if (i < exprs.length) {
|
|
476
|
+
// Primitives interpolate as JavaScript interpolates them; a container or a function is
|
|
477
|
+
// refused (L4018). Measured before the refusal: `${o}` on a record with its own
|
|
478
|
+
// `toString` crashed in the host's ToPrimitive, and `${f}` on a function PRINTED THE
|
|
479
|
+
// INTERPRETER'S OWN COMPILED CLOSURE — an implementation detail leaking into a value.
|
|
480
|
+
const v = await this.evaluate(exprs[i], env, frame);
|
|
481
|
+
refuseCoercion("${...}", v);
|
|
482
|
+
out += String(v);
|
|
483
|
+
}
|
|
314
484
|
}
|
|
315
485
|
return out;
|
|
316
486
|
}
|
|
317
487
|
case "ArrayExpression": {
|
|
318
488
|
const out = [];
|
|
319
489
|
for (const el of node.elements ?? []) {
|
|
320
|
-
if (el.type === "SpreadElement")
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
490
|
+
if (el.type === "SpreadElement")
|
|
491
|
+
out.push(...this.spreadable(await this.evaluate(el.argument, env, frame)));
|
|
492
|
+
else
|
|
325
493
|
out.push(await this.evaluate(el, env, frame));
|
|
326
|
-
}
|
|
327
494
|
}
|
|
328
|
-
return out;
|
|
495
|
+
return born(out, frame.depth);
|
|
329
496
|
}
|
|
330
497
|
case "ObjectExpression": {
|
|
331
498
|
const out = {};
|
|
332
499
|
for (const p of node.properties ?? []) {
|
|
333
500
|
if (p.type === "SpreadElement") {
|
|
334
|
-
|
|
501
|
+
const src = await this.evaluate(p.argument, env, frame);
|
|
502
|
+
if (src !== null && src !== undefined) {
|
|
503
|
+
for (const [k, v] of Object.entries(src))
|
|
504
|
+
setRecordMember(out, k, v);
|
|
505
|
+
}
|
|
335
506
|
continue;
|
|
336
507
|
}
|
|
337
508
|
const key = p.key;
|
|
338
509
|
const name = key.type === "Identifier" ? key.name : String(key.value);
|
|
339
|
-
out
|
|
510
|
+
setRecordMember(out, name, await this.evaluate(p.value, env, frame));
|
|
340
511
|
}
|
|
341
|
-
return out;
|
|
512
|
+
return born(out, frame.depth);
|
|
342
513
|
}
|
|
343
514
|
case "MemberExpression": {
|
|
344
515
|
const obj = await this.evaluate(node.object, env, frame);
|
|
345
|
-
if (obj === null || obj === undefined)
|
|
346
|
-
|
|
516
|
+
if ((obj === null || obj === undefined) && node.optional === true)
|
|
517
|
+
throw SHORT_CIRCUIT;
|
|
518
|
+
return this.memberOf(obj, await this.memberKey(node, env, frame));
|
|
519
|
+
}
|
|
520
|
+
case "ChainExpression": {
|
|
521
|
+
// `a?.b.c(d)`: a nullish `a` ends the WHOLE chain with `undefined`, and nothing after the
|
|
522
|
+
// `?.` is evaluated. The short-circuit travels as a private sentinel that only this case
|
|
523
|
+
// catches; a program's `try` cannot see it, because a chain is an expression and a `try`
|
|
524
|
+
// wraps statements.
|
|
525
|
+
try {
|
|
526
|
+
return await this.evaluate(node.expression, env, frame);
|
|
527
|
+
}
|
|
528
|
+
catch (e) {
|
|
529
|
+
if (e === SHORT_CIRCUIT)
|
|
347
530
|
return undefined;
|
|
348
|
-
throw
|
|
531
|
+
throw e;
|
|
349
532
|
}
|
|
350
|
-
const prop = node.computed === true
|
|
351
|
-
? String(await this.evaluate(node.property, env, frame))
|
|
352
|
-
: node.property.name;
|
|
353
|
-
return obj[prop];
|
|
354
533
|
}
|
|
355
534
|
case "UnaryExpression": {
|
|
356
535
|
const v = await this.evaluate(node.argument, env, frame);
|
|
@@ -358,15 +537,40 @@ class Interpreter {
|
|
|
358
537
|
case "!":
|
|
359
538
|
return !v;
|
|
360
539
|
case "-":
|
|
540
|
+
refuseCoercion("-", v);
|
|
361
541
|
return -v;
|
|
362
542
|
case "+":
|
|
543
|
+
refuseCoercion("+", v);
|
|
363
544
|
return +v;
|
|
545
|
+
case "~":
|
|
546
|
+
refuseCoercion("~", v);
|
|
547
|
+
return ~v;
|
|
364
548
|
case "typeof":
|
|
365
549
|
return typeof v;
|
|
366
550
|
default:
|
|
367
551
|
throw new RuntimeFault("L1000", `unsupported unary operator ${String(node.operator)}`);
|
|
368
552
|
}
|
|
369
553
|
}
|
|
554
|
+
case "UpdateExpression": {
|
|
555
|
+
// `x++`, `--o.count`: JavaScript's meaning, with the write going through the same two doors
|
|
556
|
+
// as an assignment (a binding's depth, a value's writability).
|
|
557
|
+
const delta = node.operator === "++" ? 1 : -1;
|
|
558
|
+
const prefix = node.prefix === true;
|
|
559
|
+
const arg = node.argument;
|
|
560
|
+
if (arg.type === "Identifier") {
|
|
561
|
+
const name = arg.name;
|
|
562
|
+
const old = Number(env.get(name));
|
|
563
|
+
const next = old + delta;
|
|
564
|
+
env.set(name, next, frame.depth);
|
|
565
|
+
return prefix ? next : old;
|
|
566
|
+
}
|
|
567
|
+
const obj = await this.evaluate(arg.object, env, frame);
|
|
568
|
+
const key = await this.memberKey(arg, env, frame);
|
|
569
|
+
const old = Number(this.memberOf(obj, key));
|
|
570
|
+
const next = old + delta;
|
|
571
|
+
this.writeMember(obj, key, next, frame);
|
|
572
|
+
return prefix ? next : old;
|
|
573
|
+
}
|
|
370
574
|
case "BinaryExpression": {
|
|
371
575
|
const l = await this.evaluate(node.left, env, frame);
|
|
372
576
|
const r = await this.evaluate(node.right, env, frame);
|
|
@@ -393,15 +597,8 @@ class Interpreter {
|
|
|
393
597
|
return (await this.evaluate(node.test, env, frame))
|
|
394
598
|
? await this.evaluate(node.consequent, env, frame)
|
|
395
599
|
: await this.evaluate(node.alternate, env, frame);
|
|
396
|
-
case "AssignmentExpression":
|
|
397
|
-
|
|
398
|
-
const left = node.left;
|
|
399
|
-
if (left.type !== "Identifier") {
|
|
400
|
-
throw new RuntimeFault("L2031", "only a plain binding can be assigned to");
|
|
401
|
-
}
|
|
402
|
-
env.set(left.name, value);
|
|
403
|
-
return value;
|
|
404
|
-
}
|
|
600
|
+
case "AssignmentExpression":
|
|
601
|
+
return await this.assign(node, env, frame);
|
|
405
602
|
case "AwaitExpression":
|
|
406
603
|
return await this.evaluate(node.argument, env, frame);
|
|
407
604
|
case "ArrowFunctionExpression":
|
|
@@ -413,60 +610,139 @@ class Interpreter {
|
|
|
413
610
|
throw new RuntimeFault("L1000", `unsupported expression ${node.type}`);
|
|
414
611
|
}
|
|
415
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* Every assignment operator, on a binding or a member: `x = v`, `x += v`, `o.a ??= v`,
|
|
615
|
+
* `[a, b] = [b, a]`. The operator's meaning is JavaScript's; the write goes through the binding's
|
|
616
|
+
* depth check ({@link Env.set}) or the value's writability check ({@link Interpreter.writeMember}).
|
|
617
|
+
*/
|
|
618
|
+
async assign(node, env, frame) {
|
|
619
|
+
const op = node.operator;
|
|
620
|
+
const left = node.left;
|
|
621
|
+
if (left.type === "ObjectPattern" || left.type === "ArrayPattern") {
|
|
622
|
+
const value = await this.evaluate(node.right, env, frame);
|
|
623
|
+
await this.bindPattern(left, value, env, frame, "assign");
|
|
624
|
+
return value;
|
|
625
|
+
}
|
|
626
|
+
const read = left.type === "Identifier"
|
|
627
|
+
? { get: () => env.get(left.name), set: (v) => env.set(left.name, v, frame.depth) }
|
|
628
|
+
: await (async () => {
|
|
629
|
+
const obj = await this.evaluate(left.object, env, frame);
|
|
630
|
+
const key = await this.memberKey(left, env, frame);
|
|
631
|
+
return { get: () => this.memberOf(obj, key), set: (v) => this.writeMember(obj, key, v, frame) };
|
|
632
|
+
})();
|
|
633
|
+
if (op === "=") {
|
|
634
|
+
const v = await this.evaluate(node.right, env, frame);
|
|
635
|
+
read.set(v);
|
|
636
|
+
return v;
|
|
637
|
+
}
|
|
638
|
+
if (op === "&&=" || op === "||=" || op === "??=") {
|
|
639
|
+
const cur = read.get();
|
|
640
|
+
const proceed = op === "&&=" ? Boolean(cur) : op === "||=" ? !cur : cur === null || cur === undefined;
|
|
641
|
+
if (!proceed)
|
|
642
|
+
return cur;
|
|
643
|
+
const v = await this.evaluate(node.right, env, frame);
|
|
644
|
+
read.set(v);
|
|
645
|
+
return v;
|
|
646
|
+
}
|
|
647
|
+
const cur = read.get();
|
|
648
|
+
const r = await this.evaluate(node.right, env, frame);
|
|
649
|
+
const v = applyBinary(op.slice(0, -1), cur, r);
|
|
650
|
+
read.set(v);
|
|
651
|
+
return v;
|
|
652
|
+
}
|
|
653
|
+
/** What `...x` and `for (const v of x)` may iterate: an array or a string, and nothing else (L4015). */
|
|
654
|
+
spreadable(v) {
|
|
655
|
+
if (Array.isArray(v))
|
|
656
|
+
return v;
|
|
657
|
+
if (typeof v === "string")
|
|
658
|
+
return [...v];
|
|
659
|
+
throw new RuntimeFault("L4015", `${v === null ? "null" : typeof v === "object" ? "a record" : typeof v} is not iterable: only arrays and strings can be spread or looped over. For a record, iterate \`keys(record)\` or \`entries(record)\`.`);
|
|
660
|
+
}
|
|
416
661
|
makeFunction(node, closure) {
|
|
417
662
|
const params = node.params ?? [];
|
|
418
663
|
const body = node.body;
|
|
419
664
|
const isExpressionBody = body.type !== "BlockStatement";
|
|
420
|
-
|
|
421
|
-
|
|
665
|
+
const self = async (frame, args) => {
|
|
666
|
+
// The calling FRAME decides the depth, not the closure: a helper declared at the top level
|
|
667
|
+
// and called from inside a branch is executing concurrently, whatever scope it was written in.
|
|
668
|
+
const env = new Env(closure, frame.depth);
|
|
669
|
+
// A named function expression sees its own name: `const f = function walk(n) { ... walk() }`.
|
|
670
|
+
if (node.type === "FunctionExpression" && node.id !== null && node.id !== undefined) {
|
|
671
|
+
env.declare(node.id.name, self, false);
|
|
672
|
+
}
|
|
422
673
|
for (let i = 0; i < params.length; i += 1) {
|
|
423
|
-
|
|
674
|
+
const param = params[i];
|
|
675
|
+
if (param.type === "RestElement") {
|
|
676
|
+
await this.bindPattern(param.argument, born(args.slice(i), frame.depth), env, frame, "let");
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
await this.bindPattern(param, args[i], env, frame, "let");
|
|
424
680
|
}
|
|
425
681
|
if (isExpressionBody)
|
|
426
682
|
return await this.evaluate(body, env, frame);
|
|
427
683
|
const c = await this.executeBlock(body, env, frame);
|
|
428
684
|
return c.type === "return" ? c.value : undefined;
|
|
429
685
|
};
|
|
686
|
+
return self;
|
|
430
687
|
}
|
|
431
|
-
|
|
688
|
+
/**
|
|
689
|
+
* Bind a pattern: declare its names (`const`/`let`, including parameters, which are `let`) or
|
|
690
|
+
* assign to bindings that already exist (`assign`, for `[a, b] = [b, a]`).
|
|
691
|
+
*/
|
|
692
|
+
async bindPattern(pattern, value, env, frame, mode) {
|
|
432
693
|
switch (pattern.type) {
|
|
433
694
|
case "Identifier":
|
|
434
|
-
|
|
695
|
+
if (mode === "assign")
|
|
696
|
+
env.set(pattern.name, value, frame.depth);
|
|
697
|
+
else
|
|
698
|
+
env.declare(pattern.name, value, mode === "let");
|
|
699
|
+
return;
|
|
700
|
+
case "MemberExpression": {
|
|
701
|
+
// Only reachable in `assign` mode: `[o.a, o.b] = pair`.
|
|
702
|
+
const obj = await this.evaluate(pattern.object, env, frame);
|
|
703
|
+
this.writeMember(obj, await this.memberKey(pattern, env, frame), value, frame);
|
|
435
704
|
return;
|
|
705
|
+
}
|
|
436
706
|
case "AssignmentPattern":
|
|
437
|
-
await this.bindPattern(pattern.left, value === undefined ? await this.evaluate(pattern.right, env, frame) : value, env, frame,
|
|
707
|
+
await this.bindPattern(pattern.left, value === undefined ? await this.evaluate(pattern.right, env, frame) : value, env, frame, mode);
|
|
438
708
|
return;
|
|
439
709
|
case "ObjectPattern": {
|
|
440
|
-
|
|
710
|
+
if (value === null || value === undefined) {
|
|
711
|
+
throw new RuntimeFault("L4010", `cannot destructure ${String(value)}: there are no fields to take`);
|
|
712
|
+
}
|
|
713
|
+
const src = value;
|
|
441
714
|
const taken = [];
|
|
442
715
|
for (const p of pattern.properties) {
|
|
443
716
|
if (p.type === "RestElement") {
|
|
444
717
|
const rest = {};
|
|
445
718
|
for (const [k, v] of Object.entries(src))
|
|
446
719
|
if (!taken.includes(k))
|
|
447
|
-
rest
|
|
448
|
-
await this.bindPattern(p.argument, rest, env, frame,
|
|
720
|
+
setRecordMember(rest, k, v);
|
|
721
|
+
await this.bindPattern(p.argument, born(rest, frame.depth), env, frame, mode);
|
|
449
722
|
continue;
|
|
450
723
|
}
|
|
451
724
|
const key = p.key;
|
|
452
725
|
const name = key.type === "Identifier" ? key.name : String(key.value);
|
|
453
726
|
taken.push(name);
|
|
454
|
-
await this.bindPattern(p.value, src
|
|
727
|
+
await this.bindPattern(p.value, this.memberOf(src, name), env, frame, mode);
|
|
455
728
|
}
|
|
456
729
|
return;
|
|
457
730
|
}
|
|
458
731
|
case "ArrayPattern": {
|
|
459
|
-
|
|
732
|
+
if (value === null || value === undefined) {
|
|
733
|
+
throw new RuntimeFault("L4010", `cannot destructure ${String(value)}: there are no elements to take`);
|
|
734
|
+
}
|
|
735
|
+
const src = this.spreadable(value);
|
|
460
736
|
const els = pattern.elements;
|
|
461
737
|
for (let i = 0; i < els.length; i += 1) {
|
|
462
738
|
const el = els[i];
|
|
463
739
|
if (el === null || el === undefined)
|
|
464
740
|
continue;
|
|
465
741
|
if (el.type === "RestElement") {
|
|
466
|
-
await this.bindPattern(el.argument, src.slice(i), env, frame,
|
|
742
|
+
await this.bindPattern(el.argument, born(src.slice(i), frame.depth), env, frame, mode);
|
|
467
743
|
break;
|
|
468
744
|
}
|
|
469
|
-
await this.bindPattern(el, src[i], env, frame,
|
|
745
|
+
await this.bindPattern(el, src[i], env, frame, mode);
|
|
470
746
|
}
|
|
471
747
|
return;
|
|
472
748
|
}
|
|
@@ -484,11 +760,24 @@ class Interpreter {
|
|
|
484
760
|
if (callee.type === "Identifier" && PRIMITIVES[callee.name] !== undefined && !env.has(callee.name)) {
|
|
485
761
|
return await this.callPrimitive(callee.name, argNodes, env, frame);
|
|
486
762
|
}
|
|
487
|
-
|
|
763
|
+
let fn;
|
|
764
|
+
if (callee.type === "MemberExpression") {
|
|
765
|
+
// The one place a method NAME may appear: as the callee. Resolving it here, with the flag,
|
|
766
|
+
// is what lets `memberOf` refuse the same name everywhere else (L4020).
|
|
767
|
+
const obj = await this.evaluate(callee.object, env, frame);
|
|
768
|
+
if ((obj === null || obj === undefined) && callee.optional === true)
|
|
769
|
+
throw SHORT_CIRCUIT;
|
|
770
|
+
fn = this.memberOf(obj, await this.memberKey(callee, env, frame), true);
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
fn = await this.evaluate(callee, env, frame);
|
|
774
|
+
}
|
|
775
|
+
if ((fn === null || fn === undefined) && node.optional === true)
|
|
776
|
+
throw SHORT_CIRCUIT;
|
|
488
777
|
const args = [];
|
|
489
778
|
for (const a of argNodes) {
|
|
490
779
|
if (a.type === "SpreadElement")
|
|
491
|
-
args.push(...(await this.evaluate(a.argument, env, frame)));
|
|
780
|
+
args.push(...this.spreadable(await this.evaluate(a.argument, env, frame)));
|
|
492
781
|
else
|
|
493
782
|
args.push(await this.evaluate(a, env, frame));
|
|
494
783
|
}
|
|
@@ -497,9 +786,6 @@ class Interpreter {
|
|
|
497
786
|
}
|
|
498
787
|
return await fn(frame, args);
|
|
499
788
|
}
|
|
500
|
-
option(bag, key) {
|
|
501
|
-
return bag === null || typeof bag !== "object" ? undefined : bag[key];
|
|
502
|
-
}
|
|
503
789
|
async callPrimitive(name, argNodes, env, frame) {
|
|
504
790
|
const spec = PRIMITIVES[name];
|
|
505
791
|
if (spec === undefined)
|
|
@@ -511,198 +797,36 @@ class Interpreter {
|
|
|
511
797
|
const args = [];
|
|
512
798
|
for (const a of argNodes)
|
|
513
799
|
args.push(await this.evaluate(a, env, frame));
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
...(this.option(bag, "permits") !== undefined
|
|
541
|
-
? { permits: this.option(bag, "permits") }
|
|
542
|
-
: {}),
|
|
543
|
-
...(this.option(bag, "supervise") !== undefined
|
|
544
|
-
? { supervise: this.option(bag, "supervise") }
|
|
545
|
-
: {}),
|
|
546
|
-
...(this.option(bag, "onFork") !== undefined ? { onFork: this.option(bag, "onFork") } : {}),
|
|
547
|
-
};
|
|
548
|
-
return await this.performEffect("spawn", stepName ?? persona,
|
|
549
|
-
// Model and variant are part of the IDENTITY being spawned, so they are hashed with the
|
|
550
|
-
// persona (design 5.12). A run that swapped the model under a recorded agent would be
|
|
551
|
-
// replaying a fact about a different agent.
|
|
552
|
-
{
|
|
553
|
-
persona,
|
|
554
|
-
model: model ?? null,
|
|
555
|
-
variant: variant ?? null,
|
|
556
|
-
worktree: req.worktree ?? null,
|
|
557
|
-
role: req.role ?? null,
|
|
558
|
-
join: (req.join ?? []).map((c) => c.channel),
|
|
559
|
-
}, (ctx) => handler.spawn(req, ctx), frame);
|
|
560
|
-
}
|
|
561
|
-
case "turn": {
|
|
562
|
-
const agent = deepFreeze(args[0]);
|
|
563
|
-
// The deadline STOPS OBSERVATION (design 5.12), so it belongs in the projection: a turn
|
|
564
|
-
// recorded under a 1m deadline cannot answer what a 10m turn would have produced, and a
|
|
565
|
-
// resumed run under the edited deadline replaying the old result is the silent-wrong-path
|
|
566
|
-
// class. Closing that for `checkpoint` and leaving it open on the siblings closed nothing.
|
|
567
|
-
const deadline = this.option(bag, "deadline");
|
|
568
|
-
return await this.performEffect("turn", stepName, { agent: agent.agent, deadline: deadline ?? null }, (ctx) => handler.turn({ agent, ...(deadline !== undefined ? { deadline } : {}) }, ctx), frame);
|
|
569
|
-
}
|
|
570
|
-
case "ask": {
|
|
571
|
-
const agent = deepFreeze(args[0]);
|
|
572
|
-
const schema = this.option(bag, "schema");
|
|
573
|
-
// Both of these END THE ASKING: `deadline` is the cutoff and `attempts` is how many
|
|
574
|
-
// schema-failed replies are tolerated before it gives up. A record made under one attempt
|
|
575
|
-
// is not an answer to what five attempts would have produced.
|
|
576
|
-
const deadline = this.option(bag, "deadline");
|
|
577
|
-
const attempts = this.option(bag, "attempts");
|
|
578
|
-
return await this.performEffect("ask", stepName, { agent: agent.agent, schema: schema ?? null, deadline: deadline ?? null, attempts: attempts ?? null }, (ctx) => handler.ask({
|
|
579
|
-
agent,
|
|
580
|
-
schema,
|
|
581
|
-
...(deadline !== undefined ? { deadline } : {}),
|
|
582
|
-
...(attempts !== undefined ? { attempts } : {}),
|
|
583
|
-
}, ctx), frame);
|
|
584
|
-
}
|
|
585
|
-
case "checkpoint": {
|
|
586
|
-
const prompt = args[1];
|
|
587
|
-
// The disposition is computed from TODAY's source, after the journal is consulted, on the
|
|
588
|
-
// live path and the replay path alike. performEffect returns the RAW outcome, which is
|
|
589
|
-
// what the journal holds; the policy sandwich closes here so a resumed run under an edited
|
|
590
|
-
// onExpiry throws even though nothing about the recorded expiry changed.
|
|
591
|
-
const onExpiry = this.option(bag, "onExpiry");
|
|
592
|
-
const schema = this.option(bag, "schema");
|
|
593
|
-
// The SAME projection the entry is keyed by, so an attempt's identity is a function of the
|
|
594
|
-
// step it belongs to rather than of anything the escalation invents.
|
|
595
|
-
// Design 5.12, and every field here earns its place. `timeout` STOPS OBSERVATION, so a
|
|
596
|
-
// record made under 1m cannot answer what a 3m wait would have seen. `escalate` and its
|
|
597
|
-
// `to` CREATE AN EFFECT rather than choosing a disposition, so editing them must diverge
|
|
598
|
-
// rather than be reapplied. `fail` versus `proceed` is the one genuine reapply and stays
|
|
599
|
-
// out. Hashing only prompt and schema left a timeout edit replaying clean, which is the
|
|
600
|
-
// silent-wrong-path class this projection exists to close.
|
|
601
|
-
const cpTimeout = this.option(bag, "timeout");
|
|
602
|
-
const cpTo = this.option(bag, "to");
|
|
603
|
-
const cpInput = {
|
|
604
|
-
prompt,
|
|
605
|
-
schema: schema ?? null,
|
|
606
|
-
timeout: cpTimeout ?? null,
|
|
607
|
-
...(onExpiry === "escalate" ? { onExpiry, to: cpTo ?? null } : {}),
|
|
608
|
-
};
|
|
609
|
-
return applyCheckpointPolicy((await this.performEffect("checkpoint", stepName, cpInput, async (ctx, inputHash) => {
|
|
610
|
-
// ONE hash value, threaded from what the entry is actually keyed by rather than
|
|
611
|
-
// re-digested from the projection here. The two agreed, which is exactly the problem:
|
|
612
|
-
// a second derivation that happens to match is a coincidence maintained by hand, and
|
|
613
|
-
// the first edit to the projection would desync attempt 1's identity from its own
|
|
614
|
-
// step with no type error and no failing test.
|
|
615
|
-
const attemptId = (n) => requestId(this.options.runId, ctx.key, inputHash, n);
|
|
616
|
-
const req = {
|
|
617
|
-
prompt,
|
|
618
|
-
...(schema !== undefined ? { schema } : {}),
|
|
619
|
-
...(cpTimeout !== undefined ? { timeout: cpTimeout } : {}),
|
|
620
|
-
...(onExpiry !== undefined ? { onExpiry } : {}),
|
|
621
|
-
...(cpTo !== undefined ? { to: cpTo } : {}),
|
|
622
|
-
};
|
|
623
|
-
// THE FINAL MINT DOES NOT ASK FOR AN ESCALATION. The interpreter owns the one-hop stop
|
|
624
|
-
// rule, and it can only own it if the far side is not simultaneously told to hop: a
|
|
625
|
-
// handler that honours `onExpiry` on the wire would mint a third attempt under an
|
|
626
|
-
// identity this journal never allocated, and nothing here would ever learn of it.
|
|
627
|
-
const finalReq = onExpiry === "escalate" ? { ...req, onExpiry: "proceed" } : req;
|
|
628
|
-
// RECOVERY COMPLETES THE OPEN ATTEMPT. IT DOES NOT REPLAY THE CHAIN.
|
|
629
|
-
//
|
|
630
|
-
// Arriving here with a non-zero attempt means the hop was issued before the crash, so
|
|
631
|
-
// the far side is already holding work under this very id. Re-running the live body
|
|
632
|
-
// from the top would call the handler again under it and take that call's cached
|
|
633
|
-
// expiry for a second observation: the stop rule would be satisfied on paper while the
|
|
634
|
-
// run had in fact observed one attempt twice. The chain's shape is recoverable without
|
|
635
|
-
// re-running it, because attempt 0's identity is derivable and its outcome is implied:
|
|
636
|
-
// the only path that opens attempt 1 is attempt 0 expiring.
|
|
637
|
-
if (ctx.attempt > 0) {
|
|
638
|
-
const raw = await handler.checkpoint(finalReq, ctx);
|
|
639
|
-
return {
|
|
640
|
-
...raw,
|
|
641
|
-
attempts: [
|
|
642
|
-
{ attempt: 0, requestId: attemptId(0), settled: "expired" },
|
|
643
|
-
{ attempt: ctx.attempt, requestId: ctx.requestId, to: cpTo ?? null, settled: raw.outcome },
|
|
644
|
-
],
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
const first = await handler.checkpoint(req, ctx);
|
|
648
|
-
if (first.outcome !== "expired" || onExpiry !== "escalate") {
|
|
649
|
-
// `ctx.attempt`, not a literal 0. Writing the literal made every recovery relabel the
|
|
650
|
-
// open attempt as the first one, which erased the hop from the journal and left the
|
|
651
|
-
// record claiming the escalated mint was the original.
|
|
652
|
-
return { ...first, attempts: [{ attempt: ctx.attempt, requestId: ctx.requestId, settled: first.outcome }] };
|
|
653
|
-
}
|
|
654
|
-
// ESCALATION STAYS INSIDE THIS ENTRY. The program made one call, and the interpreter
|
|
655
|
-
// owns key allocation, so a second mint must not become a second occurrence. What it
|
|
656
|
-
// does need is a second IDENTITY, derived from attempt 1 before the mint happens, or a
|
|
657
|
-
// crash between minting and recording leaves live work nothing in the journal names.
|
|
658
|
-
//
|
|
659
|
-
// Name the open attempt on the pending row BEFORE issuing it, index and all.
|
|
660
|
-
const nextId = attemptId(1);
|
|
661
|
-
this.journal.reissueAs(ctx.key, nextId, 1);
|
|
662
|
-
const second = await handler.checkpoint(finalReq, { ...ctx, requestId: nextId, attempt: 1 });
|
|
663
|
-
// ONE HOP. An escalation that can escalate again never terminates, so a second expiry
|
|
664
|
-
// settles as expired and the program decides, exactly as `proceed` would.
|
|
665
|
-
return {
|
|
666
|
-
...second,
|
|
667
|
-
attempts: [
|
|
668
|
-
{ attempt: 0, requestId: ctx.requestId, settled: "expired" },
|
|
669
|
-
{ attempt: 1, requestId: nextId, to: cpTo ?? null, settled: second.outcome },
|
|
670
|
-
],
|
|
671
|
-
};
|
|
672
|
-
}, frame)), onExpiry);
|
|
673
|
-
}
|
|
674
|
-
case "sleep": {
|
|
675
|
-
const duration = args[0];
|
|
676
|
-
parseDuration(duration); // fail at the call, not inside the handler
|
|
677
|
-
// The duration IS hashed (design 5.12). It determines the recorded fact: a resumed run
|
|
678
|
-
// reads the elapsed time back through the run clock, so editing 1h to 1m must diverge
|
|
679
|
-
// rather than silently keep the path the old duration chose. This hashed `null` until
|
|
680
|
-
// critic2 executed it, and the rule it violates is one this lane wrote and then only
|
|
681
|
-
// ever applied to the document.
|
|
682
|
-
return await this.performEffect("sleep", stepName ?? "", { duration }, (ctx) => handler.sleep({ duration }, ctx), frame);
|
|
683
|
-
}
|
|
684
|
-
case "wait": {
|
|
685
|
-
const event = deepFreeze(args[0]);
|
|
686
|
-
const timeout = this.option(bag, "timeout");
|
|
687
|
-
// A `wait` that resolved null did not observe "the event never happens": it observed "the
|
|
688
|
-
// event did not happen WITHIN THIS TIMEOUT". Editing the timeout therefore asks a different
|
|
689
|
-
// question, and replaying the recorded null answers the old one. This is the same hole the
|
|
690
|
-
// checkpoint projection closed, and leaving it open here left `?? recovery` steering off a
|
|
691
|
-
// stale cutoff.
|
|
692
|
-
return await this.performEffect("wait", stepName ?? "", { event, timeout: timeout ?? null }, (ctx) => handler.wait({ event, ...(timeout !== undefined ? { timeout } : {}) }, ctx), frame);
|
|
693
|
-
}
|
|
694
|
-
case "notify": {
|
|
695
|
-
const agents = deepFreeze(args[0]);
|
|
696
|
-
const fact = deepFreeze(args[1]);
|
|
697
|
-
return await this.performEffect("notify", stepName ?? "", { agents: agents.map((a) => a.agent), fact }, (ctx) => handler.notify({ agents, fact }, ctx), frame);
|
|
698
|
-
}
|
|
699
|
-
case "monitor": {
|
|
700
|
-
const agent = deepFreeze(args[0]);
|
|
701
|
-
return await this.performEffect("monitor", stepName ?? "", { agent: agent.agent }, (ctx) => handler.monitor({ agent }, ctx), frame);
|
|
702
|
-
}
|
|
703
|
-
default:
|
|
704
|
-
throw new RuntimeFault("L1000", `${name} is not implemented in this interpreter`);
|
|
800
|
+
return await dispatchPrimitive(this.effects, name, args, frame);
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* The `branchDigest`, over the arms a settled `race` will never be walked into.
|
|
804
|
+
*
|
|
805
|
+
* STRUCTURE, NOT TEXT AND NOT POSITION. Acorn nodes carry `start`/`end`, and an edit anywhere
|
|
806
|
+
* earlier in the file moves every offset after it — a digest over those would diverge on a run
|
|
807
|
+
* whose race nobody touched, which is the false positive that teaches people to bypass a check.
|
|
808
|
+
* Digesting the source SLICE instead would diverge on reindentation for the same reason. What is
|
|
809
|
+
* hashed is the branch body's shape with positions stripped, so a reformat is silent and an edit
|
|
810
|
+
* is not.
|
|
811
|
+
*
|
|
812
|
+
* Only the LOSERS, because only they are unwalked. The winner's arm is walked entry by entry and
|
|
813
|
+
* an edit inside it already diverges on the ordinary hash check with the step it broke named —
|
|
814
|
+
* a strictly better error than "some branch changed". Digesting the winner too would replace that
|
|
815
|
+
* error with this one, so it does not.
|
|
816
|
+
*/
|
|
817
|
+
branchDigester(branchesNode) {
|
|
818
|
+
if (branchesNode?.type !== "ObjectExpression")
|
|
819
|
+
return undefined;
|
|
820
|
+
const bodies = new Map();
|
|
821
|
+
for (const p of branchesNode.properties ?? []) {
|
|
822
|
+
const key = p.key;
|
|
823
|
+
const named = key?.name ?? key?.value;
|
|
824
|
+
if (named !== undefined)
|
|
825
|
+
bodies.set(named, stripPositions(p.value));
|
|
705
826
|
}
|
|
827
|
+
return (losers) => digest([...losers]
|
|
828
|
+
.sort()
|
|
829
|
+
.map((n) => [n, bodies.has(n) ? bodies.get(n) : null]));
|
|
706
830
|
}
|
|
707
831
|
/**
|
|
708
832
|
* The concurrency combinators.
|
|
@@ -712,6 +836,10 @@ class Interpreter {
|
|
|
712
836
|
* the same named effect cannot race for a counter, and replay reproduces both regardless of
|
|
713
837
|
* which one finished first.
|
|
714
838
|
*/
|
|
839
|
+
/** An options-bag field, read the way the shared scope machinery reads one. */
|
|
840
|
+
option(bag, key) {
|
|
841
|
+
return option(bag, key);
|
|
842
|
+
}
|
|
715
843
|
async callScope(name, argNodes, env, frame) {
|
|
716
844
|
const spec = PRIMITIVES[name];
|
|
717
845
|
if (spec === undefined)
|
|
@@ -722,66 +850,24 @@ class Interpreter {
|
|
|
722
850
|
const bag = bagNode === undefined ? undefined : await this.evaluate(bagNode, env, frame);
|
|
723
851
|
const scopeName = this.option(bag, "name") ?? null;
|
|
724
852
|
const occurrence = frame.keys.nextScope(scopeKind, scopeName);
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const results = await Promise.all(running);
|
|
734
|
-
frame.clock.join(frames.map((f) => f.clock));
|
|
735
|
-
return Array.isArray(first)
|
|
736
|
-
? results
|
|
737
|
-
: Object.fromEntries(entries.map(([k], i) => [k, results[i]]));
|
|
738
|
-
}
|
|
739
|
-
catch (e) {
|
|
740
|
-
// The first rejection cancels the rest, then rethrows.
|
|
741
|
-
for (const f of frames)
|
|
742
|
-
f.signal.cancel("a sibling branch failed");
|
|
743
|
-
await Promise.allSettled(running);
|
|
744
|
-
frame.clock.join(frames.map((f) => f.clock));
|
|
745
|
-
throw e;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
// race: first to settle wins, and the losers are cancelled BY SEMANTICS, not by an API the
|
|
749
|
-
// program calls. A cancelled branch performs no new effects; an agent reply already in
|
|
750
|
-
// flight completes and is ignored, which is the documented answer rather than an accident.
|
|
751
|
-
const winner = await Promise.race(running.map((p, i) => p.then((value) => ({ index: entries[i]?.[0], value }))));
|
|
752
|
-
for (const f of frames)
|
|
753
|
-
f.signal.cancel("a sibling branch won the race");
|
|
754
|
-
await Promise.allSettled(running);
|
|
755
|
-
frame.clock.join(frames.map((f) => f.clock));
|
|
756
|
-
return winner;
|
|
757
|
-
}
|
|
758
|
-
if (name === "fanOut") {
|
|
759
|
-
const items = first;
|
|
760
|
-
const fn = (await this.evaluate(argNodes[1], env, frame));
|
|
761
|
-
const keyFn = this.option(bag, "key");
|
|
762
|
-
const branchKeys = [];
|
|
763
|
-
for (let i = 0; i < items.length; i += 1) {
|
|
764
|
-
const item = items[i];
|
|
765
|
-
let k;
|
|
766
|
-
if (keyFn !== undefined)
|
|
767
|
-
k = await keyFn(frame, [item]);
|
|
768
|
-
else if (item !== null && typeof item === "object" && typeof item.id === "string") {
|
|
769
|
-
k = item.id;
|
|
770
|
-
}
|
|
771
|
-
else {
|
|
772
|
-
throw new RuntimeFault("L3021", `fanOut needs a stable key: without one, a reordered or filtered list silently reshuffles every journal key underneath it. Pass { key: (item) => ... }, or give items a string id.`);
|
|
773
|
-
}
|
|
774
|
-
branchKeys.push(String(k));
|
|
853
|
+
const scopeKey = frame.keys.scopeKey(scopeKind, scopeName, occurrence);
|
|
854
|
+
// `conclave` is the one scope whose identity includes a SUBJECT (`hashesSubject`): the
|
|
855
|
+
// members are what the sub-team IS, so editing the member list has to diverge rather than
|
|
856
|
+
// resume into a different room. The other three are identified by kind, name and occurrence.
|
|
857
|
+
const subject = spec.hashesSubject
|
|
858
|
+
? {
|
|
859
|
+
members: first.map((m) => m.agent),
|
|
860
|
+
channel: this.option(bag, "channel") ?? null,
|
|
775
861
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
862
|
+
: undefined;
|
|
863
|
+
return await performScope(this.effects, scopeKey, frame, async (ctx, only) => await runScope(this.effects, name, scopeKind, scopeName, occurrence, first,
|
|
864
|
+
// Deferred, so the body is evaluated where it was evaluated before: after the scope's
|
|
865
|
+
// entry has begun, not at this call. `parallel` and `race` never ask for it.
|
|
866
|
+
async () => await this.evaluate(argNodes[1], env, frame), bag, frame, ctx, only), subject,
|
|
867
|
+
// `race` alone. `parallel` and `fanOut` have no losers — every branch is a winner and the
|
|
868
|
+
// walk enters all of them — and a `conclave` cannot be walked into at all, so a digest there
|
|
869
|
+
// would bind arms nothing was ever going to miss.
|
|
870
|
+
name === "race" ? this.branchDigester(argNodes[0]) : undefined);
|
|
785
871
|
}
|
|
786
872
|
// ---- statements --------------------------------------------------------------------------------
|
|
787
873
|
async executeBlock(block, env, frame) {
|
|
@@ -792,6 +878,17 @@ class Interpreter {
|
|
|
792
878
|
inner.declare(s.id.name, this.makeFunction(s, inner), false);
|
|
793
879
|
}
|
|
794
880
|
}
|
|
881
|
+
// `let`/`const` bind the whole block, holding the dead-zone marker until their line runs, so a
|
|
882
|
+
// closure called early finds "declared, not yet initialized" (L2004) rather than an outer
|
|
883
|
+
// binding of the same name — which is what JavaScript does, minus the host error class.
|
|
884
|
+
for (const s of body) {
|
|
885
|
+
if (s.type !== "VariableDeclaration")
|
|
886
|
+
continue;
|
|
887
|
+
for (const d of s.declarations ?? []) {
|
|
888
|
+
for (const n of declaredNames(d.id))
|
|
889
|
+
inner.declare(n, TDZ, s.kind === "let");
|
|
890
|
+
}
|
|
891
|
+
}
|
|
795
892
|
for (const s of body) {
|
|
796
893
|
const c = await this.execute(s, inner, frame);
|
|
797
894
|
if (c.type !== "normal")
|
|
@@ -808,10 +905,10 @@ class Interpreter {
|
|
|
808
905
|
await this.evaluate(node.expression, env, frame);
|
|
809
906
|
return NORMAL;
|
|
810
907
|
case "VariableDeclaration": {
|
|
811
|
-
const
|
|
908
|
+
const mode = node.kind === "let" ? "let" : "const";
|
|
812
909
|
for (const d of node.declarations) {
|
|
813
910
|
const init = d.init === null || d.init === undefined ? undefined : await this.evaluate(d.init, env, frame);
|
|
814
|
-
await this.bindPattern(d.id, init, env, frame,
|
|
911
|
+
await this.bindPattern(d.id, init, env, frame, mode);
|
|
815
912
|
}
|
|
816
913
|
return NORMAL;
|
|
817
914
|
}
|
|
@@ -837,9 +934,22 @@ class Interpreter {
|
|
|
837
934
|
return c;
|
|
838
935
|
}
|
|
839
936
|
case "ForStatement": {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
937
|
+
let loopEnv = new Env(env);
|
|
938
|
+
const init = node.init;
|
|
939
|
+
if (init !== null && init !== undefined) {
|
|
940
|
+
if (init.type === "VariableDeclaration")
|
|
941
|
+
await this.execute(init, loopEnv, frame);
|
|
942
|
+
else
|
|
943
|
+
await this.evaluate(init, loopEnv, frame);
|
|
944
|
+
}
|
|
945
|
+
// `for (let i ...)` gives EACH ITERATION its own `i`, as JavaScript does: a closure made in
|
|
946
|
+
// one iteration keeps that iteration's value. The copy happens after the body and before the
|
|
947
|
+
// update, which is where the specification puts it.
|
|
948
|
+
const perIteration = [];
|
|
949
|
+
if (init?.type === "VariableDeclaration" && init.kind === "let") {
|
|
950
|
+
for (const d of init.declarations)
|
|
951
|
+
collectNames(d.id, perIteration);
|
|
952
|
+
}
|
|
843
953
|
for (;;) {
|
|
844
954
|
if (node.test !== null && node.test !== undefined && !(await this.evaluate(node.test, loopEnv, frame))) {
|
|
845
955
|
return NORMAL;
|
|
@@ -849,17 +959,24 @@ class Interpreter {
|
|
|
849
959
|
return NORMAL;
|
|
850
960
|
if (c.type === "return")
|
|
851
961
|
return c;
|
|
962
|
+
if (perIteration.length > 0)
|
|
963
|
+
loopEnv = loopEnv.perIteration(perIteration);
|
|
852
964
|
if (node.update !== null && node.update !== undefined)
|
|
853
965
|
await this.evaluate(node.update, loopEnv, frame);
|
|
854
966
|
}
|
|
855
967
|
}
|
|
856
968
|
case "ForOfStatement": {
|
|
857
|
-
const iterable = (await this.evaluate(node.right, env, frame));
|
|
969
|
+
const iterable = this.spreadable(await this.evaluate(node.right, env, frame));
|
|
970
|
+
const decl = node.left;
|
|
858
971
|
for (const item of iterable) {
|
|
859
972
|
const loopEnv = new Env(env);
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
973
|
+
if (decl.type === "VariableDeclaration") {
|
|
974
|
+
const target = decl.declarations[0].id;
|
|
975
|
+
await this.bindPattern(target, item, loopEnv, frame, decl.kind === "let" ? "let" : "const");
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
await this.bindPattern(decl, item, loopEnv, frame, "assign");
|
|
979
|
+
}
|
|
863
980
|
const c = await this.execute(node.body, loopEnv, frame);
|
|
864
981
|
if (c.type === "break")
|
|
865
982
|
return NORMAL;
|
|
@@ -880,49 +997,108 @@ class Interpreter {
|
|
|
880
997
|
case "ThrowStatement":
|
|
881
998
|
throw await this.evaluate(node.argument, env, frame);
|
|
882
999
|
case "TryStatement": {
|
|
1000
|
+
// What a `catch` may not have: none of these is a program error, and none is this
|
|
1001
|
+
// program's to handle. Three kinds, six classes.
|
|
1002
|
+
//
|
|
1003
|
+
// A cancellation is the scope being unwound, and swallowing it would keep a branch
|
|
1004
|
+
// working after it lost a race.
|
|
1005
|
+
//
|
|
1006
|
+
// A durability failure is the JOURNAL refusing to record: the run losing its ability to
|
|
1007
|
+
// have a result at all. A program that catches one goes on performing effects against the
|
|
1008
|
+
// world with nothing recorded from the refusal onward, so those effects exist only in the
|
|
1009
|
+
// world and a resume performs them again. An unrecordable run must stop, and no `catch`
|
|
1010
|
+
// may decide otherwise.
|
|
1011
|
+
//
|
|
1012
|
+
// And a DIVERGENCE, or a migration walk's refusal to enter a scope, is the journal saying
|
|
1013
|
+
// this program is not the one that wrote it. Measured before this line existed: a resume
|
|
1014
|
+
// whose edited `sleep` diverged inside a `try` caught `{ code: "L4000", kind: "host" }`,
|
|
1015
|
+
// logged past it, and performed a NEW effect against the journal it had just diverged
|
|
1016
|
+
// from; a migration's dry walk would have reported the same program clean.
|
|
1017
|
+
//
|
|
1018
|
+
// AND `finally` IS BOUND BY THE SAME LAW. A finalizer runs on the way out, so an
|
|
1019
|
+
// unconditional one handed the program a landing past every class above: measured, a
|
|
1020
|
+
// `finally` performed a NEW effect after a RunReleased and after a store rejection, and a
|
|
1021
|
+
// `finally { throw ... }` REPLACED a divergence, which an outer catch then swallowed as an
|
|
1022
|
+
// ordinary error. An uncatchable fault now unwinds past the finalizer too: the run's
|
|
1023
|
+
// continuation is forfeit, and that includes its cleanup — the world-side recovery belongs
|
|
1024
|
+
// to the driver and the journal, not to the program that just lost the right to run.
|
|
1025
|
+
const uncatchable = (e) => e instanceof Cancelled ||
|
|
1026
|
+
e instanceof JournalAppendRejected ||
|
|
1027
|
+
e instanceof RunReleased ||
|
|
1028
|
+
e instanceof RunDivergence ||
|
|
1029
|
+
e instanceof ScopeBranchMissing ||
|
|
1030
|
+
e instanceof UnwalkableScope;
|
|
1031
|
+
// JavaScript's completion semantics, which the one-`try` shape this replaced could not
|
|
1032
|
+
// express (measured: `try { return 1; } finally { return 2; }` returned 1): the finalizer
|
|
1033
|
+
// always runs for ordinary completions, and an ABRUPT finalizer completion — a return, a
|
|
1034
|
+
// break, a throw — replaces whatever the try or catch had decided.
|
|
1035
|
+
let completion = NORMAL;
|
|
1036
|
+
let pendingThrow;
|
|
1037
|
+
let hasThrow = false;
|
|
883
1038
|
try {
|
|
884
|
-
|
|
885
|
-
if (c.type !== "normal")
|
|
886
|
-
return c;
|
|
1039
|
+
completion = await this.execute(node.block, env, frame);
|
|
887
1040
|
}
|
|
888
1041
|
catch (e) {
|
|
889
|
-
|
|
890
|
-
// block must not be able to swallow it and keep working in a branch that lost a race.
|
|
891
|
-
if (e instanceof Cancelled)
|
|
1042
|
+
if (uncatchable(e))
|
|
892
1043
|
throw e;
|
|
893
1044
|
const handlerNode = node.handler;
|
|
894
|
-
if (handlerNode === null || handlerNode === undefined)
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if (handlerNode.param !== null && handlerNode.param !== undefined) {
|
|
898
|
-
await this.bindPattern(handlerNode.param, toProgramError(e), catchEnv, frame, false);
|
|
1045
|
+
if (handlerNode === null || handlerNode === undefined) {
|
|
1046
|
+
hasThrow = true;
|
|
1047
|
+
pendingThrow = e;
|
|
899
1048
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1049
|
+
else {
|
|
1050
|
+
try {
|
|
1051
|
+
const catchEnv = new Env(env);
|
|
1052
|
+
if (handlerNode.param !== null && handlerNode.param !== undefined) {
|
|
1053
|
+
await this.bindPattern(handlerNode.param, toProgramError(e), catchEnv, frame, "const");
|
|
1054
|
+
}
|
|
1055
|
+
completion = await this.executeBlock(handlerNode.body, catchEnv, frame);
|
|
1056
|
+
}
|
|
1057
|
+
catch (ce) {
|
|
1058
|
+
if (uncatchable(ce))
|
|
1059
|
+
throw ce;
|
|
1060
|
+
hasThrow = true;
|
|
1061
|
+
pendingThrow = ce;
|
|
1062
|
+
}
|
|
907
1063
|
}
|
|
908
1064
|
}
|
|
909
|
-
|
|
1065
|
+
if (node.finalizer !== null && node.finalizer !== undefined) {
|
|
1066
|
+
// A throw inside the finalizer — its own, or an uncatchable — propagates from here,
|
|
1067
|
+
// replacing any pending completion, exactly as JavaScript replaces it.
|
|
1068
|
+
const f = await this.execute(node.finalizer, env, frame);
|
|
1069
|
+
if (f.type !== "normal")
|
|
1070
|
+
return f;
|
|
1071
|
+
}
|
|
1072
|
+
if (hasThrow)
|
|
1073
|
+
throw pendingThrow;
|
|
1074
|
+
return completion;
|
|
910
1075
|
}
|
|
911
1076
|
case "SwitchStatement": {
|
|
1077
|
+
// JavaScript's selection: the case tests are tried in source order, the `default` clause's
|
|
1078
|
+
// position is skipped during matching, and `default` is entered only when NO case matched.
|
|
1079
|
+
// The one-pass walk this replaced treated `default` as an immediate match, so a `default`
|
|
1080
|
+
// written above a matching case shadowed it (measured: `default` ran, `case 2` did not).
|
|
1081
|
+
// Execution then falls through from the selected clause in source order, `default`
|
|
1082
|
+
// included, exactly as JavaScript falls.
|
|
912
1083
|
const disc = await this.evaluate(node.discriminant, env, frame);
|
|
913
1084
|
const cases = node.cases;
|
|
914
1085
|
const switchEnv = new Env(env);
|
|
915
|
-
let
|
|
916
|
-
for (
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
matched = true;
|
|
920
|
-
else if (Object.is(await this.evaluate(c.test, switchEnv, frame), disc))
|
|
921
|
-
matched = true;
|
|
922
|
-
}
|
|
923
|
-
if (!matched)
|
|
1086
|
+
let start = -1;
|
|
1087
|
+
for (let i = 0; i < cases.length; i += 1) {
|
|
1088
|
+
const c = cases[i];
|
|
1089
|
+
if (c.test === null || c.test === undefined)
|
|
924
1090
|
continue;
|
|
925
|
-
|
|
1091
|
+
if ((await this.evaluate(c.test, switchEnv, frame)) === disc) {
|
|
1092
|
+
start = i;
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (start === -1)
|
|
1097
|
+
start = cases.findIndex((c) => c.test === null || c.test === undefined);
|
|
1098
|
+
if (start === -1)
|
|
1099
|
+
return NORMAL;
|
|
1100
|
+
for (let i = start; i < cases.length; i += 1) {
|
|
1101
|
+
for (const s of cases[i].consequent ?? []) {
|
|
926
1102
|
const comp = await this.execute(s, switchEnv, frame);
|
|
927
1103
|
if (comp.type === "break")
|
|
928
1104
|
return NORMAL;
|
|
@@ -940,46 +1116,154 @@ class Interpreter {
|
|
|
940
1116
|
}
|
|
941
1117
|
}
|
|
942
1118
|
// ---- helpers -------------------------------------------------------------------------------------
|
|
1119
|
+
/** The names a declaration's pattern introduces, for the dead-zone pre-pass. */
|
|
1120
|
+
function declaredNames(pattern) {
|
|
1121
|
+
const out = [];
|
|
1122
|
+
const walk = (n) => {
|
|
1123
|
+
if (n === null || n === undefined)
|
|
1124
|
+
return;
|
|
1125
|
+
switch (n.type) {
|
|
1126
|
+
case "Identifier":
|
|
1127
|
+
out.push(n.name);
|
|
1128
|
+
return;
|
|
1129
|
+
case "ObjectPattern":
|
|
1130
|
+
for (const p of n.properties ?? [])
|
|
1131
|
+
walk((p.type === "RestElement" ? p.argument : p.value));
|
|
1132
|
+
return;
|
|
1133
|
+
case "ArrayPattern":
|
|
1134
|
+
for (const el of n.elements ?? [])
|
|
1135
|
+
walk(el);
|
|
1136
|
+
return;
|
|
1137
|
+
case "AssignmentPattern":
|
|
1138
|
+
walk(n.left);
|
|
1139
|
+
return;
|
|
1140
|
+
case "RestElement":
|
|
1141
|
+
walk(n.argument);
|
|
1142
|
+
return;
|
|
1143
|
+
default:
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
walk(pattern);
|
|
1148
|
+
return out;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* The binary operators, with JavaScript's meaning ON PRIMITIVES. `"a" + 1`, `true + 1` and
|
|
1152
|
+
* `null + 1` mean here exactly what they mean in JavaScript — primitive coercion is pure and
|
|
1153
|
+
* deterministic. A record, an array or a function operand is refused (L4018), a declared
|
|
1154
|
+
* difference: JavaScript would reach for the host's ToPrimitive machinery, which reads `valueOf`/
|
|
1155
|
+
* `toString` off the value — own fields a program can set to its OWN closures. Measured before the
|
|
1156
|
+
* refusal: `o + 1` invoked such a closure without an interpreter frame and crashed with a raw host
|
|
1157
|
+
* TypeError, and without one it silently produced `"[object Object]1"`. `==` and `!=` never reach
|
|
1158
|
+
* this function: the validator refuses them (L1025). `===`/`!==` compare identity and take any
|
|
1159
|
+
* operands.
|
|
1160
|
+
*/
|
|
1161
|
+
/** Refuse a container or function where a primitive is needed: there is no implicit conversion. */
|
|
1162
|
+
function refuseCoercion(where, v) {
|
|
1163
|
+
if (v !== null && (typeof v === "object" || typeof v === "function")) {
|
|
1164
|
+
const kind = typeof v === "function" ? "a function" : Array.isArray(v) ? "an array" : "a record";
|
|
1165
|
+
throw new RuntimeFault("L4018", `\`${where}\` cannot take ${kind}: there is no implicit conversion here, because converting would read \`valueOf\`/\`toString\` off the value — host machinery this language does not have. Convert explicitly: \`json.stringify(value)\` for text, or read the field you mean.`);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
943
1168
|
function applyBinary(op, l, r) {
|
|
1169
|
+
const a = l;
|
|
1170
|
+
const b = r;
|
|
944
1171
|
switch (op) {
|
|
945
1172
|
case "===":
|
|
946
|
-
return
|
|
1173
|
+
return l === r;
|
|
947
1174
|
case "!==":
|
|
948
|
-
return
|
|
1175
|
+
return l !== r;
|
|
1176
|
+
default:
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
refuseCoercion(op, l);
|
|
1180
|
+
refuseCoercion(op, r);
|
|
1181
|
+
switch (op) {
|
|
949
1182
|
case "<":
|
|
950
|
-
return
|
|
1183
|
+
return a < b;
|
|
951
1184
|
case "<=":
|
|
952
|
-
return
|
|
1185
|
+
return a <= b;
|
|
953
1186
|
case ">":
|
|
954
|
-
return
|
|
1187
|
+
return a > b;
|
|
955
1188
|
case ">=":
|
|
956
|
-
return
|
|
1189
|
+
return a >= b;
|
|
957
1190
|
case "+":
|
|
958
|
-
return
|
|
959
|
-
? String(l) + String(r)
|
|
960
|
-
: l + r;
|
|
1191
|
+
return a + b;
|
|
961
1192
|
case "-":
|
|
962
|
-
return
|
|
1193
|
+
return a - b;
|
|
963
1194
|
case "*":
|
|
964
|
-
return
|
|
1195
|
+
return a * b;
|
|
965
1196
|
case "/":
|
|
966
|
-
return
|
|
1197
|
+
return a / b;
|
|
967
1198
|
case "%":
|
|
968
|
-
return
|
|
1199
|
+
return a % b;
|
|
1200
|
+
case "**":
|
|
1201
|
+
return a ** b;
|
|
1202
|
+
case "&":
|
|
1203
|
+
return a & b;
|
|
1204
|
+
case "|":
|
|
1205
|
+
return a | b;
|
|
1206
|
+
case "^":
|
|
1207
|
+
return a ^ b;
|
|
1208
|
+
case "<<":
|
|
1209
|
+
return a << b;
|
|
1210
|
+
case ">>":
|
|
1211
|
+
return a >> b;
|
|
1212
|
+
case ">>>":
|
|
1213
|
+
return a >>> b;
|
|
969
1214
|
default:
|
|
970
1215
|
throw new RuntimeFault("L1000", `unsupported operator ${op}`);
|
|
971
1216
|
}
|
|
972
1217
|
}
|
|
973
|
-
/**
|
|
1218
|
+
/** A canonical array index (`"0"`, `"12"`), as a number, or nothing. */
|
|
1219
|
+
function arrayIndex(prop) {
|
|
1220
|
+
if (!/^(0|[1-9][0-9]*)$/.test(prop))
|
|
1221
|
+
return undefined;
|
|
1222
|
+
const n = Number(prop);
|
|
1223
|
+
return n <= 4294967294 ? n : undefined;
|
|
1224
|
+
}
|
|
1225
|
+
/** The names a binding pattern introduces. */
|
|
1226
|
+
function collectNames(pattern, out) {
|
|
1227
|
+
switch (pattern.type) {
|
|
1228
|
+
case "Identifier":
|
|
1229
|
+
out.push(pattern.name);
|
|
1230
|
+
return;
|
|
1231
|
+
case "AssignmentPattern":
|
|
1232
|
+
collectNames(pattern.left, out);
|
|
1233
|
+
return;
|
|
1234
|
+
case "RestElement":
|
|
1235
|
+
collectNames(pattern.argument, out);
|
|
1236
|
+
return;
|
|
1237
|
+
case "ObjectPattern":
|
|
1238
|
+
for (const p of pattern.properties)
|
|
1239
|
+
collectNames((p.type === "RestElement" ? p.argument : p.value), out);
|
|
1240
|
+
return;
|
|
1241
|
+
case "ArrayPattern":
|
|
1242
|
+
for (const el of pattern.elements)
|
|
1243
|
+
if (el !== null && el !== undefined)
|
|
1244
|
+
collectNames(el, out);
|
|
1245
|
+
return;
|
|
1246
|
+
default:
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
/** The optional-chain short-circuit. Private to `evaluate`; see the `ChainExpression` case. */
|
|
1251
|
+
const SHORT_CIRCUIT = Symbol("cotal-lang short circuit");
|
|
1252
|
+
/**
|
|
1253
|
+
* What a `catch` block sees. A failure the RUNTIME raised (an effect's error, an interpreter fault)
|
|
1254
|
+
* arrives as a plain record carrying its code, because programs branch on data, not on classes; a
|
|
1255
|
+
* value the PROGRAM threw arrives as itself, whatever it is, exactly as JavaScript delivers it. A
|
|
1256
|
+
* program cannot construct an `Error`, so anything that is one came from the runtime or the host.
|
|
1257
|
+
*/
|
|
974
1258
|
function toProgramError(e) {
|
|
975
1259
|
if (e instanceof EffectError) {
|
|
976
1260
|
return deepFreeze({ code: e.code, kind: e.kind, message: e.message, ...(e.detail !== undefined ? { detail: e.detail } : {}) });
|
|
977
1261
|
}
|
|
978
1262
|
if (e instanceof RuntimeFault)
|
|
979
1263
|
return deepFreeze({ code: e.code, kind: "runtime", message: e.message });
|
|
980
|
-
if (e
|
|
981
|
-
return e;
|
|
982
|
-
return
|
|
1264
|
+
if (e instanceof Error)
|
|
1265
|
+
return deepFreeze({ code: "L4000", kind: "host", message: e.message });
|
|
1266
|
+
return e;
|
|
983
1267
|
}
|
|
984
1268
|
// ---- the public entry point -------------------------------------------------------------------------
|
|
985
1269
|
/**
|
|
@@ -990,16 +1274,41 @@ function toProgramError(e) {
|
|
|
990
1274
|
*/
|
|
991
1275
|
export async function run(source, options) {
|
|
992
1276
|
const { ast } = validate(source, options.file);
|
|
993
|
-
const programHash =
|
|
994
|
-
|
|
995
|
-
|
|
1277
|
+
const programHash = programHashOf(source);
|
|
1278
|
+
// A resume is handed the pins the run STARTED under and binds to them; a fresh run resolves them
|
|
1279
|
+
// once, here, and hands them back for the run record.
|
|
1280
|
+
//
|
|
1281
|
+
// AND A RESUME MAY NOT DECLINE TO SAY WHICH RUN IT IS RESUMING. Re-resolving the pins for a run
|
|
1282
|
+
// handed history but none is not a smaller version of the right behaviour: it is a different run
|
|
1283
|
+
// wearing the same journal. The clock moves to the RESUMING host and the seed falls back to the
|
|
1284
|
+
// runId default, so both the logical epoch and every pure draw change, and nothing refuses,
|
|
1285
|
+
// because nothing can. Pure draws are not journalled and the epoch is not a recorded fact, so
|
|
1286
|
+
// there is no divergence for the replay to catch.
|
|
1287
|
+
//
|
|
1288
|
+
// A journal with NO entries is a different thing and stays allowed: that is a FRESH run being
|
|
1289
|
+
// handed a journal for its store, not a resume.
|
|
1290
|
+
if (options.pins === undefined && options.journal !== undefined && options.journal.entries().length > 0) {
|
|
1291
|
+
throw new RuntimeFault("L5021", `run ${options.runId} was handed a journal with ${options.journal.entries().length} recorded step(s) but no pins. The pins are what decide `
|
|
1292
|
+
+ `the run's logical epoch and its seed, so resolving them again here would make this a different run against a journal that was not `
|
|
1293
|
+
+ `written for it — silently, because neither the clock nor a pure draw is a recorded fact the replay could diverge on.\n\n`
|
|
1294
|
+
+ `Options\n pass the pins from the run record\n start a fresh run instead of resuming this journal`);
|
|
1295
|
+
}
|
|
1296
|
+
const pins = options.pins !== undefined
|
|
1297
|
+
? bindPins(options.pins, options, WALKER_LANGUAGE_VERSION)
|
|
1298
|
+
: resolvePins(options, options.handler.now(), WALKER_LANGUAGE_VERSION);
|
|
1299
|
+
const interp = new Interpreter(ast, options, programHash, pins);
|
|
1300
|
+
// The run clock starts at the run's LOGICAL epoch, not at this host's clock: a run resumed on
|
|
1301
|
+
// another machine hours later must see the same `now()` before its first effect as the run that
|
|
1302
|
+
// wrote the journal, or the branch it takes is a property of when it was resumed.
|
|
1303
|
+
const frame = new Frame(new KeyScope(), new RunClock(pins.startedAt), new Signal());
|
|
996
1304
|
const env = new Env(null);
|
|
997
|
-
installGlobals(env, interp
|
|
1305
|
+
installGlobals(env, interp);
|
|
998
1306
|
const completion = await interp.executeBlock(ast, env, frame);
|
|
999
1307
|
return {
|
|
1000
1308
|
value: completion.type === "return" ? completion.value : undefined,
|
|
1001
1309
|
journal: interp.journal,
|
|
1002
1310
|
programHash,
|
|
1311
|
+
pins,
|
|
1003
1312
|
steps: interp.stepCount,
|
|
1004
1313
|
};
|
|
1005
1314
|
}
|
|
@@ -1008,110 +1317,19 @@ export async function resume(source, journal, options) {
|
|
|
1008
1317
|
journal.resetConsumed();
|
|
1009
1318
|
return await run(source, { ...options, journal });
|
|
1010
1319
|
}
|
|
1011
|
-
function installGlobals(env, interp
|
|
1320
|
+
function installGlobals(env, interp) {
|
|
1012
1321
|
const fn = (impl) => async (frame, args) => impl(frame, args);
|
|
1013
|
-
//
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
//
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
...(opts.from !== undefined ? { from: opts.from.agent } : {}),
|
|
1025
|
-
...(opts.matches !== undefined ? { matches: opts.matches } : {}),
|
|
1026
|
-
};
|
|
1027
|
-
}), false);
|
|
1028
|
-
env.declare("idle", fn((_f, a) => ({ event: "idle", channel: a[0].channel, duration: a[1] })), false);
|
|
1029
|
-
env.declare("down", fn((_f, a) => ({ event: "down", agent: a[0].agent })), false);
|
|
1030
|
-
// Time and randomness, tamed. `now()` reads the branch's own run clock, which is the maximum
|
|
1031
|
-
// endedAt over the effects this point actually awaited: time advances at effect boundaries as a
|
|
1032
|
-
// property of the design rather than a rule anyone has to follow.
|
|
1033
|
-
env.declare("now", fn((frame) => frame.clock.now()), false);
|
|
1034
|
-
env.declare("random", fn((frame) => interp.prng.next(frame.keys.path)), false);
|
|
1035
|
-
env.declare("randomInt", fn((frame, a) => Math.floor(interp.prng.next(frame.keys.path) * a[0])), false);
|
|
1036
|
-
env.declare("pick", fn((frame, a) => {
|
|
1037
|
-
const list = a[0];
|
|
1038
|
-
return list[Math.floor(interp.prng.next(frame.keys.path) * list.length)];
|
|
1039
|
-
}), false);
|
|
1040
|
-
env.declare("duration", fn((_f, a) => parseDuration(a[0])), false);
|
|
1041
|
-
// Records and arrays. Iteration order is insertion order, which is deterministic; sorting for
|
|
1042
|
-
// a hash is a separate concern and happens in canonicalization, not here.
|
|
1043
|
-
env.declare("keys", fn((_f, a) => Object.keys(a[0])), false);
|
|
1044
|
-
env.declare("values", fn((_f, a) => Object.values(a[0])), false);
|
|
1045
|
-
env.declare("entries", fn((_f, a) => Object.entries(a[0])), false);
|
|
1046
|
-
env.declare("has", fn((_f, a) => Object.prototype.hasOwnProperty.call(a[0], a[1])), false);
|
|
1047
|
-
env.declare("merge", fn((_f, a) => ({ ...a[0], ...a[1] })), false);
|
|
1048
|
-
env.declare("len", fn((_f, a) => a[0].length), false);
|
|
1049
|
-
env.declare("range", fn((_f, a) => Array.from({ length: a[0] }, (_, i) => i)), false);
|
|
1050
|
-
env.declare("sum", fn((_f, a) => a[0].reduce((x, y) => x + y, 0)), false);
|
|
1051
|
-
env.declare("concat", fn((_f, a) => a[0].concat(a[1])), false);
|
|
1052
|
-
env.declare("slice", fn((_f, a) => a[0].slice(a[1], a[2])), false);
|
|
1053
|
-
env.declare("reverse", fn((_f, a) => [...a[0]].reverse()), false);
|
|
1054
|
-
env.declare("unique", fn((_f, a) => [...new Set(a[0])]), false);
|
|
1055
|
-
env.declare("join", fn((_f, a) => a[0].join(a[1])), false);
|
|
1056
|
-
// Higher-order builtins take an interpreter function, so they have to await it.
|
|
1057
|
-
const higher = (impl) => async (frame, args) => await impl(frame, args[0], args[1]);
|
|
1058
|
-
env.declare("map", higher(async (frame, list, f) => {
|
|
1059
|
-
const out = [];
|
|
1060
|
-
for (let i = 0; i < list.length; i += 1)
|
|
1061
|
-
out.push(await f(frame, [list[i], i]));
|
|
1062
|
-
return out;
|
|
1063
|
-
}), false);
|
|
1064
|
-
env.declare("filter", higher(async (frame, list, f) => {
|
|
1065
|
-
const out = [];
|
|
1066
|
-
for (let i = 0; i < list.length; i += 1)
|
|
1067
|
-
if (await f(frame, [list[i], i]))
|
|
1068
|
-
out.push(list[i]);
|
|
1069
|
-
return out;
|
|
1070
|
-
}), false);
|
|
1071
|
-
env.declare("find", higher(async (frame, list, f) => {
|
|
1072
|
-
for (let i = 0; i < list.length; i += 1)
|
|
1073
|
-
if (await f(frame, [list[i], i]))
|
|
1074
|
-
return list[i];
|
|
1075
|
-
return null;
|
|
1076
|
-
}), false);
|
|
1077
|
-
env.declare("some", higher(async (frame, list, f) => {
|
|
1078
|
-
for (let i = 0; i < list.length; i += 1)
|
|
1079
|
-
if (await f(frame, [list[i], i]))
|
|
1080
|
-
return true;
|
|
1081
|
-
return false;
|
|
1082
|
-
}), false);
|
|
1083
|
-
env.declare("every", higher(async (frame, list, f) => {
|
|
1084
|
-
for (let i = 0; i < list.length; i += 1)
|
|
1085
|
-
if (!(await f(frame, [list[i], i])))
|
|
1086
|
-
return false;
|
|
1087
|
-
return true;
|
|
1088
|
-
}), false);
|
|
1089
|
-
// Strings and numbers.
|
|
1090
|
-
env.declare("split", fn((_f, a) => a[0].split(a[1])), false);
|
|
1091
|
-
env.declare("trim", fn((_f, a) => a[0].trim()), false);
|
|
1092
|
-
env.declare("lower", fn((_f, a) => a[0].toLowerCase()), false);
|
|
1093
|
-
env.declare("upper", fn((_f, a) => a[0].toUpperCase()), false);
|
|
1094
|
-
env.declare("startsWith", fn((_f, a) => a[0].startsWith(a[1])), false);
|
|
1095
|
-
env.declare("endsWith", fn((_f, a) => a[0].endsWith(a[1])), false);
|
|
1096
|
-
env.declare("contains", fn((_f, a) => a[0].includes(a[1])), false);
|
|
1097
|
-
env.declare("replace", fn((_f, a) => a[0].split(a[1]).join(a[2])), false);
|
|
1098
|
-
env.declare("min", fn((_f, a) => Math.min(...a)), false);
|
|
1099
|
-
env.declare("max", fn((_f, a) => Math.max(...a)), false);
|
|
1100
|
-
env.declare("abs", fn((_f, a) => Math.abs(a[0])), false);
|
|
1101
|
-
env.declare("floor", fn((_f, a) => Math.floor(a[0])), false);
|
|
1102
|
-
env.declare("ceil", fn((_f, a) => Math.ceil(a[0])), false);
|
|
1103
|
-
env.declare("round", fn((_f, a) => Math.round(a[0])), false);
|
|
1104
|
-
env.declare("parseNumber", fn((_f, a) => Number(a[0])), false);
|
|
1105
|
-
env.declare("assert", fn((_f, a) => {
|
|
1106
|
-
if (!a[0])
|
|
1107
|
-
throw new RuntimeFault("L4012", String(a[1] ?? "assertion failed"));
|
|
1108
|
-
return null;
|
|
1109
|
-
}), false);
|
|
1110
|
-
env.declare("log", fn((frame, a) => {
|
|
1111
|
-
interp.options.onLog?.({ scope: scopePathString(frame.keys.path), values: a });
|
|
1112
|
-
return null;
|
|
1113
|
-
}), false);
|
|
1114
|
-
void rootFrame;
|
|
1322
|
+
// The value names. `undefined` is a value the runtime produces, so a program can name it.
|
|
1323
|
+
for (const name of VALUE_NAMES)
|
|
1324
|
+
env.declare(name, undefined, false);
|
|
1325
|
+
// Pure primitives and event constructors: ONE shared table (perform.ts), wrapped into the
|
|
1326
|
+
// walker's calling convention here. Handles are opaque frozen records the runtime mints
|
|
1327
|
+
// (design §4); event constructors are pure descriptors, and awaiting one is `wait`.
|
|
1328
|
+
for (const [name, impl] of freeConstructors({ runId: interp.options.runId, programHash: interp.programHash, startedAt: interp.pins.startedAt }))
|
|
1329
|
+
env.declare(name, fn((_f, a) => impl(a)), false);
|
|
1330
|
+
// The builtin library (design §4), one table in library.ts.
|
|
1331
|
+
for (const [name, value] of builtins(interp.libraryContext()))
|
|
1332
|
+
env.declare(name, value, false);
|
|
1115
1333
|
}
|
|
1116
1334
|
export { LangError, LangErrors };
|
|
1117
1335
|
//# sourceMappingURL=interpret.js.map
|