@cotal-ai/lang 0.0.0 → 0.15.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 (51) hide show
  1. package/LICENSE +202 -0
  2. package/dist/dryrun.d.ts +106 -0
  3. package/dist/dryrun.d.ts.map +1 -0
  4. package/dist/dryrun.js +172 -0
  5. package/dist/dryrun.js.map +1 -0
  6. package/dist/duration.d.ts +12 -0
  7. package/dist/duration.d.ts.map +1 -0
  8. package/dist/duration.js +34 -0
  9. package/dist/duration.js.map +1 -0
  10. package/dist/effects.d.ts +231 -0
  11. package/dist/effects.d.ts.map +1 -0
  12. package/dist/effects.js +64 -0
  13. package/dist/effects.js.map +1 -0
  14. package/dist/errors.d.ts +141 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/dist/errors.js +163 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/grammar.d.ts +28 -0
  19. package/dist/grammar.d.ts.map +1 -0
  20. package/dist/grammar.js +793 -0
  21. package/dist/grammar.js.map +1 -0
  22. package/dist/index.d.ts +23 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +23 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/interpret.d.ts +89 -0
  27. package/dist/interpret.d.ts.map +1 -0
  28. package/dist/interpret.js +1117 -0
  29. package/dist/interpret.js.map +1 -0
  30. package/dist/journal.d.ts +177 -0
  31. package/dist/journal.d.ts.map +1 -0
  32. package/dist/journal.js +198 -0
  33. package/dist/journal.js.map +1 -0
  34. package/dist/keys.d.ts +87 -0
  35. package/dist/keys.d.ts.map +1 -0
  36. package/dist/keys.js +105 -0
  37. package/dist/keys.js.map +1 -0
  38. package/dist/primitives.d.ts +84 -0
  39. package/dist/primitives.d.ts.map +1 -0
  40. package/dist/primitives.js +265 -0
  41. package/dist/primitives.js.map +1 -0
  42. package/dist/sim.d.ts +101 -0
  43. package/dist/sim.d.ts.map +1 -0
  44. package/dist/sim.js +192 -0
  45. package/dist/sim.js.map +1 -0
  46. package/dist/values.d.ts +35 -0
  47. package/dist/values.d.ts.map +1 -0
  48. package/dist/values.js +0 -0
  49. package/dist/values.js.map +1 -0
  50. package/package.json +27 -7
  51. package/README.md +0 -10
@@ -0,0 +1,1117 @@
1
+ /**
2
+ * The interpreter: an AST walk over a validated program.
3
+ *
4
+ * Two properties make this worth reading closely, because everything else in the durability story
5
+ * rests on them:
6
+ *
7
+ * 1. **The interpreter owns the journal; the handler never touches it.** Every effect goes through
8
+ * {@link Interpreter.performEffect}, which allocates the key, consults the journal, and either
9
+ * replays a recorded result or performs the effect live and records it. That is why simulation
10
+ * and production cannot drift apart on durability: neither handler is in a position to.
11
+ * 2. **Resume is re-running from the top.** There is no cursor and no fast-forward. Journalled
12
+ * effects return their recorded results, so the deterministic prefix reproduces itself and
13
+ * out-of-order concurrency replays correctly. This is the same thing as an effect handler's
14
+ * resume(), implemented by re-running the pure prefix, which is why no continuation is ever
15
+ * serialized.
16
+ */
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";
22
+ import { parseDuration } from "./duration.js";
23
+ import { PRIMITIVES } from "./primitives.js";
24
+ import { Cancelled, EffectError, applyCheckpointPolicy, } from "./effects.js";
25
+ // ---- environments ------------------------------------------------------------------------------
26
+ class Binding {
27
+ value;
28
+ mutable;
29
+ constructor(value, mutable) {
30
+ this.value = value;
31
+ this.mutable = mutable;
32
+ }
33
+ }
34
+ class Env {
35
+ parent;
36
+ names = new Map();
37
+ constructor(parent) {
38
+ this.parent = parent;
39
+ }
40
+ declare(name, value, mutable) {
41
+ this.names.set(name, new Binding(value, mutable));
42
+ }
43
+ find(name) {
44
+ for (let e = this; e !== null; e = e.parent) {
45
+ const b = e.names.get(name);
46
+ if (b !== undefined)
47
+ return b;
48
+ }
49
+ return undefined;
50
+ }
51
+ get(name) {
52
+ const b = this.find(name);
53
+ if (b === undefined)
54
+ throw new RuntimeFault("L2001", `${name} is not defined`);
55
+ return b.value;
56
+ }
57
+ has(name) {
58
+ return this.find(name) !== undefined;
59
+ }
60
+ set(name, value) {
61
+ const b = this.find(name);
62
+ if (b === undefined)
63
+ throw new RuntimeFault("L2001", `${name} is not defined`);
64
+ if (!b.mutable)
65
+ throw new RuntimeFault("L2003", `${name} is declared const`);
66
+ b.value = value;
67
+ }
68
+ }
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";
76
+ }
77
+ }
78
+ const NORMAL = { type: "normal" };
79
+ // ---- per-branch execution state ---------------------------------------------------------------------
80
+ class Signal {
81
+ cancelled = false;
82
+ reason;
83
+ listeners = [];
84
+ onCancel(fn) {
85
+ this.listeners.push(fn);
86
+ }
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);
94
+ }
95
+ child() {
96
+ const s = new Signal();
97
+ if (this.cancelled)
98
+ s.cancel(this.reason ?? "parent cancelled");
99
+ else
100
+ this.onCancel((r) => s.cancel(r));
101
+ return s;
102
+ }
103
+ }
104
+ /**
105
+ * One branch of execution: its own key namespace, its own clock, its own cancellation signal.
106
+ *
107
+ * The key namespace is the reason concurrency is safe here. Two branches calling the same named
108
+ * effect cannot race for an occurrence counter, because they do not share one.
109
+ */
110
+ class Frame {
111
+ keys;
112
+ clock;
113
+ signal;
114
+ constructor(keys, clock, signal) {
115
+ this.keys = keys;
116
+ this.clock = clock;
117
+ this.signal = signal;
118
+ }
119
+ 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";
134
+ }
135
+ }
136
+ class Interpreter {
137
+ ast;
138
+ options;
139
+ programHash;
140
+ journal;
141
+ prng;
142
+ effectCount = 0;
143
+ ceiling;
144
+ steps = 0;
145
+ nextYield;
146
+ stepBudget;
147
+ yieldEvery;
148
+ constructor(ast, options, programHash) {
149
+ this.ast = ast;
150
+ this.options = options;
151
+ this.programHash = programHash;
152
+ 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;
157
+ this.nextYield = this.yieldEvery;
158
+ }
159
+ // ---- the fuel ceiling -----------------------------------------------------------------------
160
+ /**
161
+ * Charge one walker dispatch.
162
+ *
163
+ * Returns null on the common path and a promise only when it is time to breathe, so a dispatch
164
+ * normally costs an increment and a compare rather than an allocated promise. Callers await the
165
+ * result only when it is non-null, which is why this is not simply an async method.
166
+ */
167
+ tick(frame) {
168
+ this.steps += 1;
169
+ 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.`);
171
+ }
172
+ if (this.steps < this.nextYield)
173
+ return null;
174
+ this.nextYield = this.steps + this.yieldEvery;
175
+ return this.breathe(frame);
176
+ }
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
+ get stepCount() {
188
+ return this.steps;
189
+ }
190
+ async breathe(frame) {
191
+ await new Promise((resolve) => {
192
+ setTimeout(resolve, 0);
193
+ });
194
+ if (frame.signal.cancelled)
195
+ throw new Cancelled(frame.signal.reason ?? "cancelled");
196
+ }
197
+ // ---- the effect seam ------------------------------------------------------------------------
198
+ /**
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.
203
+ */
204
+ 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
+ }
295
+ }
296
+ // ---- expressions ------------------------------------------------------------------------------
297
+ async evaluate(node, env, frame) {
298
+ const pause = this.tick(frame);
299
+ if (pause !== null)
300
+ await pause;
301
+ switch (node.type) {
302
+ case "Literal":
303
+ return node.value;
304
+ case "Identifier":
305
+ return env.get(node.name);
306
+ case "TemplateLiteral": {
307
+ const quasis = node.quasis;
308
+ const exprs = node.expressions;
309
+ let out = "";
310
+ for (let i = 0; i < quasis.length; i += 1) {
311
+ out += quasis[i].value.cooked;
312
+ if (i < exprs.length)
313
+ out += String(await this.evaluate(exprs[i], env, frame));
314
+ }
315
+ return out;
316
+ }
317
+ case "ArrayExpression": {
318
+ const out = [];
319
+ 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 {
325
+ out.push(await this.evaluate(el, env, frame));
326
+ }
327
+ }
328
+ return out;
329
+ }
330
+ case "ObjectExpression": {
331
+ const out = {};
332
+ for (const p of node.properties ?? []) {
333
+ if (p.type === "SpreadElement") {
334
+ Object.assign(out, await this.evaluate(p.argument, env, frame));
335
+ continue;
336
+ }
337
+ const key = p.key;
338
+ const name = key.type === "Identifier" ? key.name : String(key.value);
339
+ out[name] = await this.evaluate(p.value, env, frame);
340
+ }
341
+ return out;
342
+ }
343
+ case "MemberExpression": {
344
+ const obj = await this.evaluate(node.object, env, frame);
345
+ if (obj === null || obj === undefined) {
346
+ if (node.optional === true)
347
+ return undefined;
348
+ throw new RuntimeFault("L4010", `cannot read a field of ${String(obj)}`);
349
+ }
350
+ const prop = node.computed === true
351
+ ? String(await this.evaluate(node.property, env, frame))
352
+ : node.property.name;
353
+ return obj[prop];
354
+ }
355
+ case "UnaryExpression": {
356
+ const v = await this.evaluate(node.argument, env, frame);
357
+ switch (node.operator) {
358
+ case "!":
359
+ return !v;
360
+ case "-":
361
+ return -v;
362
+ case "+":
363
+ return +v;
364
+ case "typeof":
365
+ return typeof v;
366
+ default:
367
+ throw new RuntimeFault("L1000", `unsupported unary operator ${String(node.operator)}`);
368
+ }
369
+ }
370
+ case "BinaryExpression": {
371
+ const l = await this.evaluate(node.left, env, frame);
372
+ const r = await this.evaluate(node.right, env, frame);
373
+ return applyBinary(node.operator, l, r);
374
+ }
375
+ case "LogicalExpression": {
376
+ const l = await this.evaluate(node.left, env, frame);
377
+ switch (node.operator) {
378
+ case "&&":
379
+ return l ? await this.evaluate(node.right, env, frame) : l;
380
+ case "||":
381
+ return l ? l : await this.evaluate(node.right, env, frame);
382
+ case "??":
383
+ // Orc's `otherwise`, spelled the way JavaScript already spells it: an event that
384
+ // halted without a result resolves null, and this is the recovery path.
385
+ return l === null || l === undefined
386
+ ? await this.evaluate(node.right, env, frame)
387
+ : l;
388
+ default:
389
+ throw new RuntimeFault("L1000", `unsupported logical operator ${String(node.operator)}`);
390
+ }
391
+ }
392
+ case "ConditionalExpression":
393
+ return (await this.evaluate(node.test, env, frame))
394
+ ? await this.evaluate(node.consequent, env, frame)
395
+ : 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
+ }
405
+ case "AwaitExpression":
406
+ return await this.evaluate(node.argument, env, frame);
407
+ case "ArrowFunctionExpression":
408
+ case "FunctionExpression":
409
+ return this.makeFunction(node, env);
410
+ case "CallExpression":
411
+ return await this.call(node, env, frame);
412
+ default:
413
+ throw new RuntimeFault("L1000", `unsupported expression ${node.type}`);
414
+ }
415
+ }
416
+ makeFunction(node, closure) {
417
+ const params = node.params ?? [];
418
+ const body = node.body;
419
+ const isExpressionBody = body.type !== "BlockStatement";
420
+ return async (frame, args) => {
421
+ const env = new Env(closure);
422
+ for (let i = 0; i < params.length; i += 1) {
423
+ await this.bindPattern(params[i], args[i], env, frame, true);
424
+ }
425
+ if (isExpressionBody)
426
+ return await this.evaluate(body, env, frame);
427
+ const c = await this.executeBlock(body, env, frame);
428
+ return c.type === "return" ? c.value : undefined;
429
+ };
430
+ }
431
+ async bindPattern(pattern, value, env, frame, mutable) {
432
+ switch (pattern.type) {
433
+ case "Identifier":
434
+ env.declare(pattern.name, value, mutable);
435
+ return;
436
+ case "AssignmentPattern":
437
+ await this.bindPattern(pattern.left, value === undefined ? await this.evaluate(pattern.right, env, frame) : value, env, frame, mutable);
438
+ return;
439
+ case "ObjectPattern": {
440
+ const src = (value ?? {});
441
+ const taken = [];
442
+ for (const p of pattern.properties) {
443
+ if (p.type === "RestElement") {
444
+ const rest = {};
445
+ for (const [k, v] of Object.entries(src))
446
+ if (!taken.includes(k))
447
+ rest[k] = v;
448
+ await this.bindPattern(p.argument, rest, env, frame, mutable);
449
+ continue;
450
+ }
451
+ const key = p.key;
452
+ const name = key.type === "Identifier" ? key.name : String(key.value);
453
+ taken.push(name);
454
+ await this.bindPattern(p.value, src[name], env, frame, mutable);
455
+ }
456
+ return;
457
+ }
458
+ case "ArrayPattern": {
459
+ const src = (value ?? []);
460
+ const els = pattern.elements;
461
+ for (let i = 0; i < els.length; i += 1) {
462
+ const el = els[i];
463
+ if (el === null || el === undefined)
464
+ continue;
465
+ if (el.type === "RestElement") {
466
+ await this.bindPattern(el.argument, src.slice(i), env, frame, mutable);
467
+ break;
468
+ }
469
+ await this.bindPattern(el, src[i], env, frame, mutable);
470
+ }
471
+ return;
472
+ }
473
+ default:
474
+ throw new RuntimeFault("L1000", `unsupported binding pattern ${pattern.type}`);
475
+ }
476
+ }
477
+ // ---- calls ----------------------------------------------------------------------------------
478
+ async call(node, env, frame) {
479
+ const callee = node.callee;
480
+ const argNodes = node.arguments ?? [];
481
+ // A primitive is dispatched by NAME, not by value. The validator forbids shadowing one, so a
482
+ // call spelled `turn` is always the effect, which is what keeps the flowchart projection and
483
+ // the linter sound.
484
+ if (callee.type === "Identifier" && PRIMITIVES[callee.name] !== undefined && !env.has(callee.name)) {
485
+ return await this.callPrimitive(callee.name, argNodes, env, frame);
486
+ }
487
+ const fn = await this.evaluate(callee, env, frame);
488
+ const args = [];
489
+ for (const a of argNodes) {
490
+ if (a.type === "SpreadElement")
491
+ args.push(...(await this.evaluate(a.argument, env, frame)));
492
+ else
493
+ args.push(await this.evaluate(a, env, frame));
494
+ }
495
+ if (typeof fn !== "function") {
496
+ throw new RuntimeFault("L4011", `this value is not a function, so it cannot be called`);
497
+ }
498
+ return await fn(frame, args);
499
+ }
500
+ option(bag, key) {
501
+ return bag === null || typeof bag !== "object" ? undefined : bag[key];
502
+ }
503
+ async callPrimitive(name, argNodes, env, frame) {
504
+ const spec = PRIMITIVES[name];
505
+ if (spec === undefined)
506
+ throw new RuntimeFault("L2001", `${name} is not a primitive`);
507
+ // Concurrency combinators take their branches unevaluated: the thunks must run inside their
508
+ // own frames, so evaluating them here would defeat the whole point.
509
+ if (spec.opensScope)
510
+ return await this.callScope(name, argNodes, env, frame);
511
+ const args = [];
512
+ for (const a of argNodes)
513
+ 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`);
705
+ }
706
+ }
707
+ /**
708
+ * The concurrency combinators.
709
+ *
710
+ * Each pushes a scope frame whose occurrence is allocated HERE, synchronously, in code that is
711
+ * already deterministic. Each branch then gets its own key namespace, so two branches running
712
+ * the same named effect cannot race for a counter, and replay reproduces both regardless of
713
+ * which one finished first.
714
+ */
715
+ async callScope(name, argNodes, env, frame) {
716
+ const spec = PRIMITIVES[name];
717
+ if (spec === undefined)
718
+ throw new RuntimeFault("L2001", `${name} is not a primitive`);
719
+ const scopeKind = name;
720
+ const first = await this.evaluate(argNodes[0], env, frame);
721
+ const bagNode = argNodes[spec.optionsAt];
722
+ const bag = bagNode === undefined ? undefined : await this.evaluate(bagNode, env, frame);
723
+ const scopeName = this.option(bag, "name") ?? null;
724
+ 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));
775
+ }
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`);
785
+ }
786
+ // ---- statements --------------------------------------------------------------------------------
787
+ async executeBlock(block, env, frame) {
788
+ const inner = new Env(env);
789
+ const body = block.body ?? [];
790
+ for (const s of body) {
791
+ if (s.type === "FunctionDeclaration") {
792
+ inner.declare(s.id.name, this.makeFunction(s, inner), false);
793
+ }
794
+ }
795
+ for (const s of body) {
796
+ const c = await this.execute(s, inner, frame);
797
+ if (c.type !== "normal")
798
+ return c;
799
+ }
800
+ return NORMAL;
801
+ }
802
+ async execute(node, env, frame) {
803
+ const pause = this.tick(frame);
804
+ if (pause !== null)
805
+ await pause;
806
+ switch (node.type) {
807
+ case "ExpressionStatement":
808
+ await this.evaluate(node.expression, env, frame);
809
+ return NORMAL;
810
+ case "VariableDeclaration": {
811
+ const mutable = node.kind === "let";
812
+ for (const d of node.declarations) {
813
+ 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);
815
+ }
816
+ return NORMAL;
817
+ }
818
+ case "FunctionDeclaration":
819
+ return NORMAL; // hoisted by executeBlock
820
+ case "BlockStatement":
821
+ return await this.executeBlock(node, env, frame);
822
+ case "IfStatement":
823
+ if (await this.evaluate(node.test, env, frame)) {
824
+ return await this.execute(node.consequent, env, frame);
825
+ }
826
+ return node.alternate === null || node.alternate === undefined
827
+ ? NORMAL
828
+ : await this.execute(node.alternate, env, frame);
829
+ case "WhileStatement":
830
+ for (;;) {
831
+ if (!(await this.evaluate(node.test, env, frame)))
832
+ return NORMAL;
833
+ const c = await this.execute(node.body, env, frame);
834
+ if (c.type === "break")
835
+ return NORMAL;
836
+ if (c.type === "return")
837
+ return c;
838
+ }
839
+ case "ForStatement": {
840
+ const loopEnv = new Env(env);
841
+ if (node.init !== null && node.init !== undefined)
842
+ await this.execute(node.init, loopEnv, frame);
843
+ for (;;) {
844
+ if (node.test !== null && node.test !== undefined && !(await this.evaluate(node.test, loopEnv, frame))) {
845
+ return NORMAL;
846
+ }
847
+ const c = await this.execute(node.body, loopEnv, frame);
848
+ if (c.type === "break")
849
+ return NORMAL;
850
+ if (c.type === "return")
851
+ return c;
852
+ if (node.update !== null && node.update !== undefined)
853
+ await this.evaluate(node.update, loopEnv, frame);
854
+ }
855
+ }
856
+ case "ForOfStatement": {
857
+ const iterable = (await this.evaluate(node.right, env, frame));
858
+ for (const item of iterable) {
859
+ 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");
863
+ const c = await this.execute(node.body, loopEnv, frame);
864
+ if (c.type === "break")
865
+ return NORMAL;
866
+ if (c.type === "return")
867
+ return c;
868
+ }
869
+ return NORMAL;
870
+ }
871
+ case "ReturnStatement":
872
+ return {
873
+ type: "return",
874
+ value: node.argument === null || node.argument === undefined ? undefined : await this.evaluate(node.argument, env, frame),
875
+ };
876
+ case "BreakStatement":
877
+ return { type: "break" };
878
+ case "ContinueStatement":
879
+ return { type: "continue" };
880
+ case "ThrowStatement":
881
+ throw await this.evaluate(node.argument, env, frame);
882
+ case "TryStatement": {
883
+ try {
884
+ const c = await this.execute(node.block, env, frame);
885
+ if (c.type !== "normal")
886
+ return c;
887
+ }
888
+ 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)
892
+ throw e;
893
+ 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);
899
+ }
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);
907
+ }
908
+ }
909
+ return NORMAL;
910
+ }
911
+ case "SwitchStatement": {
912
+ const disc = await this.evaluate(node.discriminant, env, frame);
913
+ const cases = node.cases;
914
+ 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)
924
+ continue;
925
+ for (const s of c.consequent ?? []) {
926
+ const comp = await this.execute(s, switchEnv, frame);
927
+ if (comp.type === "break")
928
+ return NORMAL;
929
+ if (comp.type !== "normal")
930
+ return comp;
931
+ }
932
+ }
933
+ return NORMAL;
934
+ }
935
+ case "EmptyStatement":
936
+ return NORMAL;
937
+ default:
938
+ throw new RuntimeFault("L1000", `unsupported statement ${node.type}`);
939
+ }
940
+ }
941
+ }
942
+ // ---- helpers -------------------------------------------------------------------------------------
943
+ function applyBinary(op, l, r) {
944
+ switch (op) {
945
+ case "===":
946
+ return Object.is(l, r) || l === r;
947
+ case "!==":
948
+ return !(l === r);
949
+ case "<":
950
+ return l < r;
951
+ case "<=":
952
+ return l <= r;
953
+ case ">":
954
+ return l > r;
955
+ case ">=":
956
+ return l >= r;
957
+ case "+":
958
+ return typeof l === "string" || typeof r === "string"
959
+ ? String(l) + String(r)
960
+ : l + r;
961
+ case "-":
962
+ return l - r;
963
+ case "*":
964
+ return l * r;
965
+ case "/":
966
+ return l / r;
967
+ case "%":
968
+ return l % r;
969
+ default:
970
+ throw new RuntimeFault("L1000", `unsupported operator ${op}`);
971
+ }
972
+ }
973
+ /** What a `catch` block sees: a plain record, because programs branch on data, not on classes. */
974
+ function toProgramError(e) {
975
+ if (e instanceof EffectError) {
976
+ return deepFreeze({ code: e.code, kind: e.kind, message: e.message, ...(e.detail !== undefined ? { detail: e.detail } : {}) });
977
+ }
978
+ if (e instanceof RuntimeFault)
979
+ 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) });
983
+ }
984
+ // ---- the public entry point -------------------------------------------------------------------------
985
+ /**
986
+ * Run a program.
987
+ *
988
+ * A run pins to the content hash of its SOURCE, which is why prompts, schemas, and model config
989
+ * are covered: they are in the source. Passing an existing journal resumes rather than starts.
990
+ */
991
+ export async function run(source, options) {
992
+ 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());
996
+ const env = new Env(null);
997
+ installGlobals(env, interp, frame);
998
+ const completion = await interp.executeBlock(ast, env, frame);
999
+ return {
1000
+ value: completion.type === "return" ? completion.value : undefined,
1001
+ journal: interp.journal,
1002
+ programHash,
1003
+ steps: interp.stepCount,
1004
+ };
1005
+ }
1006
+ /** Re-run a program against an existing journal. Journalled effects return recorded results. */
1007
+ export async function resume(source, journal, options) {
1008
+ journal.resetConsumed();
1009
+ return await run(source, { ...options, journal });
1010
+ }
1011
+ function installGlobals(env, interp, rootFrame) {
1012
+ 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;
1115
+ }
1116
+ export { LangError, LangErrors };
1117
+ //# sourceMappingURL=interpret.js.map