@cotal-ai/lang 0.23.0 → 0.25.0

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