@cotal-ai/lang 0.22.0 → 0.24.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 +57 -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/errors.d.ts +48 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +90 -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 +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/interpret.d.ts +81 -3
- package/dist/interpret.d.ts.map +1 -1
- package/dist/interpret.js +1446 -270
- 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 +232 -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 +525 -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/pins.d.ts +82 -0
- package/dist/pins.d.ts.map +1 -0
- package/dist/pins.js +87 -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 +9 -1
- 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/values.d.ts +20 -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 +2 -2
package/dist/interpret.js
CHANGED
|
@@ -15,13 +15,32 @@
|
|
|
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, RuntimeFault, } from "./errors.js";
|
|
19
|
+
export { RuntimeFault } 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 } from "./pins.js";
|
|
28
|
+
import { Cancelled, RunReleased, EffectError, applyCheckpointPolicy, } from "./effects.js";
|
|
29
|
+
/**
|
|
30
|
+
* A `conclave` whose close did not acknowledge.
|
|
31
|
+
*
|
|
32
|
+
* It exists so the scope is NOT settled: the pending entry is the durable record that a close is
|
|
33
|
+
* still owed, and re-entry retries it. Settling on a close rejection would have the journal state a
|
|
34
|
+
* disposition the world never confirmed, which is the one thing this entry is for.
|
|
35
|
+
*/
|
|
36
|
+
class CloseOwed extends Error {
|
|
37
|
+
reason;
|
|
38
|
+
constructor(reason) {
|
|
39
|
+
super(`conclave close did not acknowledge: ${reason?.message ?? String(reason)}`);
|
|
40
|
+
this.reason = reason;
|
|
41
|
+
this.name = "CloseOwed";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
25
44
|
// ---- environments ------------------------------------------------------------------------------
|
|
26
45
|
class Binding {
|
|
27
46
|
value;
|
|
@@ -31,15 +50,49 @@ class Binding {
|
|
|
31
50
|
this.mutable = mutable;
|
|
32
51
|
}
|
|
33
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The value a `let`/`const` binding holds between the top of its block and its declaration: the
|
|
55
|
+
* temporal dead zone, materialized. The validator refuses every straight-line reference into it
|
|
56
|
+
* (L2004), so the only way here at run time is a function called before the declaration executed —
|
|
57
|
+
* which JavaScript answers with a ReferenceError, and this language answers with the same code the
|
|
58
|
+
* static refusal carries.
|
|
59
|
+
*/
|
|
60
|
+
const TDZ = Symbol("cotal-lang temporal dead zone");
|
|
34
61
|
class Env {
|
|
35
62
|
parent;
|
|
63
|
+
depth;
|
|
36
64
|
names = new Map();
|
|
37
|
-
|
|
65
|
+
/**
|
|
66
|
+
* How many CONCURRENT scopes deep this environment was created.
|
|
67
|
+
*
|
|
68
|
+
* L2032's runtime half rests on this. The static rule follows named and inline branches, but a
|
|
69
|
+
* branch the validator cannot resolve to a function node — one that arrives through a parameter
|
|
70
|
+
* or a computed record — is not proven, and banning that shape outright would cost more than the
|
|
71
|
+
* hazard. So the depth travels with the binding: a write from inside a concurrent branch to a
|
|
72
|
+
* binding declared OUTSIDE it is refused where it happens. `conclave` does not raise the depth,
|
|
73
|
+
* because its single body has nothing to race.
|
|
74
|
+
*/
|
|
75
|
+
constructor(parent, depth = parent?.depth ?? 0) {
|
|
38
76
|
this.parent = parent;
|
|
77
|
+
this.depth = depth;
|
|
39
78
|
}
|
|
40
79
|
declare(name, value, mutable) {
|
|
41
80
|
this.names.set(name, new Binding(value, mutable));
|
|
42
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* A fresh environment holding copies of `names` at their current values: JavaScript's
|
|
84
|
+
* per-iteration bindings for a `for (let ...)` loop, so a closure made in one iteration keeps
|
|
85
|
+
* that iteration's value rather than watching the counter move.
|
|
86
|
+
*/
|
|
87
|
+
perIteration(names) {
|
|
88
|
+
const next = new Env(this.parent, this.depth);
|
|
89
|
+
for (const n of names) {
|
|
90
|
+
const b = this.names.get(n);
|
|
91
|
+
if (b !== undefined)
|
|
92
|
+
next.declare(n, b.value, b.mutable);
|
|
93
|
+
}
|
|
94
|
+
return next;
|
|
95
|
+
}
|
|
43
96
|
find(name) {
|
|
44
97
|
for (let e = this; e !== null; e = e.parent) {
|
|
45
98
|
const b = e.names.get(name);
|
|
@@ -48,56 +101,163 @@ class Env {
|
|
|
48
101
|
}
|
|
49
102
|
return undefined;
|
|
50
103
|
}
|
|
104
|
+
owner(name) {
|
|
105
|
+
for (let e = this; e !== null; e = e.parent) {
|
|
106
|
+
if (e.names.has(name))
|
|
107
|
+
return e;
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
51
111
|
get(name) {
|
|
52
112
|
const b = this.find(name);
|
|
53
113
|
if (b === undefined)
|
|
54
114
|
throw new RuntimeFault("L2001", `${name} is not defined`);
|
|
115
|
+
if (b.value === TDZ) {
|
|
116
|
+
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.`);
|
|
117
|
+
}
|
|
55
118
|
return b.value;
|
|
56
119
|
}
|
|
57
120
|
has(name) {
|
|
58
121
|
return this.find(name) !== undefined;
|
|
59
122
|
}
|
|
60
|
-
set(name, value) {
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
123
|
+
set(name, value, atDepth) {
|
|
124
|
+
const owner = this.owner(name);
|
|
125
|
+
if (owner === undefined)
|
|
63
126
|
throw new RuntimeFault("L2001", `${name} is not defined`);
|
|
127
|
+
const b = owner.names.get(name);
|
|
128
|
+
if (b.value === TDZ) {
|
|
129
|
+
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.`);
|
|
130
|
+
}
|
|
64
131
|
if (!b.mutable)
|
|
65
132
|
throw new RuntimeFault("L2003", `${name} is declared const`);
|
|
133
|
+
if (owner.depth < atDepth) {
|
|
134
|
+
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.`);
|
|
135
|
+
}
|
|
66
136
|
b.value = value;
|
|
67
137
|
}
|
|
68
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* A scope's failure, carrying the interpreter's OWN facts about it.
|
|
141
|
+
*
|
|
142
|
+
* Attaching them to the thrown value with `Object.assign` works exactly as long as every program
|
|
143
|
+
* throws an object. `throw null` is valid, and `Object.assign(null, …)` is a TypeError, so a
|
|
144
|
+
* conclave whose body throws a primitive loses its closure fact AND hands the caller a manufactured
|
|
145
|
+
* type error in place of the body's failure, while the entry records
|
|
146
|
+
* `closed: undefined` for a room the handler had in fact closed. The facts belong to the
|
|
147
|
+
* interpreter, so they travel in the interpreter's own envelope and the program's value rides
|
|
148
|
+
* untouched inside it. Nothing outside `performScope` ever sees this class: it unwraps before it
|
|
149
|
+
* rethrows.
|
|
150
|
+
*/
|
|
151
|
+
class ScopeFailed extends Error {
|
|
152
|
+
reason;
|
|
153
|
+
facts;
|
|
154
|
+
constructor(reason, facts) {
|
|
155
|
+
super(`scope failed: ${messageOf(reason)}`);
|
|
156
|
+
this.reason = reason;
|
|
157
|
+
this.facts = facts;
|
|
158
|
+
this.name = "ScopeFailed";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function unwrapScope(e) {
|
|
162
|
+
return e instanceof ScopeFailed ? { reason: e.reason, facts: e.facts } : { reason: e, facts: {} };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The message of an arbitrary thrown value.
|
|
166
|
+
*
|
|
167
|
+
* Reading `.message` off `null` throws, and a thrown primitive is legal in a language with `throw`,
|
|
168
|
+
* so every place that has to describe a failure it did not construct goes through here. A recorded
|
|
169
|
+
* entry saying "Cannot read properties of null" describes the recorder, not the run.
|
|
170
|
+
*/
|
|
171
|
+
/**
|
|
172
|
+
* The digest fact, written wherever the loser set is — a race that FAILED owes its losers exactly
|
|
173
|
+
* as a winning one does, so it carries the digest too, and `replay-failed` compares it.
|
|
174
|
+
*/
|
|
175
|
+
function digestFacts(of, losers) {
|
|
176
|
+
if (of === undefined || losers === undefined)
|
|
177
|
+
return {};
|
|
178
|
+
const d = of(losers);
|
|
179
|
+
return d === undefined ? {} : { branchDigest: d };
|
|
180
|
+
}
|
|
181
|
+
/** An AST subtree with its source offsets removed: what the code IS, not where it sits. */
|
|
182
|
+
function stripPositions(node) {
|
|
183
|
+
if (Array.isArray(node))
|
|
184
|
+
return node.map(stripPositions);
|
|
185
|
+
if (node === null || typeof node !== "object")
|
|
186
|
+
return node;
|
|
187
|
+
const out = {};
|
|
188
|
+
for (const [k, v] of Object.entries(node)) {
|
|
189
|
+
if (k === "start" || k === "end" || k === "loc" || k === "range")
|
|
190
|
+
continue;
|
|
191
|
+
out[k] = stripPositions(v);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
function messageOf(v) {
|
|
196
|
+
if (v instanceof Error)
|
|
197
|
+
return v.message;
|
|
198
|
+
const m = v?.message;
|
|
199
|
+
return typeof m === "string" ? m : String(v);
|
|
200
|
+
}
|
|
69
201
|
/** A fault the interpreter itself raises, as opposed to one an effect handler reported. */
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
202
|
+
/**
|
|
203
|
+
* A migration's walk reached a settled scope it cannot enter.
|
|
204
|
+
*
|
|
205
|
+
* A `conclave` is the case that exists today: its channel handle is HANDLER-DERIVED — the mint
|
|
206
|
+
* returns it and nothing journals it — so a walk cannot re-enter the body without inventing a
|
|
207
|
+
* handle, and an invented one would re-hash every step inside that used the channel into a
|
|
208
|
+
* divergence the run never had. Refusing is the honest exit. Consuming the subtree instead would
|
|
209
|
+
* hide exactly the orphans a migration exists to find, which is a silent wrong answer in place of
|
|
210
|
+
* a loud refusal.
|
|
211
|
+
*/
|
|
212
|
+
export class UnwalkableScope extends Error {
|
|
213
|
+
scopeKey;
|
|
214
|
+
why;
|
|
215
|
+
constructor(scopeKey, why) {
|
|
216
|
+
super(`a migration cannot walk inside the settled ${why} at ${scopeKey}: its handle is handler-derived and was never journalled, so the walk would have to invent one. Fork from this step instead, or migrate a run that does not contain it.`);
|
|
217
|
+
this.scopeKey = scopeKey;
|
|
218
|
+
this.why = why;
|
|
219
|
+
this.name = "UnwalkableScope";
|
|
76
220
|
}
|
|
77
221
|
}
|
|
78
222
|
const NORMAL = { type: "normal" };
|
|
79
223
|
// ---- per-branch execution state ---------------------------------------------------------------------
|
|
224
|
+
/**
|
|
225
|
+
* A branch's cancellation, in two degrees.
|
|
226
|
+
*
|
|
227
|
+
* `cancelled` is the cancellation LAW: a cancelled branch performs no new effect, and every effect
|
|
228
|
+
* boundary refuses it. `cutPure` is the stronger cut a scope applies to an arm that CANNOT WIN any
|
|
229
|
+
* more: its pure work is also abandoned, at the next yield. An arm that could still win keeps
|
|
230
|
+
* running its pure work to a settle, because cutting it there would let the scheduler, and through
|
|
231
|
+
* it the `yieldEvery` pin, decide a race the recorded clocks should decide (see `runScope`).
|
|
232
|
+
* Both degrees flow to child signals, and a signal already cancelled softly can be escalated.
|
|
233
|
+
*/
|
|
80
234
|
class Signal {
|
|
81
235
|
cancelled = false;
|
|
236
|
+
cutPure = false;
|
|
82
237
|
reason;
|
|
83
238
|
listeners = [];
|
|
84
239
|
onCancel(fn) {
|
|
85
240
|
this.listeners.push(fn);
|
|
86
241
|
}
|
|
87
|
-
cancel(reason) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
242
|
+
cancel(reason, opts) {
|
|
243
|
+
const cut = opts?.cutPure ?? true;
|
|
244
|
+
const first = !this.cancelled;
|
|
245
|
+
if (first) {
|
|
246
|
+
this.cancelled = true;
|
|
247
|
+
this.reason = reason;
|
|
248
|
+
}
|
|
249
|
+
const escalated = cut && !this.cutPure;
|
|
250
|
+
if (escalated)
|
|
251
|
+
this.cutPure = true;
|
|
252
|
+
if (first || escalated)
|
|
253
|
+
for (const l of this.listeners)
|
|
254
|
+
l(reason, this.cutPure);
|
|
94
255
|
}
|
|
95
256
|
child() {
|
|
96
257
|
const s = new Signal();
|
|
97
258
|
if (this.cancelled)
|
|
98
|
-
s.cancel(this.reason ?? "parent cancelled");
|
|
99
|
-
|
|
100
|
-
this.onCancel((r) => s.cancel(r));
|
|
259
|
+
s.cancel(this.reason ?? "parent cancelled", { cutPure: this.cutPure });
|
|
260
|
+
this.onCancel((r, cut) => s.cancel(r, { cutPure: cut }));
|
|
101
261
|
return s;
|
|
102
262
|
}
|
|
103
263
|
}
|
|
@@ -111,13 +271,20 @@ class Frame {
|
|
|
111
271
|
keys;
|
|
112
272
|
clock;
|
|
113
273
|
signal;
|
|
114
|
-
|
|
274
|
+
depth;
|
|
275
|
+
constructor(keys, clock, signal,
|
|
276
|
+
/** How many CONCURRENT scopes deep. See {@link Env.depth}: this is L2032's runtime half. */
|
|
277
|
+
depth = 0) {
|
|
115
278
|
this.keys = keys;
|
|
116
279
|
this.clock = clock;
|
|
117
280
|
this.signal = signal;
|
|
281
|
+
this.depth = depth;
|
|
118
282
|
}
|
|
119
283
|
branch(kind, name, occurrence, branchKey) {
|
|
120
|
-
return new Frame(this.keys.branch(kind, name, occurrence, branchKey), this.clock.fork(), this.signal.child()
|
|
284
|
+
return new Frame(this.keys.branch(kind, name, occurrence, branchKey), this.clock.fork(), this.signal.child(),
|
|
285
|
+
// `conclave` opens a scope but not a RACE: one body, nothing running beside it, so a write
|
|
286
|
+
// from inside it is as ordered as a write anywhere else and the depth does not move.
|
|
287
|
+
kind === "conclave" ? this.depth : this.depth + 1);
|
|
121
288
|
}
|
|
122
289
|
}
|
|
123
290
|
/** A recorded step's inputs changed, so its recorded result may no longer be the truth. */
|
|
@@ -133,28 +300,225 @@ export class RunDivergence extends Error {
|
|
|
133
300
|
this.name = "RunDivergence";
|
|
134
301
|
}
|
|
135
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* A migration's walk was sent into a recorded branch the new source does not have.
|
|
305
|
+
*
|
|
306
|
+
* Its own code rather than `RunDivergence`, for the reason the L5005/L5006/L5007 collision in the
|
|
307
|
+
* orphan table bought: `RunDivergence` is a HASH comparison and says so in both its fields and its
|
|
308
|
+
* message, and putting branch NAMES in fields called `recordedHash`/`programHash` would be a lie in
|
|
309
|
+
* the payload a repair loop reads. The author's repair differs too — this one is fixed by looking at
|
|
310
|
+
* an arm's NAME, not at its body.
|
|
311
|
+
*/
|
|
312
|
+
export class ScopeBranchMissing extends Error {
|
|
313
|
+
scopeKey;
|
|
314
|
+
scope;
|
|
315
|
+
missing;
|
|
316
|
+
recorded;
|
|
317
|
+
source;
|
|
318
|
+
constructor(scopeKey, scope, missing, recorded, source) {
|
|
319
|
+
super(recorded.length === 0
|
|
320
|
+
// THE EMPTY CASE IS NOT THE SAME SENTENCE. "A recorded branch is not in the source" is false
|
|
321
|
+
// here: no branch was recorded at all, and saying "missing: " with nothing after it would
|
|
322
|
+
// send a reader looking through their source for an arm that was never named. The cause is
|
|
323
|
+
// the entry, not the edit, and the repair is different too.
|
|
324
|
+
? `L5022 A settled scope recorded no branch names\n\n step ${scopeKey} BRANCH NAMES ABSENT\n`
|
|
325
|
+
+ ` source ${source.join(", ")}\n\n`
|
|
326
|
+
+ `This ${scope} settled before scopes recorded their arm names on failure, so the walk has `
|
|
327
|
+
+ `nothing to tell it which arm ran. It cannot enter one, and entering none would wait `
|
|
328
|
+
+ `forever on a scope with no branches in it.\n\nOptions\n resume(run) replay it rather `
|
|
329
|
+
+ `than walking it\n fork(run, "${scopeKey}") re-run this scope on the current arms`
|
|
330
|
+
: `L5022 A recorded branch is not in the migrated source\n\n step ${scopeKey} BRANCH MISSING\n`
|
|
331
|
+
+ ` recorded ${recorded.join(", ")}\n source ${source.join(", ")}\n`
|
|
332
|
+
+ ` missing ${missing.join(", ")}\n\n`
|
|
333
|
+
+ `This ${scope} settled on ${missing.length === 1 ? "a branch" : "branches"} the new source no longer declares, so the walk `
|
|
334
|
+
+ `cannot enter ${missing.length === 1 ? "it" : "them"} to check what ran inside. Migrating anyway would hand the program a `
|
|
335
|
+
+ `result produced by an arm it does not have.\n\nOptions\n restore the branch ${missing.map((k) => `\`${k}\``).join(", ")} `
|
|
336
|
+
+ `keep the recorded result\n fork(run, "${scopeKey}") re-run this scope on the new arms`);
|
|
337
|
+
this.scopeKey = scopeKey;
|
|
338
|
+
this.scope = scope;
|
|
339
|
+
this.missing = missing;
|
|
340
|
+
this.recorded = recorded;
|
|
341
|
+
this.source = source;
|
|
342
|
+
this.name = "ScopeBranchMissing";
|
|
343
|
+
}
|
|
344
|
+
}
|
|
136
345
|
class Interpreter {
|
|
137
346
|
ast;
|
|
138
347
|
options;
|
|
139
348
|
programHash;
|
|
349
|
+
pins;
|
|
140
350
|
journal;
|
|
141
351
|
prng;
|
|
142
|
-
effectCount
|
|
352
|
+
effectCount;
|
|
143
353
|
ceiling;
|
|
144
354
|
steps = 0;
|
|
145
355
|
nextYield;
|
|
146
356
|
stepBudget;
|
|
147
357
|
yieldEvery;
|
|
148
|
-
|
|
358
|
+
/** The curated method tables (library.ts). Built once: they close over this interpreter's write check. */
|
|
359
|
+
methods;
|
|
360
|
+
constructor(ast, options, programHash, pins) {
|
|
149
361
|
this.ast = ast;
|
|
150
362
|
this.options = options;
|
|
151
363
|
this.programHash = programHash;
|
|
364
|
+
this.pins = pins;
|
|
365
|
+
// The journal and the run must be the same run. Request ids derive from `options.runId` while
|
|
366
|
+
// recorded results come from the journal, so a mismatch would submit work under one identity and
|
|
367
|
+
// resolve it against another's history.
|
|
368
|
+
if (options.journal !== undefined && options.journal.run !== options.runId)
|
|
369
|
+
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
370
|
this.journal = options.journal ?? new Journal({ run: options.runId });
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
371
|
+
// EVERY limit comes from the pins, never from a default applied here. A default resolved a
|
|
372
|
+
// second time is a default resolved by whichever interpreter happens to be resuming, which is
|
|
373
|
+
// exactly what pinning exists to stop.
|
|
374
|
+
// THE CEILING IS A RUN BOUND, SO THE COUNT STARTS WHERE THE RUN LEFT OFF. L4009 is named "Run
|
|
375
|
+
// effect ceiling reached" and the run record pins the ceiling — a pin is only worth
|
|
376
|
+
// refusing a mismatch on (L5009) if the thing it pins is enforced. Starting at 0 gave every
|
|
377
|
+
// activation a full allowance, so a runaway loop of effects that crashed or was released
|
|
378
|
+
// periodically never reached the ceiling however much it performed against the world, and the
|
|
379
|
+
// fault text claimed a run-scoped fact from an activation-scoped counter.
|
|
380
|
+
this.prng = new Prng(pins.seed);
|
|
381
|
+
this.effectCount = this.journal.dispatchedEffects();
|
|
382
|
+
this.ceiling = pins.effectCeiling;
|
|
383
|
+
this.stepBudget = pins.stepBudget;
|
|
384
|
+
this.yieldEvery = pins.yieldEvery;
|
|
157
385
|
this.nextYield = this.yieldEvery;
|
|
386
|
+
this.methods = {
|
|
387
|
+
array: arrayMethods(this.libraryContext()),
|
|
388
|
+
string: stringMethods(),
|
|
389
|
+
number: numberMethods(),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/** What the library sees of this interpreter. */
|
|
393
|
+
libraryContext() {
|
|
394
|
+
return {
|
|
395
|
+
runId: this.options.runId,
|
|
396
|
+
programHash: this.programHash,
|
|
397
|
+
startedAt: this.pins.startedAt,
|
|
398
|
+
prng: this.prng,
|
|
399
|
+
...(this.options.onLog !== undefined ? { onLog: this.options.onLog } : {}),
|
|
400
|
+
assertWritable: (target, frame) => this.assertWritable(target, frame),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
// ---- values: reads and writes ---------------------------------------------------------------
|
|
404
|
+
/**
|
|
405
|
+
* May this frame write into this container? Two refusals, and they are the whole of the value
|
|
406
|
+
* half of freeze-on-share (design D4, §3.4 rule 4):
|
|
407
|
+
*
|
|
408
|
+
* - a FROZEN value crossed an effect boundary, and what crossed is what was recorded (L2031);
|
|
409
|
+
* - a value born OUTSIDE this concurrent branch and written inside it is L2032's defect reached
|
|
410
|
+
* through a value instead of a binding, and just as silent on resume.
|
|
411
|
+
*/
|
|
412
|
+
assertWritable(target, frame) {
|
|
413
|
+
if (Object.isFrozen(target)) {
|
|
414
|
+
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]`.");
|
|
415
|
+
}
|
|
416
|
+
if (birthDepth(target) < frame.depth) {
|
|
417
|
+
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.");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/** The property key a member expression names, as JavaScript would spell it. A computed key is
|
|
421
|
+
* held to the same no-implicit-conversion law as every other coercion site (L4018): `String(k)`
|
|
422
|
+
* on a record would enter the host's ToPrimitive, which calls the value's own `toString` — a
|
|
423
|
+
* program closure invoked without a Frame. Measured before the refusal: the closure's rejection
|
|
424
|
+
* escaped as an unhandled host TypeError AFTER the run returned, and `o[{}] = 1` silently minted
|
|
425
|
+
* the own field `"[object Object]"`. Primitives keep JavaScript's spelling (`o[1]`, `o[true]`). */
|
|
426
|
+
async memberKey(node, env, frame) {
|
|
427
|
+
if (node.computed !== true)
|
|
428
|
+
return node.property.name;
|
|
429
|
+
const k = await this.evaluate(node.property, env, frame);
|
|
430
|
+
if (typeof k === "string")
|
|
431
|
+
return k;
|
|
432
|
+
refuseCoercion("[...]", k);
|
|
433
|
+
return String(k);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Read a member. Records answer their own fields and `undefined` for anything else, so a host
|
|
437
|
+
* prototype is never reached (`o.constructor`, `o.toString` are `undefined`); strings, arrays and
|
|
438
|
+
* numbers answer `length`, an index, or an entry of their method table, and refuse anything else
|
|
439
|
+
* (L4014). Functions and booleans have no members.
|
|
440
|
+
*/
|
|
441
|
+
memberOf(obj, prop, asCallee = false) {
|
|
442
|
+
switch (typeof obj) {
|
|
443
|
+
case "string": {
|
|
444
|
+
if (prop === "length")
|
|
445
|
+
return obj.length;
|
|
446
|
+
const i = arrayIndex(prop);
|
|
447
|
+
if (i !== undefined)
|
|
448
|
+
return obj[i];
|
|
449
|
+
return this.method(this.methods.string, obj, prop, "a string", asCallee);
|
|
450
|
+
}
|
|
451
|
+
case "number":
|
|
452
|
+
return this.method(this.methods.number, obj, prop, "a number", asCallee);
|
|
453
|
+
case "object": {
|
|
454
|
+
if (obj === null)
|
|
455
|
+
throw new RuntimeFault("L4010", `cannot read \`${prop}\` of null`);
|
|
456
|
+
if (Array.isArray(obj)) {
|
|
457
|
+
if (prop === "length")
|
|
458
|
+
return obj.length;
|
|
459
|
+
const i = arrayIndex(prop);
|
|
460
|
+
if (i !== undefined)
|
|
461
|
+
return obj[i];
|
|
462
|
+
return this.method(this.methods.array, obj, prop, "an array", asCallee);
|
|
463
|
+
}
|
|
464
|
+
return Object.prototype.hasOwnProperty.call(obj, prop) ? obj[prop] : undefined;
|
|
465
|
+
}
|
|
466
|
+
case "undefined":
|
|
467
|
+
throw new RuntimeFault("L4010", `cannot read \`${prop}\` of undefined`);
|
|
468
|
+
default:
|
|
469
|
+
throw new RuntimeFault("L4014", `\`${prop}\` is not a member: a ${typeof obj} has no members`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
method(table, receiver, prop, kind, asCallee) {
|
|
473
|
+
const m = table[prop];
|
|
474
|
+
if (m === undefined) {
|
|
475
|
+
throw new RuntimeFault("L4014", `\`${prop}\` is not a member of ${kind}. The members are: length, an index, ${Object.keys(table).join(", ")}.`);
|
|
476
|
+
}
|
|
477
|
+
// A method is looked up at the call and exists nowhere else — a declared difference from
|
|
478
|
+
// JavaScript, where `xs.map` is a value. Handing one out produced everything a bound-function
|
|
479
|
+
// factory produces (measured): `xs.map === xs.map` was false where JavaScript says true, and
|
|
480
|
+
// an extracted `push` wrote to its receiver where strict JavaScript throws. Refusing the read
|
|
481
|
+
// is honest on both counts.
|
|
482
|
+
if (!asCallee) {
|
|
483
|
+
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)\`.`);
|
|
484
|
+
}
|
|
485
|
+
return async (frame, args) => await m(frame, receiver, args);
|
|
486
|
+
}
|
|
487
|
+
/** Write a member: `o.a = v`, `xs[i] = v`. Records take any own field; arrays take an index or `length`. */
|
|
488
|
+
writeMember(obj, prop, value, frame) {
|
|
489
|
+
if (obj === null || obj === undefined || typeof obj !== "object") {
|
|
490
|
+
throw new RuntimeFault("L4010", `cannot write \`${prop}\` of ${obj === null ? "null" : typeof obj === "undefined" ? "undefined" : `a ${typeof obj}`}`);
|
|
491
|
+
}
|
|
492
|
+
this.assertWritable(obj, frame);
|
|
493
|
+
if (Array.isArray(obj)) {
|
|
494
|
+
if (prop === "length") {
|
|
495
|
+
// `xs.length = n` truncates, as in JavaScript. A LONGER length is refused: JavaScript would
|
|
496
|
+
// fill the gap with holes, and a hole is a value class this language does not have (its
|
|
497
|
+
// methods do not skip holes, so a program with holes would read differently here and on a
|
|
498
|
+
// real engine). Push what you need instead. `length` is not an own data property that
|
|
499
|
+
// `setOwn` can define, so the write goes to the array itself.
|
|
500
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > obj.length) {
|
|
501
|
+
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`);
|
|
502
|
+
}
|
|
503
|
+
obj.length = value;
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const i = arrayIndex(prop);
|
|
507
|
+
if (i === undefined) {
|
|
508
|
+
throw new RuntimeFault("L4014", `\`${prop}\` is not a member of an array: an array takes an index or \`length\``);
|
|
509
|
+
}
|
|
510
|
+
// Contiguous or refused: JavaScript would fill the gap with holes, and a hole is a value
|
|
511
|
+
// class this language does not have (measured before the refusal: `xs[2] = 1` on an empty
|
|
512
|
+
// array built a sparse array whose holes then crossed an effect boundary as silent nulls).
|
|
513
|
+
// Writing AT the length appends, which is `push` by another spelling and makes no hole.
|
|
514
|
+
if (i > obj.length) {
|
|
515
|
+
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\`.`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
else if (prop === "__proto__") {
|
|
519
|
+
throw new RuntimeFault("L4014", "`__proto__` names an object's prototype, and there are no prototypes here");
|
|
520
|
+
}
|
|
521
|
+
setOwn(obj, prop, value);
|
|
158
522
|
}
|
|
159
523
|
// ---- the fuel ceiling -----------------------------------------------------------------------
|
|
160
524
|
/**
|
|
@@ -167,31 +531,35 @@ class Interpreter {
|
|
|
167
531
|
tick(frame) {
|
|
168
532
|
this.steps += 1;
|
|
169
533
|
if (this.steps > this.stepBudget) {
|
|
170
|
-
throw new RuntimeFault("L4013", `this
|
|
534
|
+
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
535
|
}
|
|
172
536
|
if (this.steps < this.nextYield)
|
|
173
537
|
return null;
|
|
174
538
|
this.nextYield = this.steps + this.yieldEvery;
|
|
175
539
|
return this.breathe(frame);
|
|
176
540
|
}
|
|
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
541
|
get stepCount() {
|
|
188
542
|
return this.steps;
|
|
189
543
|
}
|
|
544
|
+
/**
|
|
545
|
+
* Hand the macrotask queue back, then abandon this branch's pure work IF IT CAN NO LONGER MATTER.
|
|
546
|
+
*
|
|
547
|
+
* Cancellation is otherwise observed only at effect boundaries (see {@link Interpreter.performEffect}).
|
|
548
|
+
* This line used to cut every cancelled branch, so a `race` loser in a pure tail was abandoned at
|
|
549
|
+
* its next yield, and whether an arm that had already performed its last effect got to settle
|
|
550
|
+
* depended on how many dispatches its tail took against `yieldEvery`: the winner of a live race
|
|
551
|
+
* was a function of a host tuning knob (design §3.4, measured). The cut is now the scope's call
|
|
552
|
+
* (`Signal.cutPure`): an arm that cannot win any more is abandoned here, and an arm that could
|
|
553
|
+
* still win runs its pure work to a settle, so the winner is the recorded clocks and declaration
|
|
554
|
+
* order and nothing else (see `runScope`). An arm that could still win and spins forever is a
|
|
555
|
+
* pure infinite loop, and it ends the way every pure infinite loop ends: on the step budget
|
|
556
|
+
* (L4013), loudly, which is the run's answer rather than the scheduler's.
|
|
557
|
+
*/
|
|
190
558
|
async breathe(frame) {
|
|
191
559
|
await new Promise((resolve) => {
|
|
192
560
|
setTimeout(resolve, 0);
|
|
193
561
|
});
|
|
194
|
-
if (frame.signal.
|
|
562
|
+
if (frame.signal.cutPure)
|
|
195
563
|
throw new Cancelled(frame.signal.reason ?? "cancelled");
|
|
196
564
|
}
|
|
197
565
|
// ---- the effect seam ------------------------------------------------------------------------
|
|
@@ -229,6 +597,17 @@ class Interpreter {
|
|
|
229
597
|
if (frame.signal.cancelled) {
|
|
230
598
|
throw new Cancelled(frame.signal.reason ?? "cancelled");
|
|
231
599
|
}
|
|
600
|
+
// THE HOST'S STOP, asked before anything is begun and after every replay has been served. A
|
|
601
|
+
// driver holds its run under an absolute work horizon and may be asked to hand it back, and
|
|
602
|
+
// neither is a fact about the program — so the place to stop is here, where no entry has been
|
|
603
|
+
// written and no handler dispatched. One step later would mean a pending entry for work nobody
|
|
604
|
+
// performed; inside the handler would mean settling a failure for work that really happened.
|
|
605
|
+
// Replays above are deliberately unaffected: replaying a recorded prefix performs nothing, and
|
|
606
|
+
// a run that stopped mid-journal has to be able to walk back to where it stopped.
|
|
607
|
+
const stop = this.options.shouldStop?.();
|
|
608
|
+
if (stop !== undefined) {
|
|
609
|
+
throw new RunReleased(stop);
|
|
610
|
+
}
|
|
232
611
|
this.effectCount += 1;
|
|
233
612
|
if (this.effectCount > this.ceiling) {
|
|
234
613
|
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.`);
|
|
@@ -247,7 +626,20 @@ class Interpreter {
|
|
|
247
626
|
// is for every effect that never hops.
|
|
248
627
|
const attempt = recorded?.attempt ?? 0;
|
|
249
628
|
if (verdict.verdict === "miss") {
|
|
250
|
-
|
|
629
|
+
// AWAITED, and the await is the point: the request id the handler is about to submit under
|
|
630
|
+
// has to be durable BEFORE the work is issued, or a crash in the gap leaves real work that
|
|
631
|
+
// nothing in the journal names.
|
|
632
|
+
await this.journal.begin(key, inputHash, this.options.handler.now(), reqId);
|
|
633
|
+
// THE AWAIT ABOVE IS A GAP, and the cancellation law has to hold on both sides of it. The
|
|
634
|
+
// check before `begin` sees the world as it was when this step started; while the append was
|
|
635
|
+
// in flight a sibling can settle the race and cancel this branch. Measured before this line:
|
|
636
|
+
// the loser's effect was still dispatched, performed against the world, and recorded `ok` —
|
|
637
|
+
// a NEW effect by a cancelled branch, which is the one thing the law forbids. The pending
|
|
638
|
+
// entry is real (the append happened), so it settles as what this branch now is: cancelled.
|
|
639
|
+
if (frame.signal.cancelled) {
|
|
640
|
+
await this.journal.settle(key, { status: "cancelled" }, this.options.handler.now());
|
|
641
|
+
throw new Cancelled(frame.signal.reason ?? "cancelled");
|
|
642
|
+
}
|
|
251
643
|
}
|
|
252
644
|
const ctx = {
|
|
253
645
|
key,
|
|
@@ -259,21 +651,30 @@ class Interpreter {
|
|
|
259
651
|
attempt,
|
|
260
652
|
...(resume !== undefined ? { resume } : {}),
|
|
261
653
|
bind: async (external) => {
|
|
262
|
-
this.journal.bind(key, external);
|
|
654
|
+
await this.journal.bind(key, external);
|
|
263
655
|
},
|
|
264
656
|
};
|
|
657
|
+
// TWO FAILURE DOMAINS, AND THE TERMINAL APPEND IS NOT IN THE HANDLER'S.
|
|
658
|
+
//
|
|
659
|
+
// One `try` around both the dispatch and the settle produces the worst bug a journal can have:
|
|
660
|
+
// the handler completes, the store refuses the settling append, the catch below records that
|
|
661
|
+
// refusal as a handler fault, and the durable sequence becomes `[pending, settled:failed]` for
|
|
662
|
+
// work the world actually did, so every later replay reports failure for a real success. The
|
|
663
|
+
// handler's outcome is decided first, alone, and the append that records it happens outside,
|
|
664
|
+
// where a rejection is a durability failure that travels as itself and settles nothing.
|
|
665
|
+
let result;
|
|
265
666
|
try {
|
|
266
|
-
|
|
667
|
+
result = await perform(ctx, inputHash);
|
|
267
668
|
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
669
|
}
|
|
273
670
|
catch (e) {
|
|
274
671
|
const endedAt = this.options.handler.now();
|
|
672
|
+
// A journal that just refused an append cannot be asked to record why. It leaves by its own
|
|
673
|
+
// door, unwrapped, before anything tries to settle on top of it.
|
|
674
|
+
if (e instanceof JournalAppendRejected)
|
|
675
|
+
throw e;
|
|
275
676
|
if (e instanceof Cancelled) {
|
|
276
|
-
this.journal.settle(key, { status: "cancelled" }, endedAt);
|
|
677
|
+
await this.journal.settle(key, { status: "cancelled" }, endedAt);
|
|
277
678
|
throw e;
|
|
278
679
|
}
|
|
279
680
|
// A handler may raise a language code directly, and it survives. The simulator's "unscripted
|
|
@@ -281,17 +682,21 @@ class Interpreter {
|
|
|
281
682
|
// on `code` that the handler broke, when what actually happened is that their script is
|
|
282
683
|
// incomplete. Only the L-code shape is honoured: anything else a thrown object happens to
|
|
283
684
|
// call `code` (an errno, an HTTP status) is a handler fault and is recorded as one.
|
|
284
|
-
|
|
685
|
+
// Read defensively: a handler is other people's code and may throw a primitive, and reading
|
|
686
|
+
// `.code` or `.message` off `null` would replace its failure with the recorder's own.
|
|
687
|
+
const raised = e?.code;
|
|
285
688
|
const carried = typeof raised === "string" && /^L\d{4}$/.test(raised) ? raised : null;
|
|
286
689
|
const error = e instanceof EffectError
|
|
287
690
|
? { code: e.code, kind: e.kind, message: e.message, ...(e.detail !== undefined ? { detail: e.detail } : {}) }
|
|
288
|
-
: carried
|
|
289
|
-
|
|
290
|
-
: { code: "L4000", kind: "handler-fault", message: e.message };
|
|
291
|
-
this.journal.settle(key, { status: "failed", error }, endedAt);
|
|
691
|
+
: { code: carried ?? "L4000", kind: "handler-fault", message: messageOf(e) };
|
|
692
|
+
await this.journal.settle(key, { status: "failed", error }, endedAt);
|
|
292
693
|
frame.clock.advance(endedAt);
|
|
293
694
|
throw e instanceof EffectError ? e : new EffectError(error.code, error.kind, error.message);
|
|
294
695
|
}
|
|
696
|
+
const endedAt = this.options.handler.now();
|
|
697
|
+
await this.journal.settle(key, { status: "ok", result: deepFreeze(result) }, endedAt);
|
|
698
|
+
frame.clock.advance(endedAt);
|
|
699
|
+
return result;
|
|
295
700
|
}
|
|
296
701
|
// ---- expressions ------------------------------------------------------------------------------
|
|
297
702
|
async evaluate(node, env, frame) {
|
|
@@ -309,48 +714,64 @@ class Interpreter {
|
|
|
309
714
|
let out = "";
|
|
310
715
|
for (let i = 0; i < quasis.length; i += 1) {
|
|
311
716
|
out += quasis[i].value.cooked;
|
|
312
|
-
if (i < exprs.length)
|
|
313
|
-
|
|
717
|
+
if (i < exprs.length) {
|
|
718
|
+
// Primitives interpolate as JavaScript interpolates them; a container or a function is
|
|
719
|
+
// refused (L4018). Measured before the refusal: `${o}` on a record with its own
|
|
720
|
+
// `toString` crashed in the host's ToPrimitive, and `${f}` on a function PRINTED THE
|
|
721
|
+
// INTERPRETER'S OWN COMPILED CLOSURE — an implementation detail leaking into a value.
|
|
722
|
+
const v = await this.evaluate(exprs[i], env, frame);
|
|
723
|
+
refuseCoercion("${...}", v);
|
|
724
|
+
out += String(v);
|
|
725
|
+
}
|
|
314
726
|
}
|
|
315
727
|
return out;
|
|
316
728
|
}
|
|
317
729
|
case "ArrayExpression": {
|
|
318
730
|
const out = [];
|
|
319
731
|
for (const el of node.elements ?? []) {
|
|
320
|
-
if (el.type === "SpreadElement")
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
732
|
+
if (el.type === "SpreadElement")
|
|
733
|
+
out.push(...this.spreadable(await this.evaluate(el.argument, env, frame)));
|
|
734
|
+
else
|
|
325
735
|
out.push(await this.evaluate(el, env, frame));
|
|
326
|
-
}
|
|
327
736
|
}
|
|
328
|
-
return out;
|
|
737
|
+
return born(out, frame.depth);
|
|
329
738
|
}
|
|
330
739
|
case "ObjectExpression": {
|
|
331
740
|
const out = {};
|
|
332
741
|
for (const p of node.properties ?? []) {
|
|
333
742
|
if (p.type === "SpreadElement") {
|
|
334
|
-
|
|
743
|
+
const src = await this.evaluate(p.argument, env, frame);
|
|
744
|
+
if (src !== null && src !== undefined) {
|
|
745
|
+
for (const [k, v] of Object.entries(src))
|
|
746
|
+
setOwn(out, k, v);
|
|
747
|
+
}
|
|
335
748
|
continue;
|
|
336
749
|
}
|
|
337
750
|
const key = p.key;
|
|
338
751
|
const name = key.type === "Identifier" ? key.name : String(key.value);
|
|
339
|
-
out
|
|
752
|
+
setOwn(out, name, await this.evaluate(p.value, env, frame));
|
|
340
753
|
}
|
|
341
|
-
return out;
|
|
754
|
+
return born(out, frame.depth);
|
|
342
755
|
}
|
|
343
756
|
case "MemberExpression": {
|
|
344
757
|
const obj = await this.evaluate(node.object, env, frame);
|
|
345
|
-
if (obj === null || obj === undefined)
|
|
346
|
-
|
|
758
|
+
if ((obj === null || obj === undefined) && node.optional === true)
|
|
759
|
+
throw SHORT_CIRCUIT;
|
|
760
|
+
return this.memberOf(obj, await this.memberKey(node, env, frame));
|
|
761
|
+
}
|
|
762
|
+
case "ChainExpression": {
|
|
763
|
+
// `a?.b.c(d)`: a nullish `a` ends the WHOLE chain with `undefined`, and nothing after the
|
|
764
|
+
// `?.` is evaluated. The short-circuit travels as a private sentinel that only this case
|
|
765
|
+
// catches; a program's `try` cannot see it, because a chain is an expression and a `try`
|
|
766
|
+
// wraps statements.
|
|
767
|
+
try {
|
|
768
|
+
return await this.evaluate(node.expression, env, frame);
|
|
769
|
+
}
|
|
770
|
+
catch (e) {
|
|
771
|
+
if (e === SHORT_CIRCUIT)
|
|
347
772
|
return undefined;
|
|
348
|
-
throw
|
|
773
|
+
throw e;
|
|
349
774
|
}
|
|
350
|
-
const prop = node.computed === true
|
|
351
|
-
? String(await this.evaluate(node.property, env, frame))
|
|
352
|
-
: node.property.name;
|
|
353
|
-
return obj[prop];
|
|
354
775
|
}
|
|
355
776
|
case "UnaryExpression": {
|
|
356
777
|
const v = await this.evaluate(node.argument, env, frame);
|
|
@@ -358,15 +779,40 @@ class Interpreter {
|
|
|
358
779
|
case "!":
|
|
359
780
|
return !v;
|
|
360
781
|
case "-":
|
|
782
|
+
refuseCoercion("-", v);
|
|
361
783
|
return -v;
|
|
362
784
|
case "+":
|
|
785
|
+
refuseCoercion("+", v);
|
|
363
786
|
return +v;
|
|
787
|
+
case "~":
|
|
788
|
+
refuseCoercion("~", v);
|
|
789
|
+
return ~v;
|
|
364
790
|
case "typeof":
|
|
365
791
|
return typeof v;
|
|
366
792
|
default:
|
|
367
793
|
throw new RuntimeFault("L1000", `unsupported unary operator ${String(node.operator)}`);
|
|
368
794
|
}
|
|
369
795
|
}
|
|
796
|
+
case "UpdateExpression": {
|
|
797
|
+
// `x++`, `--o.count`: JavaScript's meaning, with the write going through the same two doors
|
|
798
|
+
// as an assignment (a binding's depth, a value's writability).
|
|
799
|
+
const delta = node.operator === "++" ? 1 : -1;
|
|
800
|
+
const prefix = node.prefix === true;
|
|
801
|
+
const arg = node.argument;
|
|
802
|
+
if (arg.type === "Identifier") {
|
|
803
|
+
const name = arg.name;
|
|
804
|
+
const old = Number(env.get(name));
|
|
805
|
+
const next = old + delta;
|
|
806
|
+
env.set(name, next, frame.depth);
|
|
807
|
+
return prefix ? next : old;
|
|
808
|
+
}
|
|
809
|
+
const obj = await this.evaluate(arg.object, env, frame);
|
|
810
|
+
const key = await this.memberKey(arg, env, frame);
|
|
811
|
+
const old = Number(this.memberOf(obj, key));
|
|
812
|
+
const next = old + delta;
|
|
813
|
+
this.writeMember(obj, key, next, frame);
|
|
814
|
+
return prefix ? next : old;
|
|
815
|
+
}
|
|
370
816
|
case "BinaryExpression": {
|
|
371
817
|
const l = await this.evaluate(node.left, env, frame);
|
|
372
818
|
const r = await this.evaluate(node.right, env, frame);
|
|
@@ -393,15 +839,8 @@ class Interpreter {
|
|
|
393
839
|
return (await this.evaluate(node.test, env, frame))
|
|
394
840
|
? await this.evaluate(node.consequent, env, frame)
|
|
395
841
|
: 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
|
-
}
|
|
842
|
+
case "AssignmentExpression":
|
|
843
|
+
return await this.assign(node, env, frame);
|
|
405
844
|
case "AwaitExpression":
|
|
406
845
|
return await this.evaluate(node.argument, env, frame);
|
|
407
846
|
case "ArrowFunctionExpression":
|
|
@@ -413,60 +852,139 @@ class Interpreter {
|
|
|
413
852
|
throw new RuntimeFault("L1000", `unsupported expression ${node.type}`);
|
|
414
853
|
}
|
|
415
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* Every assignment operator, on a binding or a member: `x = v`, `x += v`, `o.a ??= v`,
|
|
857
|
+
* `[a, b] = [b, a]`. The operator's meaning is JavaScript's; the write goes through the binding's
|
|
858
|
+
* depth check ({@link Env.set}) or the value's writability check ({@link Interpreter.writeMember}).
|
|
859
|
+
*/
|
|
860
|
+
async assign(node, env, frame) {
|
|
861
|
+
const op = node.operator;
|
|
862
|
+
const left = node.left;
|
|
863
|
+
if (left.type === "ObjectPattern" || left.type === "ArrayPattern") {
|
|
864
|
+
const value = await this.evaluate(node.right, env, frame);
|
|
865
|
+
await this.bindPattern(left, value, env, frame, "assign");
|
|
866
|
+
return value;
|
|
867
|
+
}
|
|
868
|
+
const read = left.type === "Identifier"
|
|
869
|
+
? { get: () => env.get(left.name), set: (v) => env.set(left.name, v, frame.depth) }
|
|
870
|
+
: await (async () => {
|
|
871
|
+
const obj = await this.evaluate(left.object, env, frame);
|
|
872
|
+
const key = await this.memberKey(left, env, frame);
|
|
873
|
+
return { get: () => this.memberOf(obj, key), set: (v) => this.writeMember(obj, key, v, frame) };
|
|
874
|
+
})();
|
|
875
|
+
if (op === "=") {
|
|
876
|
+
const v = await this.evaluate(node.right, env, frame);
|
|
877
|
+
read.set(v);
|
|
878
|
+
return v;
|
|
879
|
+
}
|
|
880
|
+
if (op === "&&=" || op === "||=" || op === "??=") {
|
|
881
|
+
const cur = read.get();
|
|
882
|
+
const proceed = op === "&&=" ? Boolean(cur) : op === "||=" ? !cur : cur === null || cur === undefined;
|
|
883
|
+
if (!proceed)
|
|
884
|
+
return cur;
|
|
885
|
+
const v = await this.evaluate(node.right, env, frame);
|
|
886
|
+
read.set(v);
|
|
887
|
+
return v;
|
|
888
|
+
}
|
|
889
|
+
const cur = read.get();
|
|
890
|
+
const r = await this.evaluate(node.right, env, frame);
|
|
891
|
+
const v = applyBinary(op.slice(0, -1), cur, r);
|
|
892
|
+
read.set(v);
|
|
893
|
+
return v;
|
|
894
|
+
}
|
|
895
|
+
/** What `...x` and `for (const v of x)` may iterate: an array or a string, and nothing else (L4015). */
|
|
896
|
+
spreadable(v) {
|
|
897
|
+
if (Array.isArray(v))
|
|
898
|
+
return v;
|
|
899
|
+
if (typeof v === "string")
|
|
900
|
+
return [...v];
|
|
901
|
+
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)\`.`);
|
|
902
|
+
}
|
|
416
903
|
makeFunction(node, closure) {
|
|
417
904
|
const params = node.params ?? [];
|
|
418
905
|
const body = node.body;
|
|
419
906
|
const isExpressionBody = body.type !== "BlockStatement";
|
|
420
|
-
|
|
421
|
-
|
|
907
|
+
const self = async (frame, args) => {
|
|
908
|
+
// The calling FRAME decides the depth, not the closure: a helper declared at the top level
|
|
909
|
+
// and called from inside a branch is executing concurrently, whatever scope it was written in.
|
|
910
|
+
const env = new Env(closure, frame.depth);
|
|
911
|
+
// A named function expression sees its own name: `const f = function walk(n) { ... walk() }`.
|
|
912
|
+
if (node.type === "FunctionExpression" && node.id !== null && node.id !== undefined) {
|
|
913
|
+
env.declare(node.id.name, self, false);
|
|
914
|
+
}
|
|
422
915
|
for (let i = 0; i < params.length; i += 1) {
|
|
423
|
-
|
|
916
|
+
const param = params[i];
|
|
917
|
+
if (param.type === "RestElement") {
|
|
918
|
+
await this.bindPattern(param.argument, born(args.slice(i), frame.depth), env, frame, "let");
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
await this.bindPattern(param, args[i], env, frame, "let");
|
|
424
922
|
}
|
|
425
923
|
if (isExpressionBody)
|
|
426
924
|
return await this.evaluate(body, env, frame);
|
|
427
925
|
const c = await this.executeBlock(body, env, frame);
|
|
428
926
|
return c.type === "return" ? c.value : undefined;
|
|
429
927
|
};
|
|
928
|
+
return self;
|
|
430
929
|
}
|
|
431
|
-
|
|
930
|
+
/**
|
|
931
|
+
* Bind a pattern: declare its names (`const`/`let`, including parameters, which are `let`) or
|
|
932
|
+
* assign to bindings that already exist (`assign`, for `[a, b] = [b, a]`).
|
|
933
|
+
*/
|
|
934
|
+
async bindPattern(pattern, value, env, frame, mode) {
|
|
432
935
|
switch (pattern.type) {
|
|
433
936
|
case "Identifier":
|
|
434
|
-
|
|
937
|
+
if (mode === "assign")
|
|
938
|
+
env.set(pattern.name, value, frame.depth);
|
|
939
|
+
else
|
|
940
|
+
env.declare(pattern.name, value, mode === "let");
|
|
941
|
+
return;
|
|
942
|
+
case "MemberExpression": {
|
|
943
|
+
// Only reachable in `assign` mode: `[o.a, o.b] = pair`.
|
|
944
|
+
const obj = await this.evaluate(pattern.object, env, frame);
|
|
945
|
+
this.writeMember(obj, await this.memberKey(pattern, env, frame), value, frame);
|
|
435
946
|
return;
|
|
947
|
+
}
|
|
436
948
|
case "AssignmentPattern":
|
|
437
|
-
await this.bindPattern(pattern.left, value === undefined ? await this.evaluate(pattern.right, env, frame) : value, env, frame,
|
|
949
|
+
await this.bindPattern(pattern.left, value === undefined ? await this.evaluate(pattern.right, env, frame) : value, env, frame, mode);
|
|
438
950
|
return;
|
|
439
951
|
case "ObjectPattern": {
|
|
440
|
-
|
|
952
|
+
if (value === null || value === undefined) {
|
|
953
|
+
throw new RuntimeFault("L4010", `cannot destructure ${String(value)}: there are no fields to take`);
|
|
954
|
+
}
|
|
955
|
+
const src = value;
|
|
441
956
|
const taken = [];
|
|
442
957
|
for (const p of pattern.properties) {
|
|
443
958
|
if (p.type === "RestElement") {
|
|
444
959
|
const rest = {};
|
|
445
960
|
for (const [k, v] of Object.entries(src))
|
|
446
961
|
if (!taken.includes(k))
|
|
447
|
-
rest
|
|
448
|
-
await this.bindPattern(p.argument, rest, env, frame,
|
|
962
|
+
setOwn(rest, k, v);
|
|
963
|
+
await this.bindPattern(p.argument, born(rest, frame.depth), env, frame, mode);
|
|
449
964
|
continue;
|
|
450
965
|
}
|
|
451
966
|
const key = p.key;
|
|
452
967
|
const name = key.type === "Identifier" ? key.name : String(key.value);
|
|
453
968
|
taken.push(name);
|
|
454
|
-
await this.bindPattern(p.value, src
|
|
969
|
+
await this.bindPattern(p.value, this.memberOf(src, name), env, frame, mode);
|
|
455
970
|
}
|
|
456
971
|
return;
|
|
457
972
|
}
|
|
458
973
|
case "ArrayPattern": {
|
|
459
|
-
|
|
974
|
+
if (value === null || value === undefined) {
|
|
975
|
+
throw new RuntimeFault("L4010", `cannot destructure ${String(value)}: there are no elements to take`);
|
|
976
|
+
}
|
|
977
|
+
const src = this.spreadable(value);
|
|
460
978
|
const els = pattern.elements;
|
|
461
979
|
for (let i = 0; i < els.length; i += 1) {
|
|
462
980
|
const el = els[i];
|
|
463
981
|
if (el === null || el === undefined)
|
|
464
982
|
continue;
|
|
465
983
|
if (el.type === "RestElement") {
|
|
466
|
-
await this.bindPattern(el.argument, src.slice(i), env, frame,
|
|
984
|
+
await this.bindPattern(el.argument, born(src.slice(i), frame.depth), env, frame, mode);
|
|
467
985
|
break;
|
|
468
986
|
}
|
|
469
|
-
await this.bindPattern(el, src[i], env, frame,
|
|
987
|
+
await this.bindPattern(el, src[i], env, frame, mode);
|
|
470
988
|
}
|
|
471
989
|
return;
|
|
472
990
|
}
|
|
@@ -484,11 +1002,24 @@ class Interpreter {
|
|
|
484
1002
|
if (callee.type === "Identifier" && PRIMITIVES[callee.name] !== undefined && !env.has(callee.name)) {
|
|
485
1003
|
return await this.callPrimitive(callee.name, argNodes, env, frame);
|
|
486
1004
|
}
|
|
487
|
-
|
|
1005
|
+
let fn;
|
|
1006
|
+
if (callee.type === "MemberExpression") {
|
|
1007
|
+
// The one place a method NAME may appear: as the callee. Resolving it here, with the flag,
|
|
1008
|
+
// is what lets `memberOf` refuse the same name everywhere else (L4020).
|
|
1009
|
+
const obj = await this.evaluate(callee.object, env, frame);
|
|
1010
|
+
if ((obj === null || obj === undefined) && callee.optional === true)
|
|
1011
|
+
throw SHORT_CIRCUIT;
|
|
1012
|
+
fn = this.memberOf(obj, await this.memberKey(callee, env, frame), true);
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
fn = await this.evaluate(callee, env, frame);
|
|
1016
|
+
}
|
|
1017
|
+
if ((fn === null || fn === undefined) && node.optional === true)
|
|
1018
|
+
throw SHORT_CIRCUIT;
|
|
488
1019
|
const args = [];
|
|
489
1020
|
for (const a of argNodes) {
|
|
490
1021
|
if (a.type === "SpreadElement")
|
|
491
|
-
args.push(...(await this.evaluate(a.argument, env, frame)));
|
|
1022
|
+
args.push(...this.spreadable(await this.evaluate(a.argument, env, frame)));
|
|
492
1023
|
else
|
|
493
1024
|
args.push(await this.evaluate(a, env, frame));
|
|
494
1025
|
}
|
|
@@ -511,6 +1042,27 @@ class Interpreter {
|
|
|
511
1042
|
const args = [];
|
|
512
1043
|
for (const a of argNodes)
|
|
513
1044
|
args.push(await this.evaluate(a, env, frame));
|
|
1045
|
+
// Every argument crosses the effect boundary: it is hashed, recorded, or handed to the handler,
|
|
1046
|
+
// and a value with no canonical form can be none of those. Refused HERE, before any entry is
|
|
1047
|
+
// written, with the argument named: `undefined`, a non-finite number and an opaque object are
|
|
1048
|
+
// L3041, a function is L3042. The result of the effect is held to the same rule in
|
|
1049
|
+
// {@link Interpreter.performEffect}.
|
|
1050
|
+
args.forEach((arg, i) => {
|
|
1051
|
+
try {
|
|
1052
|
+
assertCrossable(arg, `argument ${i + 1} of \`${name}\``);
|
|
1053
|
+
}
|
|
1054
|
+
catch (e) {
|
|
1055
|
+
if (e instanceof NotCrossable)
|
|
1056
|
+
throw new RuntimeFault(e.why === "function" ? "L3042" : "L3041", e.message);
|
|
1057
|
+
throw e;
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
// FREEZE ON SHARE, at the share. What crossed is what was hashed and recorded, so the program
|
|
1061
|
+
// mutating it afterwards — or the HANDLER mutating it on its side — would make the run's own
|
|
1062
|
+
// value disagree with its recorded form (measured before this line: `schema.deep.x = 2` after
|
|
1063
|
+
// an `ask` succeeded, no L2031, and a handler's write to `req.schema` reached the program).
|
|
1064
|
+
for (const arg of args)
|
|
1065
|
+
deepFreeze(arg);
|
|
514
1066
|
const bag = args[spec.optionsAt];
|
|
515
1067
|
const stepName = (name === "checkpoint" ? args[0] : this.option(bag, "name"));
|
|
516
1068
|
const handler = this.options.handler;
|
|
@@ -658,7 +1210,7 @@ class Interpreter {
|
|
|
658
1210
|
//
|
|
659
1211
|
// Name the open attempt on the pending row BEFORE issuing it, index and all.
|
|
660
1212
|
const nextId = attemptId(1);
|
|
661
|
-
this.journal.reissueAs(ctx.key, nextId, 1);
|
|
1213
|
+
await this.journal.reissueAs(ctx.key, nextId, 1);
|
|
662
1214
|
const second = await handler.checkpoint(finalReq, { ...ctx, requestId: nextId, attempt: 1 });
|
|
663
1215
|
// ONE HOP. An escalation that can escalate again never terminates, so a second expiry
|
|
664
1216
|
// settles as expired and the program decides, exactly as `proceed` would.
|
|
@@ -694,6 +1246,14 @@ class Interpreter {
|
|
|
694
1246
|
case "notify": {
|
|
695
1247
|
const agents = deepFreeze(args[0]);
|
|
696
1248
|
const fact = deepFreeze(args[1]);
|
|
1249
|
+
// THE BOUND, WHERE THE VALUE EXISTS. The validator checks a literal fact exactly and says
|
|
1250
|
+
// so about the computed one; this is the computed one. It is checked BEFORE the entry is
|
|
1251
|
+
// written, so a fact that breaks the bound never reaches a journal, a record, or a
|
|
1252
|
+
// handler — an out-of-bound notice recorded as performed would be laundered bytes with a
|
|
1253
|
+
// durable receipt. An error, never a truncation: a shortened notice still delivers.
|
|
1254
|
+
const violation = notifyFactViolation(fact);
|
|
1255
|
+
if (violation !== null)
|
|
1256
|
+
throw new RuntimeFault("L3043", violation);
|
|
697
1257
|
return await this.performEffect("notify", stepName ?? "", { agents: agents.map((a) => a.agent), fact }, (ctx) => handler.notify({ agents, fact }, ctx), frame);
|
|
698
1258
|
}
|
|
699
1259
|
case "monitor": {
|
|
@@ -704,6 +1264,35 @@ class Interpreter {
|
|
|
704
1264
|
throw new RuntimeFault("L1000", `${name} is not implemented in this interpreter`);
|
|
705
1265
|
}
|
|
706
1266
|
}
|
|
1267
|
+
/**
|
|
1268
|
+
* The `branchDigest`, over the arms a settled `race` will never be walked into.
|
|
1269
|
+
*
|
|
1270
|
+
* STRUCTURE, NOT TEXT AND NOT POSITION. Acorn nodes carry `start`/`end`, and an edit anywhere
|
|
1271
|
+
* earlier in the file moves every offset after it — a digest over those would diverge on a run
|
|
1272
|
+
* whose race nobody touched, which is the false positive that teaches people to bypass a check.
|
|
1273
|
+
* Digesting the source SLICE instead would diverge on reindentation for the same reason. What is
|
|
1274
|
+
* hashed is the branch body's shape with positions stripped, so a reformat is silent and an edit
|
|
1275
|
+
* is not.
|
|
1276
|
+
*
|
|
1277
|
+
* Only the LOSERS, because only they are unwalked. The winner's arm is walked entry by entry and
|
|
1278
|
+
* an edit inside it already diverges on the ordinary hash check with the step it broke named —
|
|
1279
|
+
* a strictly better error than "some branch changed". Digesting the winner too would replace that
|
|
1280
|
+
* error with this one, so it does not.
|
|
1281
|
+
*/
|
|
1282
|
+
branchDigester(branchesNode) {
|
|
1283
|
+
if (branchesNode?.type !== "ObjectExpression")
|
|
1284
|
+
return undefined;
|
|
1285
|
+
const bodies = new Map();
|
|
1286
|
+
for (const p of branchesNode.properties ?? []) {
|
|
1287
|
+
const key = p.key;
|
|
1288
|
+
const named = key?.name ?? key?.value;
|
|
1289
|
+
if (named !== undefined)
|
|
1290
|
+
bodies.set(named, stripPositions(p.value));
|
|
1291
|
+
}
|
|
1292
|
+
return (losers) => digest([...losers]
|
|
1293
|
+
.sort()
|
|
1294
|
+
.map((n) => [n, bodies.has(n) ? bodies.get(n) : null]));
|
|
1295
|
+
}
|
|
707
1296
|
/**
|
|
708
1297
|
* The concurrency combinators.
|
|
709
1298
|
*
|
|
@@ -722,38 +1311,394 @@ class Interpreter {
|
|
|
722
1311
|
const bag = bagNode === undefined ? undefined : await this.evaluate(bagNode, env, frame);
|
|
723
1312
|
const scopeName = this.option(bag, "name") ?? null;
|
|
724
1313
|
const occurrence = frame.keys.nextScope(scopeKind, scopeName);
|
|
1314
|
+
const scopeKey = frame.keys.scopeKey(scopeKind, scopeName, occurrence);
|
|
1315
|
+
// `conclave` is the one scope whose identity includes a SUBJECT (`hashesSubject`): the
|
|
1316
|
+
// members are what the sub-team IS, so editing the member list has to diverge rather than
|
|
1317
|
+
// resume into a different room. The other three are identified by kind, name and occurrence.
|
|
1318
|
+
const subject = spec.hashesSubject
|
|
1319
|
+
? {
|
|
1320
|
+
members: first.map((m) => m.agent),
|
|
1321
|
+
channel: this.option(bag, "channel") ?? null,
|
|
1322
|
+
}
|
|
1323
|
+
: undefined;
|
|
1324
|
+
return await this.performScope(scopeKey, frame, async (ctx, only) => await this.runScope(name, scopeKind, scopeName, occurrence, first, argNodes, bag, env, frame, ctx, only), subject,
|
|
1325
|
+
// `race` alone. `parallel` and `fanOut` have no losers — every branch is a winner and the
|
|
1326
|
+
// walk enters all of them — and a `conclave` cannot be walked into at all, so a digest there
|
|
1327
|
+
// would bind arms nothing was ever going to miss.
|
|
1328
|
+
name === "race" ? this.branchDigester(argNodes[0]) : undefined);
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* A concurrency scope's own journal entry, and what replay does with it.
|
|
1332
|
+
*
|
|
1333
|
+
* The scope is journalled as ONE durable record carrying its outcome, and for a cancelling scope
|
|
1334
|
+
* the intent to cancel its siblings. Without it a replayed `race` re-races: both branches may have
|
|
1335
|
+
* settled before the cancellation reached the loser, so the journal holds two successful branches
|
|
1336
|
+
* and nothing saying which one won, and a replayed run can take the other path and reach a step
|
|
1337
|
+
* that was never recorded.
|
|
1338
|
+
*
|
|
1339
|
+
* A settled scope therefore ENTERS NO BRANCH, and the order below is normative rather than
|
|
1340
|
+
* convenient: account for the subtree first, then discharge the cancellation, and only then
|
|
1341
|
+
* deliver the outcome. Leading with the delivery is the defect — the next program step can share
|
|
1342
|
+
* a worktree with a loser that is still writing.
|
|
1343
|
+
*/
|
|
1344
|
+
async performScope(scopeKey, frame, body, subject,
|
|
1345
|
+
/** The `branchDigest` over a named loser set. Absent where there is nothing to digest. */
|
|
1346
|
+
branchDigest) {
|
|
1347
|
+
const inputHash = digest(subject === undefined
|
|
1348
|
+
? { kind: scopeKey.kind, name: scopeKey.name }
|
|
1349
|
+
: { kind: scopeKey.kind, name: scopeKey.name, subject });
|
|
1350
|
+
const verdict = this.journal.lookup(scopeKey, inputHash);
|
|
1351
|
+
if (verdict.verdict === "diverged") {
|
|
1352
|
+
throw new RunDivergence(stepKeyString(scopeKey), verdict.recordedHash, verdict.programHash);
|
|
1353
|
+
}
|
|
1354
|
+
if (verdict.verdict === "replay" || verdict.verdict === "replay-failed") {
|
|
1355
|
+
const entry = verdict.entry;
|
|
1356
|
+
const endedAt = entry.endedAt ?? this.options.handler.now();
|
|
1357
|
+
// The comparison: `branchDigest` is checked whenever the entry carries one. The scope's own
|
|
1358
|
+
// `inputHash` is `{kind, name}` — an arm's body is not in it — and a settled race is
|
|
1359
|
+
// delivered from this entry without entering a branch, so without this comparison an edit
|
|
1360
|
+
// inside a LOSING arm reaches nothing that could notice it. Both replay paths, not the
|
|
1361
|
+
// migration path alone: a resume of edited source is exactly the case a divergence exists to
|
|
1362
|
+
// make loud, and the run record carries no program hash to have refused it earlier.
|
|
1363
|
+
if (entry.branchDigest !== undefined && branchDigest !== undefined) {
|
|
1364
|
+
const now = branchDigest(entry.cancel?.losers ?? []);
|
|
1365
|
+
if (now !== undefined && now !== entry.branchDigest) {
|
|
1366
|
+
throw new RunDivergence(stepKeyString(scopeKey), entry.branchDigest, now);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
// A MIGRATION MUST NOT TAKE THE SHORT-CIRCUIT ABOVE.
|
|
1370
|
+
//
|
|
1371
|
+
// Consuming the subtree wholesale is right for a resume — the program hash is unchanged, so
|
|
1372
|
+
// nothing under this scope can have been removed, and the branches were DECIDED rather than
|
|
1373
|
+
// deleted. Under a migration the source HAS changed, and marking every entry beneath the
|
|
1374
|
+
// scope accounted for means an effect the new source removed never reaches `orphans()`: a
|
|
1375
|
+
// resolved human checkpoint inside the winning branch disappears and L5004 never fires. A
|
|
1376
|
+
// silent disappearance whose log line never fires is invisible in the artifact AND in the
|
|
1377
|
+
// trace, which is the worst available failure.
|
|
1378
|
+
//
|
|
1379
|
+
// So the walk enters the RECORDED WINNING branches and runs the ordinary hash and orphan
|
|
1380
|
+
// checks inside them, while the losers — decided, not removed — are accounted for as before.
|
|
1381
|
+
if (this.options.migration === true) {
|
|
1382
|
+
if (subject !== undefined)
|
|
1383
|
+
throw new UnwalkableScope(stepKeyString(scopeKey), "conclave");
|
|
1384
|
+
// A SETTLED SCOPE CARRIES ITS ARM NAMES IN ONE OF TWO PLACES, and reading only the first
|
|
1385
|
+
// is what made a failed scope look like a scope with no arms. `result` holds them when the
|
|
1386
|
+
// scope succeeded; the `branches` FACT holds them when it failed, because `settle` writes
|
|
1387
|
+
// no `result` for a failure.
|
|
1388
|
+
const recorded = entry.result;
|
|
1389
|
+
const branches = recorded?.branches ?? entry.branches ?? [];
|
|
1390
|
+
const losers = new Set(entry.cancel?.losers ?? []);
|
|
1391
|
+
await this.journal.consumeScope(stepKeyString(scopeKey), endedAt, losers);
|
|
1392
|
+
try {
|
|
1393
|
+
await body({
|
|
1394
|
+
key: scopeKey,
|
|
1395
|
+
signal: frame.signal,
|
|
1396
|
+
requestId: entry.requestId ?? requestId(this.options.runId, scopeKey, inputHash),
|
|
1397
|
+
attempt: entry.attempt ?? 0,
|
|
1398
|
+
bind: async () => {
|
|
1399
|
+
throw new UnwalkableScope(stepKeyString(scopeKey), "bind");
|
|
1400
|
+
},
|
|
1401
|
+
}, new Set(branches.filter((b) => !losers.has(b))));
|
|
1402
|
+
}
|
|
1403
|
+
catch (e) {
|
|
1404
|
+
// UNWRAPPED, because the caller of a migration wants the step that diverged and not the
|
|
1405
|
+
// scope that carried it. A live scope wraps a branch's failure so it can record the
|
|
1406
|
+
// cancellation intent with it; a walk records nothing and cancels nobody, so the wrapper
|
|
1407
|
+
// would only hide a `RunDivergence` behind a generic scope fault.
|
|
1408
|
+
throw unwrapScope(e).reason;
|
|
1409
|
+
}
|
|
1410
|
+
if (entry.endedAt !== undefined)
|
|
1411
|
+
frame.clock.advance(entry.endedAt);
|
|
1412
|
+
if (verdict.verdict === "replay-failed") {
|
|
1413
|
+
const e = entry.error;
|
|
1414
|
+
throw new EffectError(e.code, e.kind, e.message, e.detail);
|
|
1415
|
+
}
|
|
1416
|
+
return entry.result.value;
|
|
1417
|
+
}
|
|
1418
|
+
// (1) account for the subtree, settling any loser still pending as cancelled;
|
|
1419
|
+
await this.journal.consumeScope(stepKeyString(scopeKey), endedAt);
|
|
1420
|
+
// (2) the cancellation intent is the driver's to discharge against the world; a journal write
|
|
1421
|
+
// cancels nothing by itself, so an undischarged intent stays visible rather than silently
|
|
1422
|
+
// reading as done.
|
|
1423
|
+
// (3) only now, the outcome.
|
|
1424
|
+
if (entry.endedAt !== undefined)
|
|
1425
|
+
frame.clock.advance(entry.endedAt);
|
|
1426
|
+
if (verdict.verdict === "replay-failed") {
|
|
1427
|
+
const e = entry.error;
|
|
1428
|
+
throw new EffectError(e.code, e.kind, e.message, e.detail);
|
|
1429
|
+
}
|
|
1430
|
+
return entry.result.value;
|
|
1431
|
+
}
|
|
1432
|
+
if (verdict.verdict === "replay-cancelled") {
|
|
1433
|
+
throw new Cancelled("this scope was cancelled on the recorded run");
|
|
1434
|
+
}
|
|
1435
|
+
// `miss` and `pending` alike RE-ENTER the scope: there is no recorded outcome to return, and a
|
|
1436
|
+
// pending scope's losers were never durably cancelled. Settling is idempotent, so the arm that
|
|
1437
|
+
// finishes first wins again — except where the journal already knows better, which is what
|
|
1438
|
+
// `runScope`'s replayed-branch tie-break is for.
|
|
1439
|
+
// A scope that CALLS THE HANDLER owes a durable request id exactly as an effect does, and for
|
|
1440
|
+
// the same reason: a crash between issuing the work and recording who issued it leaves real
|
|
1441
|
+
// work — for `conclave`, a live channel with members joined — that nothing in the journal
|
|
1442
|
+
// names. `subject` marks that scope, because `conclave` is the only one that dispatches from
|
|
1443
|
+
// this path; the other three launch thunks and touch no handler of their own.
|
|
1444
|
+
const dispatches = subject !== undefined;
|
|
1445
|
+
const resume = verdict.verdict === "pending" ? verdict.entry.external : undefined;
|
|
1446
|
+
const recorded = verdict.verdict === "pending" && verdict.entry.requestId !== undefined ? verdict.entry : undefined;
|
|
1447
|
+
const reqId = recorded?.requestId ?? requestId(this.options.runId, scopeKey, inputHash);
|
|
1448
|
+
if (verdict.verdict === "miss") {
|
|
1449
|
+
await this.journal.begin(scopeKey, inputHash, this.options.handler.now(), dispatches ? reqId : undefined);
|
|
1450
|
+
// The same gap as {@link Interpreter.performEffect}'s begin, for the scope that DISPATCHES: a
|
|
1451
|
+
// conclave cancelled while its begin was in flight must not open a channel and join members.
|
|
1452
|
+
// The non-dispatching scopes launch no work of their own — each branch effect re-checks its
|
|
1453
|
+
// own signal — so only the dispatching path re-checks here.
|
|
1454
|
+
if (dispatches && frame.signal.cancelled) {
|
|
1455
|
+
await this.journal.settle(scopeKey, { status: "cancelled" }, frame.clock.now());
|
|
1456
|
+
throw new Cancelled(frame.signal.reason ?? "cancelled");
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
const ctx = {
|
|
1460
|
+
key: scopeKey,
|
|
1461
|
+
signal: frame.signal,
|
|
1462
|
+
requestId: reqId,
|
|
1463
|
+
attempt: recorded?.attempt ?? 0,
|
|
1464
|
+
...(resume !== undefined ? { resume } : {}),
|
|
1465
|
+
bind: async (external) => {
|
|
1466
|
+
await this.journal.bind(scopeKey, external);
|
|
1467
|
+
},
|
|
1468
|
+
};
|
|
1469
|
+
// The same two domains as {@link Interpreter.performEffect}, for the same reason: a scope whose
|
|
1470
|
+
// branches all succeeded and whose settling append was refused must not be recorded as failed.
|
|
1471
|
+
let outcome;
|
|
1472
|
+
try {
|
|
1473
|
+
outcome = await body(ctx);
|
|
1474
|
+
}
|
|
1475
|
+
catch (raw) {
|
|
1476
|
+
// The interpreter's facts come out of the envelope; the program's thrown value comes out
|
|
1477
|
+
// whole, and is what the caller sees. A value the program threw is never written on.
|
|
1478
|
+
const { reason, facts } = unwrapScope(raw);
|
|
1479
|
+
// THE SCOPE'S CLOCK AT SETTLE, not the host's clock at append. `runScope` joins the branch
|
|
1480
|
+
// clocks before the outcome leaves it, so `frame.clock.now()` here is the greatest `endedAt`
|
|
1481
|
+
// the scope's branches awaited — which is what `now()` answers after the scope, live. Replay
|
|
1482
|
+
// advances the parent clock from this stamp and enters no branch, so stamping anything else
|
|
1483
|
+
// (measured: the handler's clock at append time) makes live and replay disagree on `now()`
|
|
1484
|
+
// after every scope whose last-to-land effect was not the handler's last stamp, and a program
|
|
1485
|
+
// that branches on `now()` takes a path on resume that the live run never took.
|
|
1486
|
+
const endedAt = frame.clock.now();
|
|
1487
|
+
if (reason instanceof JournalAppendRejected)
|
|
1488
|
+
throw reason;
|
|
1489
|
+
// A close that did not acknowledge settles NOTHING. The entry stays pending, which is exactly
|
|
1490
|
+
// what "a close is still owed" looks like in a journal, and the underlying handler error is
|
|
1491
|
+
// what the caller sees.
|
|
1492
|
+
if (reason instanceof CloseOwed)
|
|
1493
|
+
throw reason.reason;
|
|
1494
|
+
if (reason instanceof Cancelled) {
|
|
1495
|
+
await this.journal.settle(scopeKey, { status: "cancelled" }, endedAt, facts);
|
|
1496
|
+
throw reason;
|
|
1497
|
+
}
|
|
1498
|
+
const err = reason instanceof EffectError
|
|
1499
|
+
? {
|
|
1500
|
+
code: reason.code,
|
|
1501
|
+
kind: reason.kind,
|
|
1502
|
+
message: reason.message,
|
|
1503
|
+
...(reason.detail !== undefined ? { detail: reason.detail } : {}),
|
|
1504
|
+
}
|
|
1505
|
+
: { code: "L4000", kind: "scope-fault", message: messageOf(reason) };
|
|
1506
|
+
// A rejecting branch cancels its siblings and can crash before they hear it, so a FAILED scope
|
|
1507
|
+
// carries the intent too — and a conclave that closed says so even when its body failed.
|
|
1508
|
+
await this.journal.settle(scopeKey, { status: "failed", error: err }, endedAt, {
|
|
1509
|
+
...facts,
|
|
1510
|
+
...digestFacts(branchDigest, facts.cancel?.losers),
|
|
1511
|
+
});
|
|
1512
|
+
throw reason;
|
|
1513
|
+
}
|
|
1514
|
+
await this.journal.settle(scopeKey, { status: "ok", result: { branches: outcome.branches, value: deepFreeze(outcome.value) } },
|
|
1515
|
+
// The joined branch clock, for the same reason as the failure path above: this is the value
|
|
1516
|
+
// `now()` answers after the scope, and the stamp replay hands back must be that value.
|
|
1517
|
+
frame.clock.now(), {
|
|
1518
|
+
...(outcome.cancel !== undefined ? { cancel: outcome.cancel } : {}),
|
|
1519
|
+
...(outcome.closed !== undefined ? { closed: outcome.closed } : {}),
|
|
1520
|
+
...digestFacts(branchDigest, outcome.cancel?.losers),
|
|
1521
|
+
});
|
|
1522
|
+
return outcome.value;
|
|
1523
|
+
}
|
|
1524
|
+
async runScope(name, scopeKind, scopeName, occurrence, first, argNodes, bag, env, frame, ctx,
|
|
1525
|
+
/** A migration's walk: enter exactly these branches, the ones the recorded run WON with. */
|
|
1526
|
+
only) {
|
|
725
1527
|
if (name === "parallel" || name === "race") {
|
|
726
|
-
const
|
|
1528
|
+
const all = Array.isArray(first)
|
|
727
1529
|
? first.map((fn, i) => [String(i), fn])
|
|
728
1530
|
: Object.entries(first);
|
|
1531
|
+
const entries = only === undefined ? all : all.filter(([k]) => only.has(k));
|
|
1532
|
+
// THE WALK MUST FIND EVERY ARM IT WAS SENT TO ENTER.
|
|
1533
|
+
//
|
|
1534
|
+
// `only` is the set of RECORDED WINNING branch keys, and the whole "losers only" digest rule
|
|
1535
|
+
// rests on the walk entering the winner: an edit there is supposed to diverge at the step it
|
|
1536
|
+
// broke, which is a strictly better error than "some arm of this race changed". A RENAME
|
|
1537
|
+
// removes the arm, so there is no step left to diverge at and the argument silently stops
|
|
1538
|
+
// holding. What happened instead was worse than a silent pass. `entries` came back empty,
|
|
1539
|
+
// `running` with it, and `Promise.race([])` NEVER SETTLES — a migration or a fork over a
|
|
1540
|
+
// renamed winning arm hung rather than returning any verdict at all. `parallel` did not hang,
|
|
1541
|
+
// because `Promise.all([])` resolves, and handed the program back the recorded value keyed by
|
|
1542
|
+
// the arm the source no longer has.
|
|
1543
|
+
//
|
|
1544
|
+
// Narrow on purpose, and every neighbouring shape already has an answer: a renamed or deleted
|
|
1545
|
+
// LOSER diverges through the branch digest, and an ADDED arm is not an edit to anything
|
|
1546
|
+
// recorded, so neither reaches this.
|
|
1547
|
+
if (only !== undefined) {
|
|
1548
|
+
const present = new Set(all.map(([k]) => k));
|
|
1549
|
+
const missing = [...only].filter((k) => !present.has(k));
|
|
1550
|
+
if (missing.length > 0) {
|
|
1551
|
+
throw new ScopeBranchMissing(stepKeyString(ctx.key), name, missing, [...only], [...present]);
|
|
1552
|
+
}
|
|
1553
|
+
// AND THE EMPTY CASE, which the check above cannot see: with no recorded branches at all,
|
|
1554
|
+
// "every recorded branch is present" is vacuously true, so the guard passed and the walk
|
|
1555
|
+
// still entered nothing and still hung. A guard over an empty set grades nothing and is
|
|
1556
|
+
// green forever. Journals written before scopes recorded their arm names on failure are
|
|
1557
|
+
// exactly that shape, so this refuses them by name instead of hanging on them. It cannot
|
|
1558
|
+
// fire on a scope that has no arms in the source either, because `all` is empty then too.
|
|
1559
|
+
if (only.size === 0 && all.length > 0) {
|
|
1560
|
+
throw new ScopeBranchMissing(stepKeyString(ctx.key), name, [], [], all.map(([k]) => k));
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
729
1563
|
const frames = entries.map(([k]) => frame.branch(scopeKind, scopeName, occurrence, k));
|
|
730
1564
|
const running = entries.map(([, fn], i) => fn(frames[i], []));
|
|
1565
|
+
const branches = entries.map(([k]) => k);
|
|
731
1566
|
if (name === "parallel") {
|
|
1567
|
+
let failed = null;
|
|
1568
|
+
const tracked = running.map((p, i) => p.catch((e) => {
|
|
1569
|
+
if (failed === null)
|
|
1570
|
+
failed = entries[i]?.[0];
|
|
1571
|
+
throw e;
|
|
1572
|
+
}));
|
|
732
1573
|
try {
|
|
733
|
-
const results = await Promise.all(
|
|
1574
|
+
const results = await Promise.all(tracked);
|
|
734
1575
|
frame.clock.join(frames.map((f) => f.clock));
|
|
735
|
-
return
|
|
736
|
-
|
|
737
|
-
: Object.fromEntries(entries.map(([k], i) => [k, results[i]]))
|
|
1576
|
+
return {
|
|
1577
|
+
branches,
|
|
1578
|
+
value: Array.isArray(first) ? results : Object.fromEntries(entries.map(([k], i) => [k, results[i]])),
|
|
1579
|
+
};
|
|
738
1580
|
}
|
|
739
1581
|
catch (e) {
|
|
740
|
-
// The first rejection cancels the rest, then rethrows.
|
|
1582
|
+
// The first rejection cancels the rest, then rethrows. The intent travels WITH the
|
|
1583
|
+
// failure, because a rejecting branch cancels its siblings and can crash before they
|
|
1584
|
+
// hear it, so a failed scope owes its losers exactly as a winning one does.
|
|
741
1585
|
for (const f of frames)
|
|
742
1586
|
f.signal.cancel("a sibling branch failed");
|
|
743
1587
|
await Promise.allSettled(running);
|
|
744
1588
|
frame.clock.join(frames.map((f) => f.clock));
|
|
745
|
-
|
|
1589
|
+
const losers = branches.filter((k) => k !== failed);
|
|
1590
|
+
throw new ScopeFailed(e, { branches, cancel: { losers, issued: false } });
|
|
746
1591
|
}
|
|
747
1592
|
}
|
|
748
|
-
// race:
|
|
749
|
-
// program calls. A cancelled branch performs no new effects; an agent reply already in
|
|
1593
|
+
// race: the earliest to settle wins, and the losers are cancelled BY SEMANTICS, not by an API
|
|
1594
|
+
// the program calls. A cancelled branch performs no new effects; an agent reply already in
|
|
750
1595
|
// flight completes and is ignored, which is the documented answer rather than an accident.
|
|
751
|
-
|
|
1596
|
+
// THE WINNER IS THE EARLIEST BRANCH, NOT THE FIRST ONE SCHEDULING HAPPENED TO WAKE.
|
|
1597
|
+
//
|
|
1598
|
+
// An arm's logical settlement time is its branch clock: the max endedAt of the effects it
|
|
1599
|
+
// awaited (the scope's entry clock if it awaited none), which is recorded. The winner is the
|
|
1600
|
+
// least clock among the arms that settled; equal clocks fall to declaration order, which is
|
|
1601
|
+
// recorded too. So the same journal resolves the same arm on every re-entry.
|
|
1602
|
+
//
|
|
1603
|
+
// AND LIVE, NO SCHEDULER AND NO `yieldEvery` VALUE CAN CHOOSE. When an arm settles, every
|
|
1604
|
+
// sibling is cancelled (no new effects, the cancellation law), and each sibling is CUT, pure
|
|
1605
|
+
// work included, only if it can no longer win: its clock is later, or equal and it is declared
|
|
1606
|
+
// later. A sibling that could still win runs its pure work to a settle, and a sibling that
|
|
1607
|
+
// reaches a new effect is cut there, having proven it would end after the settled arm's clock.
|
|
1608
|
+
// Which arms settle is therefore a function of their effects and the declaration order, and
|
|
1609
|
+
// so is the winner. A later settle with an earlier clock re-decides the cut for the rest.
|
|
1610
|
+
// A FAILURE IS A SETTLE, so a rejecting arm is a candidate to win — it just wins by failing
|
|
1611
|
+
// the scope. What is NOT a candidate is a branch that rejected with `Cancelled`, because that
|
|
1612
|
+
// is not an outcome the branch reached, it is what losing did to it. Counting those would let
|
|
1613
|
+
// a loser cut short at an early step outrank the winner that ran longer.
|
|
1614
|
+
// The FRONTIER: the least clock among the arms that have settled as candidates, ties to the
|
|
1615
|
+
// earlier declaration. The cut compares against it in both places below, because it is the
|
|
1616
|
+
// bar an unsettled arm actually has to beat.
|
|
1617
|
+
let bestAt = -1;
|
|
1618
|
+
let bestIndex = -1;
|
|
1619
|
+
const behindFrontier = (j) => {
|
|
1620
|
+
const other = frames[j].clock.now();
|
|
1621
|
+
return !(other < bestAt || (other === bestAt && j < bestIndex));
|
|
1622
|
+
};
|
|
1623
|
+
const onSettle = (i, wasCancelled) => {
|
|
1624
|
+
if (wasCancelled)
|
|
1625
|
+
return;
|
|
1626
|
+
const at = frames[i].clock.now();
|
|
1627
|
+
if (bestIndex === -1 || at < bestAt || (at === bestAt && i < bestIndex)) {
|
|
1628
|
+
bestAt = at;
|
|
1629
|
+
bestIndex = i;
|
|
1630
|
+
}
|
|
1631
|
+
for (let j = 0; j < frames.length; j += 1) {
|
|
1632
|
+
if (j === i)
|
|
1633
|
+
continue;
|
|
1634
|
+
frames[j].signal.cancel("a sibling branch won the race", { cutPure: behindFrontier(j) });
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
running.forEach((p, i) => {
|
|
1638
|
+
p.then(() => onSettle(i, false), (e) => onSettle(i, e instanceof Cancelled));
|
|
1639
|
+
});
|
|
1640
|
+
// AND THE CUT IS RE-DECIDED WHEN AN ARM'S OWN CLOCK MOVES. A cancelled arm with an effect
|
|
1641
|
+
// already in flight is allowed to see it land — the work was issued before the cancellation
|
|
1642
|
+
// — but landing advances the arm's clock, and an arm that lands PAST the frontier has just
|
|
1643
|
+
// proven it cannot win. Deciding only at settles left that arm running its pure tail on a
|
|
1644
|
+
// verdict reached from its old clock: measured, an infinite pure tail burned the whole step
|
|
1645
|
+
// budget and killed a run whose race had already settled `ok`, while a resume of the same
|
|
1646
|
+
// journal returned the winner — live and replay disagreeing on the run's outcome. An arm
|
|
1647
|
+
// that lands BEFORE the frontier keeps running, because it can still win (its own cell).
|
|
1648
|
+
frames.forEach((f, j) => {
|
|
1649
|
+
f.clock.onAdvance(() => {
|
|
1650
|
+
if (f.signal.cancelled && !f.signal.cutPure && bestIndex !== -1 && behindFrontier(j)) {
|
|
1651
|
+
f.signal.cancel("a sibling branch won the race", { cutPure: true });
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
});
|
|
1655
|
+
// BOTH HANDLERS, and the rejection handler is the whole point. `p.then(() => undefined)`
|
|
1656
|
+
// propagates a rejection, so the first arm to FAIL threw straight out of this await: past the
|
|
1657
|
+
// cancellation, past `allSettled`, and into a scope entry recorded as failed with no losers on
|
|
1658
|
+
// it. The run terminated while a sibling was still performing effects, which is the exact
|
|
1659
|
+
// defect the scope entry exists to prevent. A rejection is a settle.
|
|
1660
|
+
await Promise.race(running.map((p) => p.then(() => undefined, () => undefined)));
|
|
1661
|
+
const settled = await Promise.allSettled(running);
|
|
1662
|
+
// Every arm has settled, so whatever cut it did not get earlier no longer matters; the
|
|
1663
|
+
// signal still says cancelled, which is what a nested branch that outlives this line reads.
|
|
752
1664
|
for (const f of frames)
|
|
753
1665
|
f.signal.cancel("a sibling branch won the race");
|
|
754
|
-
await Promise.allSettled(running);
|
|
755
1666
|
frame.clock.join(frames.map((f) => f.clock));
|
|
756
|
-
|
|
1667
|
+
let winnerAt = -1;
|
|
1668
|
+
let winnerIndex = -1;
|
|
1669
|
+
for (let i = 0; i < settled.length; i += 1) {
|
|
1670
|
+
const r = settled[i];
|
|
1671
|
+
if (r.status === "rejected" && r.reason instanceof Cancelled)
|
|
1672
|
+
continue;
|
|
1673
|
+
const at = frames[i].clock.now();
|
|
1674
|
+
if (winnerIndex === -1 || at < winnerAt) {
|
|
1675
|
+
winnerAt = at;
|
|
1676
|
+
winnerIndex = i;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
if (winnerIndex === -1) {
|
|
1680
|
+
// Every arm was cancelled, so the race itself was: nothing here decided anything.
|
|
1681
|
+
const first = settled.find((r) => r.status === "rejected");
|
|
1682
|
+
throw first === undefined ? new Cancelled("every branch was cancelled") : first.reason;
|
|
1683
|
+
}
|
|
1684
|
+
const index = entries[winnerIndex]?.[0];
|
|
1685
|
+
const won = settled[winnerIndex];
|
|
1686
|
+
if (won.status === "rejected") {
|
|
1687
|
+
// The earliest branch to settle FAILED. The scope fails with it, carrying the siblings it
|
|
1688
|
+
// cancelled — a losing arm can crash before the cancellation reaches it, so the intent has
|
|
1689
|
+
// to travel with the outcome exactly as it does for a winning race.
|
|
1690
|
+
throw new ScopeFailed(won.reason, {
|
|
1691
|
+
branches,
|
|
1692
|
+
cancel: { losers: branches.filter((k) => k !== index), issued: false },
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
return {
|
|
1696
|
+
branches,
|
|
1697
|
+
// BOTH the index and the value. The index alone is not enough: an edit to an arm's returned
|
|
1698
|
+
// expression would resume as the new value with no divergence raised.
|
|
1699
|
+
value: { index, value: settled[winnerIndex].value },
|
|
1700
|
+
cancel: { losers: branches.filter((k) => k !== index), issued: false },
|
|
1701
|
+
};
|
|
757
1702
|
}
|
|
758
1703
|
if (name === "fanOut") {
|
|
759
1704
|
const items = first;
|
|
@@ -777,9 +1722,97 @@ class Interpreter {
|
|
|
777
1722
|
throw new RuntimeFault("L3024", `fanOut produced duplicate branch keys (${branchKeys.join(", ")}), so two branches would share one journal namespace and allocate the same step key with different inputs. Nothing has run yet: the keys are all evaluated before any branch launches, because rejecting after launch would be too late by exactly the side effects the check exists to prevent.`);
|
|
778
1723
|
}
|
|
779
1724
|
const frames = branchKeys.map((k) => frame.branch(scopeKind, scopeName, occurrence, k));
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1725
|
+
// A fanOut has no losers: every branch is a winner, so a migration's walk enters the ones the
|
|
1726
|
+
// recorded run actually had. A branch the new source no longer produces is simply not walked,
|
|
1727
|
+
// and its entries surface as orphans — which is the whole point of walking rather than
|
|
1728
|
+
// consuming.
|
|
1729
|
+
const walk = items
|
|
1730
|
+
.map((item, i) => [item, i])
|
|
1731
|
+
.filter(([, i]) => only === undefined || only.has(branchKeys[i]));
|
|
1732
|
+
// The same failure law as `parallel`: the first rejection cancels the siblings and the scope
|
|
1733
|
+
// fails with it, carrying the losers. Measured before this block: a rejecting branch threw out
|
|
1734
|
+
// of `Promise.all` alone, and every sibling went on performing effects against a scope whose
|
|
1735
|
+
// entry had already settled failed.
|
|
1736
|
+
let failed = null;
|
|
1737
|
+
const launched = walk.map(([item, i]) => fn(frames[i], [item, i]).catch((e) => {
|
|
1738
|
+
if (failed === null)
|
|
1739
|
+
failed = branchKeys[i];
|
|
1740
|
+
throw e;
|
|
1741
|
+
}));
|
|
1742
|
+
try {
|
|
1743
|
+
const results = await Promise.all(launched);
|
|
1744
|
+
frame.clock.join(frames.map((f) => f.clock));
|
|
1745
|
+
return { branches: branchKeys, value: results };
|
|
1746
|
+
}
|
|
1747
|
+
catch (e) {
|
|
1748
|
+
for (const f of frames)
|
|
1749
|
+
f.signal.cancel("a sibling branch failed");
|
|
1750
|
+
await Promise.allSettled(launched);
|
|
1751
|
+
frame.clock.join(frames.map((f) => f.clock));
|
|
1752
|
+
const losers = branchKeys.filter((k) => k !== failed);
|
|
1753
|
+
throw new ScopeFailed(e, { branches: branchKeys, cancel: { losers, issued: false } });
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
if (name === "conclave") {
|
|
1757
|
+
// A conclave is a scope AND an effect, and it gets ONE entry, of kind `conclave`, carrying
|
|
1758
|
+
// the durable answer to "is this sub-team still live". That answer is the explicit `closed`
|
|
1759
|
+
// FACT, not the entry's state: a body that failed after a clean close settles `failed`
|
|
1760
|
+
// exactly like one whose close never acknowledged, and only the fact separates them. Pending
|
|
1761
|
+
// means a close is still owed. The migrate table reads that fact — an orphaned conclave is
|
|
1762
|
+
// rejected unless the scope closed — so a second entry for the close would be a second thing
|
|
1763
|
+
// to keep in agreement with the first, and nothing needs it.
|
|
1764
|
+
const members = deepFreeze(first);
|
|
1765
|
+
const fn = (await this.evaluate(argNodes[1], env, frame));
|
|
1766
|
+
const channel = this.option(bag, "channel");
|
|
1767
|
+
const req = { members, ...(channel !== undefined ? { channel } : {}) };
|
|
1768
|
+
const handler = this.options.handler;
|
|
1769
|
+
const handle = deepFreeze(await handler.openConclave(req, ctx));
|
|
1770
|
+
// One body, one branch, and the branch key is the fixed literal `in` rather than the channel
|
|
1771
|
+
// name. The channel is HANDLER-DERIVED — the simulator and the mesh mint different ones — so
|
|
1772
|
+
// keying the journal namespace by it would make a journal replayable only under the handler
|
|
1773
|
+
// that wrote it, which is the one thing the effect seam exists to prevent.
|
|
1774
|
+
// ONE constant, used for both the namespace and the recorded branch list, so the entry cannot
|
|
1775
|
+
// claim a key the body's steps were not actually filed under.
|
|
1776
|
+
const branchKey = "in";
|
|
1777
|
+
const branch = frame.branch(scopeKind, scopeName, occurrence, branchKey);
|
|
1778
|
+
// The body's outcome is decided FIRST, alone. The close is a separate act with a separate
|
|
1779
|
+
// failure mode, and folding it into this try is what made a close rejection retry itself and
|
|
1780
|
+
// then settle as an ordinary body failure — a `failed` entry indistinguishable from "the body
|
|
1781
|
+
// failed and the room closed cleanly", which an orphan walk reads as closed while the members
|
|
1782
|
+
// are still joined.
|
|
1783
|
+
// `threw` is a separate flag rather than `bodyError !== undefined`, because `throw undefined`
|
|
1784
|
+
// is a thing a program may do and "the body failed" must not depend on what it failed WITH.
|
|
1785
|
+
let bodyError;
|
|
1786
|
+
let threw = false;
|
|
1787
|
+
let value;
|
|
1788
|
+
try {
|
|
1789
|
+
value = await fn(branch, [handle]);
|
|
1790
|
+
}
|
|
1791
|
+
catch (e) {
|
|
1792
|
+
bodyError = e;
|
|
1793
|
+
threw = true;
|
|
1794
|
+
}
|
|
1795
|
+
frame.clock.join([branch.clock]);
|
|
1796
|
+
// A CANCELLED branch performs no new effects, so a cancelled conclave does not close
|
|
1797
|
+
// itself: releasing the membership travels the same recovery path as every other branch-local
|
|
1798
|
+
// resource a race loser took. A conclave whose body merely FAILED is not cancelled —
|
|
1799
|
+
// this process is live and the world is reachable — and walking away from live membership on
|
|
1800
|
+
// an ordinary error would be the `spawn` leak in another shape.
|
|
1801
|
+
if (bodyError instanceof Cancelled)
|
|
1802
|
+
throw new ScopeFailed(bodyError, { closed: false });
|
|
1803
|
+
try {
|
|
1804
|
+
await handler.closeConclave(req, ctx);
|
|
1805
|
+
}
|
|
1806
|
+
catch (e) {
|
|
1807
|
+
// THE CLOSE DID NOT ACKNOWLEDGE, so the scope does not settle at all. A pending entry IS
|
|
1808
|
+
// the durable "a close is still owed" — re-entry retries it — and settling anything here
|
|
1809
|
+
// would be the journal claiming a disposition the world never confirmed. The body's own
|
|
1810
|
+
// error, if there was one, is subordinate: it did not leave members joined; this did.
|
|
1811
|
+
throw new CloseOwed(e);
|
|
1812
|
+
}
|
|
1813
|
+
if (threw)
|
|
1814
|
+
throw new ScopeFailed(bodyError, { closed: true });
|
|
1815
|
+
return { branches: [branchKey], value, closed: true };
|
|
783
1816
|
}
|
|
784
1817
|
throw new RuntimeFault("L1000", `${name} is not implemented in this interpreter`);
|
|
785
1818
|
}
|
|
@@ -792,6 +1825,17 @@ class Interpreter {
|
|
|
792
1825
|
inner.declare(s.id.name, this.makeFunction(s, inner), false);
|
|
793
1826
|
}
|
|
794
1827
|
}
|
|
1828
|
+
// `let`/`const` bind the whole block, holding the dead-zone marker until their line runs, so a
|
|
1829
|
+
// closure called early finds "declared, not yet initialized" (L2004) rather than an outer
|
|
1830
|
+
// binding of the same name — which is what JavaScript does, minus the host error class.
|
|
1831
|
+
for (const s of body) {
|
|
1832
|
+
if (s.type !== "VariableDeclaration")
|
|
1833
|
+
continue;
|
|
1834
|
+
for (const d of s.declarations ?? []) {
|
|
1835
|
+
for (const n of declaredNames(d.id))
|
|
1836
|
+
inner.declare(n, TDZ, s.kind === "let");
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
795
1839
|
for (const s of body) {
|
|
796
1840
|
const c = await this.execute(s, inner, frame);
|
|
797
1841
|
if (c.type !== "normal")
|
|
@@ -808,10 +1852,10 @@ class Interpreter {
|
|
|
808
1852
|
await this.evaluate(node.expression, env, frame);
|
|
809
1853
|
return NORMAL;
|
|
810
1854
|
case "VariableDeclaration": {
|
|
811
|
-
const
|
|
1855
|
+
const mode = node.kind === "let" ? "let" : "const";
|
|
812
1856
|
for (const d of node.declarations) {
|
|
813
1857
|
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,
|
|
1858
|
+
await this.bindPattern(d.id, init, env, frame, mode);
|
|
815
1859
|
}
|
|
816
1860
|
return NORMAL;
|
|
817
1861
|
}
|
|
@@ -837,9 +1881,22 @@ class Interpreter {
|
|
|
837
1881
|
return c;
|
|
838
1882
|
}
|
|
839
1883
|
case "ForStatement": {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1884
|
+
let loopEnv = new Env(env);
|
|
1885
|
+
const init = node.init;
|
|
1886
|
+
if (init !== null && init !== undefined) {
|
|
1887
|
+
if (init.type === "VariableDeclaration")
|
|
1888
|
+
await this.execute(init, loopEnv, frame);
|
|
1889
|
+
else
|
|
1890
|
+
await this.evaluate(init, loopEnv, frame);
|
|
1891
|
+
}
|
|
1892
|
+
// `for (let i ...)` gives EACH ITERATION its own `i`, as JavaScript does: a closure made in
|
|
1893
|
+
// one iteration keeps that iteration's value. The copy happens after the body and before the
|
|
1894
|
+
// update, which is where the specification puts it.
|
|
1895
|
+
const perIteration = [];
|
|
1896
|
+
if (init?.type === "VariableDeclaration" && init.kind === "let") {
|
|
1897
|
+
for (const d of init.declarations)
|
|
1898
|
+
collectNames(d.id, perIteration);
|
|
1899
|
+
}
|
|
843
1900
|
for (;;) {
|
|
844
1901
|
if (node.test !== null && node.test !== undefined && !(await this.evaluate(node.test, loopEnv, frame))) {
|
|
845
1902
|
return NORMAL;
|
|
@@ -849,17 +1906,24 @@ class Interpreter {
|
|
|
849
1906
|
return NORMAL;
|
|
850
1907
|
if (c.type === "return")
|
|
851
1908
|
return c;
|
|
1909
|
+
if (perIteration.length > 0)
|
|
1910
|
+
loopEnv = loopEnv.perIteration(perIteration);
|
|
852
1911
|
if (node.update !== null && node.update !== undefined)
|
|
853
1912
|
await this.evaluate(node.update, loopEnv, frame);
|
|
854
1913
|
}
|
|
855
1914
|
}
|
|
856
1915
|
case "ForOfStatement": {
|
|
857
|
-
const iterable = (await this.evaluate(node.right, env, frame));
|
|
1916
|
+
const iterable = this.spreadable(await this.evaluate(node.right, env, frame));
|
|
1917
|
+
const decl = node.left;
|
|
858
1918
|
for (const item of iterable) {
|
|
859
1919
|
const loopEnv = new Env(env);
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1920
|
+
if (decl.type === "VariableDeclaration") {
|
|
1921
|
+
const target = decl.declarations[0].id;
|
|
1922
|
+
await this.bindPattern(target, item, loopEnv, frame, decl.kind === "let" ? "let" : "const");
|
|
1923
|
+
}
|
|
1924
|
+
else {
|
|
1925
|
+
await this.bindPattern(decl, item, loopEnv, frame, "assign");
|
|
1926
|
+
}
|
|
863
1927
|
const c = await this.execute(node.body, loopEnv, frame);
|
|
864
1928
|
if (c.type === "break")
|
|
865
1929
|
return NORMAL;
|
|
@@ -880,49 +1944,108 @@ class Interpreter {
|
|
|
880
1944
|
case "ThrowStatement":
|
|
881
1945
|
throw await this.evaluate(node.argument, env, frame);
|
|
882
1946
|
case "TryStatement": {
|
|
1947
|
+
// What a `catch` may not have: none of these is a program error, and none is this
|
|
1948
|
+
// program's to handle. Three kinds, six classes.
|
|
1949
|
+
//
|
|
1950
|
+
// A cancellation is the scope being unwound, and swallowing it would keep a branch
|
|
1951
|
+
// working after it lost a race.
|
|
1952
|
+
//
|
|
1953
|
+
// A durability failure is the JOURNAL refusing to record: the run losing its ability to
|
|
1954
|
+
// have a result at all. A program that catches one goes on performing effects against the
|
|
1955
|
+
// world with nothing recorded from the refusal onward, so those effects exist only in the
|
|
1956
|
+
// world and a resume performs them again. An unrecordable run must stop, and no `catch`
|
|
1957
|
+
// may decide otherwise.
|
|
1958
|
+
//
|
|
1959
|
+
// And a DIVERGENCE, or a migration walk's refusal to enter a scope, is the journal saying
|
|
1960
|
+
// this program is not the one that wrote it. Measured before this line existed: a resume
|
|
1961
|
+
// whose edited `sleep` diverged inside a `try` caught `{ code: "L4000", kind: "host" }`,
|
|
1962
|
+
// logged past it, and performed a NEW effect against the journal it had just diverged
|
|
1963
|
+
// from; a migration's dry walk would have reported the same program clean.
|
|
1964
|
+
//
|
|
1965
|
+
// AND `finally` IS BOUND BY THE SAME LAW. A finalizer runs on the way out, so an
|
|
1966
|
+
// unconditional one handed the program a landing past every class above: measured, a
|
|
1967
|
+
// `finally` performed a NEW effect after a RunReleased and after a store rejection, and a
|
|
1968
|
+
// `finally { throw ... }` REPLACED a divergence, which an outer catch then swallowed as an
|
|
1969
|
+
// ordinary error. An uncatchable fault now unwinds past the finalizer too: the run's
|
|
1970
|
+
// continuation is forfeit, and that includes its cleanup — the world-side recovery belongs
|
|
1971
|
+
// to the driver and the journal, not to the program that just lost the right to run.
|
|
1972
|
+
const uncatchable = (e) => e instanceof Cancelled ||
|
|
1973
|
+
e instanceof JournalAppendRejected ||
|
|
1974
|
+
e instanceof RunReleased ||
|
|
1975
|
+
e instanceof RunDivergence ||
|
|
1976
|
+
e instanceof ScopeBranchMissing ||
|
|
1977
|
+
e instanceof UnwalkableScope;
|
|
1978
|
+
// JavaScript's completion semantics, which the one-`try` shape this replaced could not
|
|
1979
|
+
// express (measured: `try { return 1; } finally { return 2; }` returned 1): the finalizer
|
|
1980
|
+
// always runs for ordinary completions, and an ABRUPT finalizer completion — a return, a
|
|
1981
|
+
// break, a throw — replaces whatever the try or catch had decided.
|
|
1982
|
+
let completion = NORMAL;
|
|
1983
|
+
let pendingThrow;
|
|
1984
|
+
let hasThrow = false;
|
|
883
1985
|
try {
|
|
884
|
-
|
|
885
|
-
if (c.type !== "normal")
|
|
886
|
-
return c;
|
|
1986
|
+
completion = await this.execute(node.block, env, frame);
|
|
887
1987
|
}
|
|
888
1988
|
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)
|
|
1989
|
+
if (uncatchable(e))
|
|
892
1990
|
throw e;
|
|
893
1991
|
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);
|
|
1992
|
+
if (handlerNode === null || handlerNode === undefined) {
|
|
1993
|
+
hasThrow = true;
|
|
1994
|
+
pendingThrow = e;
|
|
899
1995
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1996
|
+
else {
|
|
1997
|
+
try {
|
|
1998
|
+
const catchEnv = new Env(env);
|
|
1999
|
+
if (handlerNode.param !== null && handlerNode.param !== undefined) {
|
|
2000
|
+
await this.bindPattern(handlerNode.param, toProgramError(e), catchEnv, frame, "const");
|
|
2001
|
+
}
|
|
2002
|
+
completion = await this.executeBlock(handlerNode.body, catchEnv, frame);
|
|
2003
|
+
}
|
|
2004
|
+
catch (ce) {
|
|
2005
|
+
if (uncatchable(ce))
|
|
2006
|
+
throw ce;
|
|
2007
|
+
hasThrow = true;
|
|
2008
|
+
pendingThrow = ce;
|
|
2009
|
+
}
|
|
907
2010
|
}
|
|
908
2011
|
}
|
|
909
|
-
|
|
2012
|
+
if (node.finalizer !== null && node.finalizer !== undefined) {
|
|
2013
|
+
// A throw inside the finalizer — its own, or an uncatchable — propagates from here,
|
|
2014
|
+
// replacing any pending completion, exactly as JavaScript replaces it.
|
|
2015
|
+
const f = await this.execute(node.finalizer, env, frame);
|
|
2016
|
+
if (f.type !== "normal")
|
|
2017
|
+
return f;
|
|
2018
|
+
}
|
|
2019
|
+
if (hasThrow)
|
|
2020
|
+
throw pendingThrow;
|
|
2021
|
+
return completion;
|
|
910
2022
|
}
|
|
911
2023
|
case "SwitchStatement": {
|
|
2024
|
+
// JavaScript's selection: the case tests are tried in source order, the `default` clause's
|
|
2025
|
+
// position is skipped during matching, and `default` is entered only when NO case matched.
|
|
2026
|
+
// The one-pass walk this replaced treated `default` as an immediate match, so a `default`
|
|
2027
|
+
// written above a matching case shadowed it (measured: `default` ran, `case 2` did not).
|
|
2028
|
+
// Execution then falls through from the selected clause in source order, `default`
|
|
2029
|
+
// included, exactly as JavaScript falls.
|
|
912
2030
|
const disc = await this.evaluate(node.discriminant, env, frame);
|
|
913
2031
|
const cases = node.cases;
|
|
914
2032
|
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)
|
|
2033
|
+
let start = -1;
|
|
2034
|
+
for (let i = 0; i < cases.length; i += 1) {
|
|
2035
|
+
const c = cases[i];
|
|
2036
|
+
if (c.test === null || c.test === undefined)
|
|
924
2037
|
continue;
|
|
925
|
-
|
|
2038
|
+
if ((await this.evaluate(c.test, switchEnv, frame)) === disc) {
|
|
2039
|
+
start = i;
|
|
2040
|
+
break;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
if (start === -1)
|
|
2044
|
+
start = cases.findIndex((c) => c.test === null || c.test === undefined);
|
|
2045
|
+
if (start === -1)
|
|
2046
|
+
return NORMAL;
|
|
2047
|
+
for (let i = start; i < cases.length; i += 1) {
|
|
2048
|
+
for (const s of cases[i].consequent ?? []) {
|
|
926
2049
|
const comp = await this.execute(s, switchEnv, frame);
|
|
927
2050
|
if (comp.type === "break")
|
|
928
2051
|
return NORMAL;
|
|
@@ -940,46 +2063,154 @@ class Interpreter {
|
|
|
940
2063
|
}
|
|
941
2064
|
}
|
|
942
2065
|
// ---- helpers -------------------------------------------------------------------------------------
|
|
2066
|
+
/** The names a declaration's pattern introduces, for the dead-zone pre-pass. */
|
|
2067
|
+
function declaredNames(pattern) {
|
|
2068
|
+
const out = [];
|
|
2069
|
+
const walk = (n) => {
|
|
2070
|
+
if (n === null || n === undefined)
|
|
2071
|
+
return;
|
|
2072
|
+
switch (n.type) {
|
|
2073
|
+
case "Identifier":
|
|
2074
|
+
out.push(n.name);
|
|
2075
|
+
return;
|
|
2076
|
+
case "ObjectPattern":
|
|
2077
|
+
for (const p of n.properties ?? [])
|
|
2078
|
+
walk((p.type === "RestElement" ? p.argument : p.value));
|
|
2079
|
+
return;
|
|
2080
|
+
case "ArrayPattern":
|
|
2081
|
+
for (const el of n.elements ?? [])
|
|
2082
|
+
walk(el);
|
|
2083
|
+
return;
|
|
2084
|
+
case "AssignmentPattern":
|
|
2085
|
+
walk(n.left);
|
|
2086
|
+
return;
|
|
2087
|
+
case "RestElement":
|
|
2088
|
+
walk(n.argument);
|
|
2089
|
+
return;
|
|
2090
|
+
default:
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
walk(pattern);
|
|
2095
|
+
return out;
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* The binary operators, with JavaScript's meaning ON PRIMITIVES. `"a" + 1`, `true + 1` and
|
|
2099
|
+
* `null + 1` mean here exactly what they mean in JavaScript — primitive coercion is pure and
|
|
2100
|
+
* deterministic. A record, an array or a function operand is refused (L4018), a declared
|
|
2101
|
+
* difference: JavaScript would reach for the host's ToPrimitive machinery, which reads `valueOf`/
|
|
2102
|
+
* `toString` off the value — own fields a program can set to its OWN closures. Measured before the
|
|
2103
|
+
* refusal: `o + 1` invoked such a closure without an interpreter frame and crashed with a raw host
|
|
2104
|
+
* TypeError, and without one it silently produced `"[object Object]1"`. `==` and `!=` never reach
|
|
2105
|
+
* this function: the validator refuses them (L1025). `===`/`!==` compare identity and take any
|
|
2106
|
+
* operands.
|
|
2107
|
+
*/
|
|
2108
|
+
/** Refuse a container or function where a primitive is needed: there is no implicit conversion. */
|
|
2109
|
+
function refuseCoercion(where, v) {
|
|
2110
|
+
if (v !== null && (typeof v === "object" || typeof v === "function")) {
|
|
2111
|
+
const kind = typeof v === "function" ? "a function" : Array.isArray(v) ? "an array" : "a record";
|
|
2112
|
+
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.`);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
943
2115
|
function applyBinary(op, l, r) {
|
|
2116
|
+
const a = l;
|
|
2117
|
+
const b = r;
|
|
944
2118
|
switch (op) {
|
|
945
2119
|
case "===":
|
|
946
|
-
return
|
|
2120
|
+
return l === r;
|
|
947
2121
|
case "!==":
|
|
948
|
-
return
|
|
2122
|
+
return l !== r;
|
|
2123
|
+
default:
|
|
2124
|
+
break;
|
|
2125
|
+
}
|
|
2126
|
+
refuseCoercion(op, l);
|
|
2127
|
+
refuseCoercion(op, r);
|
|
2128
|
+
switch (op) {
|
|
949
2129
|
case "<":
|
|
950
|
-
return
|
|
2130
|
+
return a < b;
|
|
951
2131
|
case "<=":
|
|
952
|
-
return
|
|
2132
|
+
return a <= b;
|
|
953
2133
|
case ">":
|
|
954
|
-
return
|
|
2134
|
+
return a > b;
|
|
955
2135
|
case ">=":
|
|
956
|
-
return
|
|
2136
|
+
return a >= b;
|
|
957
2137
|
case "+":
|
|
958
|
-
return
|
|
959
|
-
? String(l) + String(r)
|
|
960
|
-
: l + r;
|
|
2138
|
+
return a + b;
|
|
961
2139
|
case "-":
|
|
962
|
-
return
|
|
2140
|
+
return a - b;
|
|
963
2141
|
case "*":
|
|
964
|
-
return
|
|
2142
|
+
return a * b;
|
|
965
2143
|
case "/":
|
|
966
|
-
return
|
|
2144
|
+
return a / b;
|
|
967
2145
|
case "%":
|
|
968
|
-
return
|
|
2146
|
+
return a % b;
|
|
2147
|
+
case "**":
|
|
2148
|
+
return a ** b;
|
|
2149
|
+
case "&":
|
|
2150
|
+
return a & b;
|
|
2151
|
+
case "|":
|
|
2152
|
+
return a | b;
|
|
2153
|
+
case "^":
|
|
2154
|
+
return a ^ b;
|
|
2155
|
+
case "<<":
|
|
2156
|
+
return a << b;
|
|
2157
|
+
case ">>":
|
|
2158
|
+
return a >> b;
|
|
2159
|
+
case ">>>":
|
|
2160
|
+
return a >>> b;
|
|
969
2161
|
default:
|
|
970
2162
|
throw new RuntimeFault("L1000", `unsupported operator ${op}`);
|
|
971
2163
|
}
|
|
972
2164
|
}
|
|
973
|
-
/**
|
|
2165
|
+
/** A canonical array index (`"0"`, `"12"`), as a number, or nothing. */
|
|
2166
|
+
function arrayIndex(prop) {
|
|
2167
|
+
if (!/^(0|[1-9][0-9]*)$/.test(prop))
|
|
2168
|
+
return undefined;
|
|
2169
|
+
const n = Number(prop);
|
|
2170
|
+
return n <= 4294967294 ? n : undefined;
|
|
2171
|
+
}
|
|
2172
|
+
/** The names a binding pattern introduces. */
|
|
2173
|
+
function collectNames(pattern, out) {
|
|
2174
|
+
switch (pattern.type) {
|
|
2175
|
+
case "Identifier":
|
|
2176
|
+
out.push(pattern.name);
|
|
2177
|
+
return;
|
|
2178
|
+
case "AssignmentPattern":
|
|
2179
|
+
collectNames(pattern.left, out);
|
|
2180
|
+
return;
|
|
2181
|
+
case "RestElement":
|
|
2182
|
+
collectNames(pattern.argument, out);
|
|
2183
|
+
return;
|
|
2184
|
+
case "ObjectPattern":
|
|
2185
|
+
for (const p of pattern.properties)
|
|
2186
|
+
collectNames((p.type === "RestElement" ? p.argument : p.value), out);
|
|
2187
|
+
return;
|
|
2188
|
+
case "ArrayPattern":
|
|
2189
|
+
for (const el of pattern.elements)
|
|
2190
|
+
if (el !== null && el !== undefined)
|
|
2191
|
+
collectNames(el, out);
|
|
2192
|
+
return;
|
|
2193
|
+
default:
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
/** The optional-chain short-circuit. Private to `evaluate`; see the `ChainExpression` case. */
|
|
2198
|
+
const SHORT_CIRCUIT = Symbol("cotal-lang short circuit");
|
|
2199
|
+
/**
|
|
2200
|
+
* What a `catch` block sees. A failure the RUNTIME raised (an effect's error, an interpreter fault)
|
|
2201
|
+
* arrives as a plain record carrying its code, because programs branch on data, not on classes; a
|
|
2202
|
+
* value the PROGRAM threw arrives as itself, whatever it is, exactly as JavaScript delivers it. A
|
|
2203
|
+
* program cannot construct an `Error`, so anything that is one came from the runtime or the host.
|
|
2204
|
+
*/
|
|
974
2205
|
function toProgramError(e) {
|
|
975
2206
|
if (e instanceof EffectError) {
|
|
976
2207
|
return deepFreeze({ code: e.code, kind: e.kind, message: e.message, ...(e.detail !== undefined ? { detail: e.detail } : {}) });
|
|
977
2208
|
}
|
|
978
2209
|
if (e instanceof RuntimeFault)
|
|
979
2210
|
return deepFreeze({ code: e.code, kind: "runtime", message: e.message });
|
|
980
|
-
if (e
|
|
981
|
-
return e;
|
|
982
|
-
return
|
|
2211
|
+
if (e instanceof Error)
|
|
2212
|
+
return deepFreeze({ code: "L4000", kind: "host", message: e.message });
|
|
2213
|
+
return e;
|
|
983
2214
|
}
|
|
984
2215
|
// ---- the public entry point -------------------------------------------------------------------------
|
|
985
2216
|
/**
|
|
@@ -990,16 +2221,39 @@ function toProgramError(e) {
|
|
|
990
2221
|
*/
|
|
991
2222
|
export async function run(source, options) {
|
|
992
2223
|
const { ast } = validate(source, options.file);
|
|
993
|
-
const programHash =
|
|
994
|
-
|
|
995
|
-
|
|
2224
|
+
const programHash = programHashOf(source);
|
|
2225
|
+
// A resume is handed the pins the run STARTED under and binds to them; a fresh run resolves them
|
|
2226
|
+
// once, here, and hands them back for the run record.
|
|
2227
|
+
//
|
|
2228
|
+
// AND A RESUME MAY NOT DECLINE TO SAY WHICH RUN IT IS RESUMING. Re-resolving the pins for a run
|
|
2229
|
+
// handed history but none is not a smaller version of the right behaviour: it is a different run
|
|
2230
|
+
// wearing the same journal. The clock moves to the RESUMING host and the seed falls back to the
|
|
2231
|
+
// runId default, so both the logical epoch and every pure draw change, and nothing refuses,
|
|
2232
|
+
// because nothing can. Pure draws are not journalled and the epoch is not a recorded fact, so
|
|
2233
|
+
// there is no divergence for the replay to catch.
|
|
2234
|
+
//
|
|
2235
|
+
// A journal with NO entries is a different thing and stays allowed: that is a FRESH run being
|
|
2236
|
+
// handed a journal for its store, not a resume.
|
|
2237
|
+
if (options.pins === undefined && options.journal !== undefined && options.journal.entries().length > 0) {
|
|
2238
|
+
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 `
|
|
2239
|
+
+ `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 `
|
|
2240
|
+
+ `written for it — silently, because neither the clock nor a pure draw is a recorded fact the replay could diverge on.\n\n`
|
|
2241
|
+
+ `Options\n pass the pins from the run record\n start a fresh run instead of resuming this journal`);
|
|
2242
|
+
}
|
|
2243
|
+
const pins = options.pins !== undefined ? bindPins(options.pins, options) : resolvePins(options, options.handler.now());
|
|
2244
|
+
const interp = new Interpreter(ast, options, programHash, pins);
|
|
2245
|
+
// The run clock starts at the run's LOGICAL epoch, not at this host's clock: a run resumed on
|
|
2246
|
+
// another machine hours later must see the same `now()` before its first effect as the run that
|
|
2247
|
+
// wrote the journal, or the branch it takes is a property of when it was resumed.
|
|
2248
|
+
const frame = new Frame(new KeyScope(), new RunClock(pins.startedAt), new Signal());
|
|
996
2249
|
const env = new Env(null);
|
|
997
|
-
installGlobals(env, interp
|
|
2250
|
+
installGlobals(env, interp);
|
|
998
2251
|
const completion = await interp.executeBlock(ast, env, frame);
|
|
999
2252
|
return {
|
|
1000
2253
|
value: completion.type === "return" ? completion.value : undefined,
|
|
1001
2254
|
journal: interp.journal,
|
|
1002
2255
|
programHash,
|
|
2256
|
+
pins,
|
|
1003
2257
|
steps: interp.stepCount,
|
|
1004
2258
|
};
|
|
1005
2259
|
}
|
|
@@ -1008,110 +2262,32 @@ export async function resume(source, journal, options) {
|
|
|
1008
2262
|
journal.resetConsumed();
|
|
1009
2263
|
return await run(source, { ...options, journal });
|
|
1010
2264
|
}
|
|
1011
|
-
function installGlobals(env, interp
|
|
2265
|
+
function installGlobals(env, interp) {
|
|
1012
2266
|
const fn = (impl) => async (frame, args) => impl(frame, args);
|
|
2267
|
+
// The value names. `undefined` is a value the runtime produces, so a program can name it.
|
|
2268
|
+
for (const name of VALUE_NAMES)
|
|
2269
|
+
env.declare(name, undefined, false);
|
|
1013
2270
|
// Pure primitives: a channel name is a name, so naming one costs nothing and journals nothing.
|
|
1014
|
-
|
|
1015
|
-
env.declare("
|
|
2271
|
+
// Handles are opaque frozen records the runtime mints (design §4).
|
|
2272
|
+
env.declare("channel", fn((_f, a) => deepFreeze({ channel: a[0] })), false);
|
|
2273
|
+
env.declare("run", fn(() => deepFreeze({ id: interp.options.runId, programHash: interp.programHash, startedAt: interp.pins.startedAt })), false);
|
|
1016
2274
|
// Event constructors are pure descriptors; awaiting them is `wait`.
|
|
1017
|
-
env.declare("replied", fn((_f, a) => ({ event: "replied", agent: a[0].agent })), false);
|
|
2275
|
+
env.declare("replied", fn((_f, a) => deepFreeze({ event: "replied", agent: a[0].agent })), false);
|
|
1018
2276
|
env.declare("message", fn((_f, a) => {
|
|
1019
2277
|
const ch = a[0].channel;
|
|
1020
2278
|
const opts = (a[1] ?? {});
|
|
1021
|
-
return {
|
|
2279
|
+
return deepFreeze({
|
|
1022
2280
|
event: "message",
|
|
1023
2281
|
channel: ch,
|
|
1024
2282
|
...(opts.from !== undefined ? { from: opts.from.agent } : {}),
|
|
1025
2283
|
...(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;
|
|
2284
|
+
});
|
|
1113
2285
|
}), false);
|
|
1114
|
-
|
|
2286
|
+
env.declare("idle", fn((_f, a) => deepFreeze({ event: "idle", channel: a[0].channel, duration: a[1] })), false);
|
|
2287
|
+
env.declare("down", fn((_f, a) => deepFreeze({ event: "down", agent: a[0].agent })), false);
|
|
2288
|
+
// The builtin library (design §4), one table in library.ts.
|
|
2289
|
+
for (const [name, value] of builtins(interp.libraryContext()))
|
|
2290
|
+
env.declare(name, value, false);
|
|
1115
2291
|
}
|
|
1116
2292
|
export { LangError, LangErrors };
|
|
1117
2293
|
//# sourceMappingURL=interpret.js.map
|