@hew-lang/sandbox-vm 0.6.1 → 0.6.3

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 (52) hide show
  1. package/README.md +48 -4
  2. package/cli/test-runner.mjs +4 -1
  3. package/dist/interpreter/interpreter.d.ts +1 -1
  4. package/dist/interpreter/interpreter.js +1 -1
  5. package/dist/interpreter/parity-runner.js +1 -1
  6. package/dist/interpreter/run-program.d.ts +3 -2
  7. package/dist/interpreter/run-program.js +5 -3
  8. package/dist/interpreter/schema-validator.d.ts +9 -4
  9. package/dist/interpreter/schema-validator.js +15 -49
  10. package/dist/interpreter/strings.d.ts +21 -0
  11. package/dist/interpreter/strings.js +191 -0
  12. package/dist/interpreter/types.d.ts +6 -0
  13. package/dist/interpreter/unicode-data.d.ts +4 -0
  14. package/dist/interpreter/unicode-data.js +1673 -0
  15. package/dist/interpreter/unicode.d.ts +3 -0
  16. package/dist/interpreter/unicode.js +35 -0
  17. package/dist/interpreter/v1/actor-identity.d.ts +3 -0
  18. package/dist/interpreter/v1/actor-identity.js +7 -0
  19. package/dist/interpreter/v1/actor-waits.d.ts +6 -0
  20. package/dist/interpreter/v1/actor-waits.js +33 -0
  21. package/dist/interpreter/v1/browser-directory.d.ts +107 -0
  22. package/dist/interpreter/v1/browser-directory.js +288 -0
  23. package/dist/interpreter/v1/calendar.d.ts +4 -0
  24. package/dist/interpreter/v1/calendar.js +64 -0
  25. package/dist/interpreter/v1/codecs.js +8 -11
  26. package/dist/interpreter/v1/exec.d.ts +2 -2
  27. package/dist/interpreter/v1/exec.js +1761 -489
  28. package/dist/interpreter/v1/instructions.generated.d.ts +1135 -0
  29. package/dist/interpreter/v1/instructions.generated.js +106 -0
  30. package/dist/interpreter/v1/package-graph.d.ts +2 -0
  31. package/dist/interpreter/v1/package-graph.js +729 -0
  32. package/dist/interpreter/v1/package-validator.generated.d.ts +3 -0
  33. package/dist/interpreter/v1/package-validator.generated.js +75902 -0
  34. package/dist/interpreter/v1/package.d.ts +5 -645
  35. package/dist/interpreter/v1/package.js +0 -12
  36. package/dist/interpreter/v1/pipes.d.ts +1 -0
  37. package/dist/interpreter/v1/pipes.js +9 -6
  38. package/dist/interpreter/v1/scheduler.d.ts +8 -1
  39. package/dist/interpreter/v1/scheduler.js +50 -11
  40. package/dist/interpreter/v1/shims.d.ts +260 -7
  41. package/dist/interpreter/v1/shims.js +647 -175
  42. package/dist/interpreter/v1/structural.d.ts +2 -28
  43. package/dist/interpreter/v1/timestamp.d.ts +7 -0
  44. package/dist/interpreter/v1/timestamp.js +54 -0
  45. package/dist/interpreter/v1/validate.d.ts +8 -2
  46. package/dist/interpreter/v1/validate.js +284 -200
  47. package/dist/interpreter/v1/value-operations.d.ts +11 -3
  48. package/dist/interpreter/v1/value-operations.js +70 -18
  49. package/dist/interpreter/values.d.ts +30 -8
  50. package/dist/interpreter/values.js +31 -5
  51. package/dist/interpreter/worker.js +4 -3
  52. package/package.json +4 -4
@@ -1,3 +1,4 @@
1
+ import { BrowserDirectories, } from "./browser-directory.js";
1
2
  /// The `hew.sandbox.bytecode.v1` executor.
2
3
  ///
3
4
  /// The package is a projection of verified ownership SIR, so this executor
@@ -6,18 +7,22 @@
6
7
  /// follows the `unwind` edge the package names, and takes the failure edge a
7
8
  /// `checked.binary` names rather than trapping on its own.
8
9
  import { executeCodec } from "./codecs.js";
10
+ import { actorIdentical } from "./actor-identity.js";
11
+ import { ActorWaits } from "./actor-waits.js";
9
12
  import { TraceBuilder, runtimeFailure } from "../trace.js";
10
- import { UNIT, cloneValue, toJsonValue } from "../values.js";
13
+ import { UNIT, cloneValue, stringValue, toJsonValue, } from "../values.js";
11
14
  import { runtimeTag } from "./package.js";
12
15
  import { StructuralRenderer } from "./structural.js";
16
+ import { timestampBinary } from "./timestamp.js";
13
17
  import { selectedValue, collectionOperation, sharedOperation, } from "./value-operations.js";
14
18
  import { Pipes } from "./pipes.js";
15
19
  import { FrameScheduler } from "./scheduler.js";
16
20
  import { DeterministicIds } from "../../scheduler/ids.js";
17
21
  import { Mt19937 } from "./mt19937.js";
18
- import { ShimFault, resolveExternShim, resolveRuntimeShim, } from "./shims.js";
19
- import { admitPackage } from "./validate.js";
22
+ import { ShimFault, CODEC_REGISTRATION, decodeUtf8Checked, resolveExtern, resolveRuntime, resolveSuspension, } from "./shims.js";
23
+ import { AdmittedPackage } from "./validate.js";
20
24
  const DEFAULT_STEP_BUDGET = 1_000_000;
25
+ const DEFAULT_ACTOR_STOP_NS = 5_000_000_000;
21
26
  /// Ends the run. The status is already recorded on the trace.
22
27
  class Halt extends Error {
23
28
  status;
@@ -52,7 +57,7 @@ class StdinReader {
52
57
  return this.bytes.slice(start, this.offset);
53
58
  }
54
59
  }
55
- export function runPackageV1(pkg, options = {}) {
60
+ export async function runPackageV1(pkg, options = {}) {
56
61
  const fixtureId = options.fixtureId ?? "package";
57
62
  const replay = {
58
63
  seed: options.replay?.seed ?? 0,
@@ -65,14 +70,25 @@ export function runPackageV1(pkg, options = {}) {
65
70
  const trace = new TraceBuilder(pkg, fixtureId, options.traceId ?? `trace:${fixtureId}`, replay, options.sandboxVersion ?? "0.0.0-spec");
66
71
  // Admission is a load-time decision: nothing runs when the package names a
67
72
  // capability the VM has no shim for.
68
- const rejection = admitPackage(pkg);
69
- if (rejection) {
70
- trace.reject(rejection);
73
+ const admission = AdmittedPackage.load(pkg, options);
74
+ if (!(admission instanceof AdmittedPackage)) {
75
+ trace.reject(admission);
71
76
  return trace.finish();
72
77
  }
73
- const vm = new ExecutorV1(pkg, trace, StdinReader.fromReplay(replay.inputs), options.schedulerPolicy ?? "round_robin", options);
78
+ const directoryRefusal = await BrowserDirectories.admission(options.directoryRoots);
79
+ if (directoryRefusal) {
80
+ trace.reject({
81
+ category: "not_implemented",
82
+ code: "sandbox.host.directory",
83
+ capability: "directoryRoots",
84
+ message: directoryRefusal,
85
+ span: null,
86
+ });
87
+ return trace.finish();
88
+ }
89
+ const vm = new ExecutorV1(admission, trace, StdinReader.fromReplay(replay.inputs), options.schedulerPolicy ?? "round_robin", options);
74
90
  try {
75
- vm.run();
91
+ await vm.run();
76
92
  }
77
93
  catch (error) {
78
94
  if (!(error instanceof Halt)) {
@@ -83,7 +99,6 @@ export function runPackageV1(pkg, options = {}) {
83
99
  return trace.finish();
84
100
  }
85
101
  class ExecutorV1 {
86
- pkg;
87
102
  trace;
88
103
  stdin;
89
104
  options;
@@ -92,10 +107,13 @@ class ExecutorV1 {
92
107
  scheduler;
93
108
  ids;
94
109
  actors = new Map();
110
+ actorWaits = new ActorWaits();
95
111
  supervisors = new Map();
96
112
  roles = new Map();
97
113
  pipes = new Pipes();
98
114
  generators = new Map();
115
+ generatorStreams = new Map();
116
+ senderEndpoints = new Map();
99
117
  tasks = new Map();
100
118
  completedTasks = 0;
101
119
  actorIdentity = 0n;
@@ -105,18 +123,68 @@ class ExecutorV1 {
105
123
  consumedResources = new WeakSet();
106
124
  running = false;
107
125
  rootComplete = false;
126
+ terminating = false;
108
127
  rootFault = null;
109
- constructor(pkg, trace, stdin, policy, options) {
110
- this.pkg = pkg;
128
+ pkg;
129
+ directories;
130
+ constructor(admission, trace, stdin, policy, options) {
111
131
  this.trace = trace;
112
132
  this.stdin = stdin;
113
133
  this.options = options;
134
+ const pkg = admission.bytecode;
135
+ this.pkg = pkg;
136
+ this.directories = new BrowserDirectories(options.directoryRoots);
114
137
  this.scheduler = new FrameScheduler(trace, policy);
115
138
  this.ids = new DeterministicIds(trace.replay.seed);
116
139
  this.host = {
140
+ argv: [...(options.argv ?? ["main.hew"])],
141
+ unixTimeMillis: () => this.trace.virtualTimeMs,
142
+ calendarBridge: options.calendarBridge,
143
+ fileError: (operation) => {
144
+ const error = this.current.context.fileError;
145
+ switch (operation) {
146
+ case "HasError":
147
+ return { kind: "bool", value: error != null };
148
+ case "LastErrorKind":
149
+ return { kind: "i64", value: BigInt(error?.kind ?? 0) };
150
+ case "LastErrno":
151
+ if (error)
152
+ error.kind = 0;
153
+ return { kind: "i64", value: 0n };
154
+ case "LastError":
155
+ this.current.context.fileError = null;
156
+ return stringValue(error?.message ?? "");
157
+ }
158
+ },
117
159
  pipes: this.pipes,
160
+ generatorStream: (value) => {
161
+ const id = this.ids.channel();
162
+ this.generatorStreams.set(id, {
163
+ generator: this.generatorFor(value),
164
+ closed: false,
165
+ pending: null,
166
+ fault: null,
167
+ receiving: false,
168
+ });
169
+ return { kind: "stream", channelId: id };
170
+ },
171
+ encoderSink: (sink, encoder) => this.senderEndpoint({
172
+ sink,
173
+ encoder,
174
+ references: 0,
175
+ endpoints: 0,
176
+ busy: false,
177
+ waiting: new Set(),
178
+ }, !this.sinkClosed(sink)),
179
+ cloneSink: (value) => {
180
+ const endpoint = value.kind === "sink"
181
+ ? this.senderEndpoints.get(value.channelId)
182
+ : undefined;
183
+ return endpoint
184
+ ? this.senderEndpoint(endpoint.owner, endpoint.open)
185
+ : this.pipes.cloneSink(value);
186
+ },
118
187
  newPipe: (capacity) => this.newPipe(capacity),
119
- closePipe: (value) => this.pipes.close(value, this.pipeFault(this.current)),
120
188
  releaseValue: (value) => this.closeValue(value, this.pipeFault(this.current)),
121
189
  writeStdout: (text) => this.trace.writeStdout(text, null),
122
190
  writeStderr: (text) => this.trace.writeStderr(text, null),
@@ -135,7 +203,7 @@ class ExecutorV1 {
135
203
  id: "actor:root",
136
204
  actor: null,
137
205
  returned: (value) => {
138
- this.rootComplete = true;
206
+ this.completeRoot();
139
207
  this.trace.exitCode =
140
208
  pkg.entry?.exit === "status" && value.kind === "i64"
141
209
  ? Number(value.value)
@@ -143,36 +211,63 @@ class ExecutorV1 {
143
211
  },
144
212
  failed: (fault) => {
145
213
  this.rootFault = fault;
146
- this.rootComplete = true;
214
+ this.completeRoot();
147
215
  },
148
216
  });
149
217
  }
150
218
  /// One instruction or terminator executed is one step, ownership ops
151
219
  /// included: the package's ops are what the program does.
152
- run() {
153
- this.runFrame(this.current);
154
- this.scheduler.run();
155
- if (!this.rootComplete) {
156
- throw new Error("the root frame is waiting with no runnable work");
157
- }
158
- // Native main return drains accepted work before closing idle actors.
159
- for (const owner of this.supervisors.values())
160
- if (!owner.supervisor)
161
- this.stopSupervisor(owner);
162
- for (const actor of this.actors.values())
163
- if (!actor.supervisor && actor.alive)
164
- this.requestActorStop(actor);
165
- this.scheduler.run();
166
- if (this.rootFault)
167
- this.haltWithFault(this.rootFault);
168
- if (this.trace.exitCode === 0 && this.faultDebts.size)
169
- this.trace.exitCode = 1;
220
+ async run() {
221
+ const root = this.current.context;
222
+ const cancel = () => {
223
+ const fault = {
224
+ kind: "panic",
225
+ message: "execution cancelled",
226
+ cancelled: true,
227
+ };
228
+ if (root.cancel)
229
+ root.cancel(fault);
230
+ else
231
+ root.pendingCancellation ??= fault;
232
+ };
233
+ this.options.signal?.addEventListener("abort", cancel, { once: true });
234
+ if (this.options.signal?.aborted)
235
+ cancel();
236
+ try {
237
+ this.runFrame(this.current);
238
+ await this.scheduler.run();
239
+ if (!this.rootComplete) {
240
+ throw new Error("the root frame is waiting with no runnable work");
241
+ }
242
+ // Native main return drains accepted work before closing idle actors.
243
+ for (const owner of this.supervisors.values())
244
+ if (!owner.supervisor)
245
+ this.stopSupervisor(owner);
246
+ for (const actor of this.actors.values())
247
+ if (!actor.supervisor && actor.alive)
248
+ this.requestActorStop(actor);
249
+ await this.scheduler.run();
250
+ if (this.rootFault)
251
+ this.haltWithFault(this.rootFault);
252
+ if (this.trace.exitCode === 0 && this.faultDebts.size)
253
+ this.trace.exitCode = 1;
254
+ }
255
+ finally {
256
+ this.terminating = true;
257
+ await this.scheduler.retireHost();
258
+ this.directories.retire((value) => this.closeHostResource(value));
259
+ this.options.signal?.removeEventListener("abort", cancel);
260
+ }
170
261
  }
171
262
  runFrame(frame) {
172
263
  this.current = frame;
173
264
  this.running = true;
174
265
  while (this.running) {
175
266
  const act = this.current;
267
+ act.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
268
+ if (this.scopeDeadline(act, fault) !== fault)
269
+ act.context.pendingCancellation ??= fault;
270
+ };
176
271
  while (act.opIndex < act.block.ops.length) {
177
272
  const op = act.block.ops[act.opIndex++];
178
273
  if (op.op !== "coverage.hit")
@@ -203,9 +298,19 @@ class ExecutorV1 {
203
298
  }
204
299
  // ── frames ───────────────────────────────────────────────────────────────
205
300
  activate(fn, args, caller, result, normal, unwind, context) {
206
- if ((this.pkg.resources ?? []).some((resource) => resource.close === fn.id) &&
207
- args[0])
301
+ const mandatoryClose = (this.pkg.resources ?? []).some((resource) => "close" in resource && resource.close === fn.id);
302
+ const parentContext = caller?.context ?? context;
303
+ parentContext.pollBudget ??= { remaining: 0 };
304
+ if (mandatoryClose && args[0])
208
305
  this.closingResources.add(args[0]);
306
+ const executionContext = mandatoryClose
307
+ ? {
308
+ ...parentContext,
309
+ cancel: undefined,
310
+ pendingCancellation: undefined,
311
+ requestDeadline: undefined,
312
+ }
313
+ : parentContext;
209
314
  const env = new Map();
210
315
  for (const [at, param] of fn.params.entries()) {
211
316
  env.set(param.value, ownedRef(args[at] ?? UNIT));
@@ -220,7 +325,7 @@ class ExecutorV1 {
220
325
  for (const block of fn.blocks) {
221
326
  block.params.forEach(record);
222
327
  for (const op of block.ops) {
223
- if (op.dst != null && op.own === "none")
328
+ if ("dst" in op && op.dst != null && op.own === "none")
224
329
  trivial.add(op.dst);
225
330
  }
226
331
  const term = block.term;
@@ -231,7 +336,13 @@ class ExecutorV1 {
231
336
  arm.fields.forEach(record);
232
337
  }
233
338
  return {
234
- context: caller?.context ?? context,
339
+ rcOwner: fn.actor_construction
340
+ ? { blocks: new Set(), closed: false }
341
+ : (caller?.rcOwner ??
342
+ executionContext.rcOwner ??
343
+ executionContext.actor?.rcOwner ??
344
+ null),
345
+ context: executionContext,
235
346
  fn,
236
347
  block: this.blockAt(fn, fn.entry),
237
348
  opIndex: 0,
@@ -243,7 +354,7 @@ class ExecutorV1 {
243
354
  parked: [],
244
355
  caller,
245
356
  result,
246
- normal,
357
+ normal: normal ?? undefined,
247
358
  unwind,
248
359
  };
249
360
  }
@@ -373,7 +484,7 @@ class ExecutorV1 {
373
484
  this.define(act, op.dst, { kind: "f64", value: floatConst(op) });
374
485
  return;
375
486
  case "const.char":
376
- this.define(act, op.dst, { kind: "string", value: op.value });
487
+ this.define(act, op.dst, stringValue(op.value));
377
488
  return;
378
489
  case "const.unit":
379
490
  this.define(act, op.dst, UNIT);
@@ -382,10 +493,7 @@ class ExecutorV1 {
382
493
  this.define(act, op.dst, { kind: "i64", value: BigInt(op.nanos) });
383
494
  return;
384
495
  case "const.str":
385
- this.define(act, op.dst, {
386
- kind: "string",
387
- value: this.pkg.strings[op.str] ?? "",
388
- });
496
+ this.define(act, op.dst, stringValue(this.pkg.strings[op.str] ?? ""));
389
497
  return;
390
498
  case "const.bytes":
391
499
  this.define(act, op.dst, bytesValue(this.pkg.bytes[op.bytes] ?? []));
@@ -459,7 +567,7 @@ class ExecutorV1 {
459
567
  case "aggregate.make":
460
568
  this.define(act, op.dst, {
461
569
  kind: "record",
462
- typeId: this.aggregateName(op.shape),
570
+ typeId: op.shape === null ? "" : this.aggregateName(op.shape),
463
571
  shape: op.shape ?? undefined,
464
572
  fields: op.fields.map((id) => this.read(act, id)),
465
573
  });
@@ -652,8 +760,9 @@ class ExecutorV1 {
652
760
  const context = {
653
761
  id,
654
762
  actor: act.context.actor,
763
+ rcOwner: act.rcOwner,
655
764
  requestDeadline: this.requestDeadline(act),
656
- returned: (value) => this.closeValueAsync(value, null, (fault) => this.finishGenerator(generator, fault)),
765
+ returned: (value) => this.closeValueAsync(value, null, (fault) => this.finishGenerator(generator, fault), act.context.actor, act.rcOwner),
657
766
  failed: (fault) => this.finishGenerator(generator, fault),
658
767
  };
659
768
  const generator = {
@@ -748,10 +857,10 @@ class ExecutorV1 {
748
857
  changed();
749
858
  };
750
859
  if (failure && winner)
751
- this.closeValueAsync(value, this.faultText(failure), finish, act.context.actor);
860
+ this.closeValueAsync(value, this.faultText(failure), finish, act.context.actor, act.rcOwner);
752
861
  else
753
862
  finish();
754
- }, act.context.actor);
863
+ }, act.context.actor, act.rcOwner);
755
864
  };
756
865
  task.cancel = () => {
757
866
  if (task.done || task.finishing)
@@ -770,12 +879,44 @@ class ExecutorV1 {
770
879
  ready();
771
880
  return;
772
881
  }
773
- case "task.spawn": {
882
+ case "task.spawn":
883
+ case "task.invoke": {
774
884
  const group = act.scopes.get(op.scope);
775
885
  if (!group)
776
886
  throw new Error(`task scope ${op.scope} is missing`);
777
- const callable = this.read(act, op.callable);
778
- this.invalidate(act, op.callable);
887
+ const consuming = op.op === "task.spawn" ||
888
+ ("receiver" in op.callee && op.callee.receiver.decision === "move");
889
+ const dynamic = op.op === "task.invoke" && "dyn" in op.callee ? op.callee : null;
890
+ const dynamicReceiver = dynamic
891
+ ? this.boundary(act, dynamic.receiver)
892
+ : null;
893
+ if (dynamicReceiver && dynamicReceiver.kind !== "dyn")
894
+ throw new TypeError("prepared dynamic receiver is not a trait object");
895
+ const dynamicEntry = dynamicReceiver?.kind === "dyn" && dynamic
896
+ ? this.dynamicEntry(dynamicReceiver, dynamic.dyn)
897
+ : null;
898
+ if (dynamic && dynamicEntry?.method !== dynamic.method)
899
+ throw new Error("prepared dynamic slot differs from its checked method");
900
+ const callable = op.op === "task.spawn"
901
+ ? this.read(act, op.callable)
902
+ : "direct" in op.callee
903
+ ? { kind: "function", id: String(op.callee.direct) }
904
+ : "mutable" in op.callee
905
+ ? { kind: "function", id: String(op.callee.mutable) }
906
+ : "dyn" in op.callee
907
+ ? {
908
+ kind: "function",
909
+ id: String(dynamicEntry.callee),
910
+ }
911
+ : this.boundary(act, op.callee.receiver);
912
+ if (op.op === "task.spawn")
913
+ this.invalidate(act, op.callable);
914
+ const args = op.op === "task.invoke"
915
+ ? op.args.map((arg) => this.boundary(act, arg))
916
+ : [];
917
+ const specialization = op.op === "task.invoke" && "specialization" in op.callee
918
+ ? op.callee.specialization
919
+ : null;
779
920
  const task = this.newTask();
780
921
  group.tasks.push(task);
781
922
  const settle = (value, fault) => {
@@ -786,9 +927,7 @@ class ExecutorV1 {
786
927
  !(fault.kind === "panic" && (fault.cancelled || fault.deadline)) &&
787
928
  act.context.actor)
788
929
  act.context.actor.crashing ??= fault;
789
- this.closeValueAsync(callable, act.context.actor
790
- ? this.faultText(act.context.actor.crashing)
791
- : null, (closeFault) => {
930
+ const complete = (closeFault) => {
792
931
  if (!fault || (fault.kind === "panic" && fault.cancelled))
793
932
  fault = closeFault ?? fault;
794
933
  this.settleTask(task, value, fault);
@@ -803,41 +942,104 @@ class ExecutorV1 {
803
942
  }
804
943
  for (const changed of [...group.changed])
805
944
  changed();
806
- });
945
+ };
946
+ if (!consuming) {
947
+ complete(null);
948
+ return;
949
+ }
950
+ this.closeValueAsync(callable, act.context.actor
951
+ ? this.faultText(act.context.actor.crashing)
952
+ : null, complete, act.context.actor, act.rcOwner);
953
+ };
954
+ const mutable = op.op === "task.invoke" && "mutable" in op.callee ? op.callee : null;
955
+ const receiverRef = mutable
956
+ ? this.refOf(act, mutable.receiver.value)
957
+ : null;
958
+ if (dynamicReceiver?.kind === "dyn" && dynamicEntry && dynamic)
959
+ args.unshift(this.erasedReceiver(act, dynamic.receiver, dynamicReceiver.value, dynamicEntry.receiver));
960
+ if (receiverRef)
961
+ args.unshift(mutable.staged
962
+ ? cloneValue(readRef(receiverRef))
963
+ : readRef(receiverRef));
964
+ const handback = (value, fault, result) => {
965
+ if (!receiverRef || !mutable || value === null)
966
+ throw new Error("prepared mutable method omitted its receiver handback");
967
+ if (mutable.staged && fault) {
968
+ this.closeValueAsync(value, this.faultText(fault), (closeFault) => settle(UNIT, combineFaults(fault, closeFault)), act.context.actor, act.rcOwner);
969
+ }
970
+ else {
971
+ const previous = readRef(receiverRef);
972
+ writeRef(receiverRef, value);
973
+ if (mutable.staged)
974
+ this.closeValueAsync(previous, null, (closeFault) => {
975
+ if (closeFault)
976
+ this.closeValueAsync(result, this.faultText(closeFault), (resultFault) => settle(UNIT, combineFaults(closeFault, resultFault)), act.context.actor, act.rcOwner);
977
+ else
978
+ settle(result, null);
979
+ }, act.context.actor, act.rcOwner);
980
+ else
981
+ settle(result, fault);
982
+ }
807
983
  };
808
984
  const context = {
809
985
  id: task.id,
810
986
  actor: act.context.actor,
987
+ rcOwner: act.rcOwner,
811
988
  requestDeadline: this.requestDeadline(act),
812
- returned: (value) => settle(value, null),
813
- failed: (fault) => settle(UNIT, fault),
989
+ returned: (value) => {
990
+ if (dynamicEntry?.receiver_handback &&
991
+ dynamicReceiver?.kind === "dyn") {
992
+ if (value.kind !== "record" || value.fields.length !== 2)
993
+ throw new Error("prepared dynamic method omitted its dual result");
994
+ dynamicReceiver.value = value.fields[1];
995
+ settle(value.fields[0], null);
996
+ }
997
+ else if (!mutable)
998
+ settle(value, null);
999
+ else if (value.kind === "record" && value.fields.length === 2)
1000
+ handback(value.fields[1], null, value.fields[0]);
1001
+ else
1002
+ throw new Error("prepared mutable method returned no dual result");
1003
+ },
1004
+ failed: (fault, value) => {
1005
+ if (dynamicEntry?.receiver_handback &&
1006
+ dynamicReceiver?.kind === "dyn") {
1007
+ if (!value)
1008
+ throw new Error("prepared dynamic method omitted its fault handback");
1009
+ dynamicReceiver.value = value;
1010
+ }
1011
+ if (mutable)
1012
+ handback(value ?? null, fault, UNIT);
1013
+ else
1014
+ settle(UNIT, fault);
1015
+ },
814
1016
  };
815
- const frame = this.activate(this.functionAt(calleeFunction(callable)), callable.kind === "closure" ? [callable.environment] : [], null, null, undefined, null, context);
816
- let started = false;
1017
+ const frame = this.activate(this.functionAt(calleeFunction(callable, specialization)), callable.kind === "closure" ? [callable.environment, ...args] : args, null, null, undefined, null, context);
817
1018
  task.cancel = () => {
818
1019
  if (task.done || task.finishing)
819
1020
  return;
820
- if (started)
821
- context.cancel?.();
822
- else
823
- settle(UNIT, {
824
- kind: "panic",
825
- message: "task cancelled",
826
- cancelled: true,
827
- });
1021
+ context.pendingCancellation ??= {
1022
+ kind: "panic",
1023
+ message: "task cancelled",
1024
+ cancelled: true,
1025
+ };
1026
+ context.cancel?.(context.pendingCancellation);
828
1027
  };
829
1028
  this.scheduler.enqueue(task.id, () => {
830
1029
  if (task.done || task.finishing)
831
1030
  return;
832
- started = true;
833
1031
  this.runFrame(frame);
834
1032
  });
835
1033
  this.trace.allocateId("task", task.id, act.context.id);
836
1034
  this.define(act, op.dst, { kind: "task", id: task.id });
837
1035
  return;
838
1036
  }
839
- default:
840
- throw new Error(`opcode ${op.op} has no sequential executor`);
1037
+ case "actor.ingress_adapter":
1038
+ throw new Error("actor ingress adapters are not admitted in the VM");
1039
+ default: {
1040
+ const exhaustive = op;
1041
+ throw new Error("unhandled instruction: " + String(exhaustive));
1042
+ }
841
1043
  }
842
1044
  }
843
1045
  /// Hand each field of a consumed container, or each payload of a matched
@@ -887,6 +1089,20 @@ class ExecutorV1 {
887
1089
  case "return":
888
1090
  this.returnFrom(act, term.value ? this.boundary(act, term.value) : UNIT);
889
1091
  return;
1092
+ case "timestamp.binary": {
1093
+ const outcome = timestampBinary(this.pkg, term, this.read(act, term.lhs), this.read(act, term.rhs));
1094
+ if (outcome === null) {
1095
+ const failure = term.failures.find((edge) => edge.trap === "integer_overflow");
1096
+ if (!failure)
1097
+ throw new Error("timestamp arithmetic lacks its overflow edge");
1098
+ this.takeEdge(act, failure.edge);
1099
+ }
1100
+ else {
1101
+ this.define(act, term.result.value, outcome);
1102
+ this.takeEdge(act, term.normal);
1103
+ }
1104
+ return;
1105
+ }
890
1106
  case "checked.binary": {
891
1107
  const failure = checkedFailure(term.binary_op, this.read(act, term.lhs), this.read(act, term.rhs), term.ty);
892
1108
  if (failure) {
@@ -906,14 +1122,7 @@ class ExecutorV1 {
906
1122
  if (receiver.kind !== "dyn") {
907
1123
  throw new TypeError(`dyn.call expected a trait object, got ${receiver.kind}`);
908
1124
  }
909
- const vtable = this.pkg.vtables[receiver.vtable] ??
910
- this.pkg.vtables.find((candidate) => candidate.id === receiver.vtable);
911
- // The slot number is the checker's own index, so it is matched rather
912
- // than used as a position in the slot array.
913
- const entry = vtable?.slots.find((candidate) => candidate.slot === term.slot);
914
- if (!entry) {
915
- throw new Error(`vtable ${receiver.vtable} has no slot ${term.slot}`);
916
- }
1125
+ const entry = this.dynamicEntry(receiver, term.slot);
917
1126
  // The wrapped value is the method's receiver, ahead of the call's own
918
1127
  // arguments, passed under the decision the slot records: the erasure
919
1128
  // the concrete type was made under, not something read off the method.
@@ -946,102 +1155,128 @@ class ExecutorV1 {
946
1155
  return;
947
1156
  case "runtime.call": {
948
1157
  const entry = this.pkg.runtime_families[term.family];
949
- if (entry?.family === "ActorCaller") {
950
- const caller = act.context.caller ?? { kind: "Task" };
951
- const payload = [];
952
- if (caller.kind === "Actor") {
953
- const shape = this.pkg.variants[term.result_shape];
954
- const tag = runtimeTag(shape, "CallerActor");
955
- payload.push({
956
- kind: "record",
957
- typeId: shape.cases[tag].fields[0],
958
- fields: [{ kind: "i64", value: caller.identity }],
959
- });
960
- }
961
- this.completeShim(act, term, this.variant(term.result_shape, caller.kind === "Actor"
962
- ? "CallerActor"
963
- : caller.kind === "Timer"
964
- ? "CallerTimer"
965
- : "CallerTask", payload));
966
- return;
967
- }
968
- if (entry?.family === "ActorRefusalLog") {
969
- const [message, handler, reason] = term.args.map((argument) => this.boundary(act, argument));
970
- this.trace.snapshot("actor.delivery_refused", {
971
- actor_id: act.context.actor?.id ?? null,
972
- handler: toJsonValue(handler),
973
- message: toJsonValue(message),
974
- reason: toJsonValue(reason),
975
- });
976
- if (!act.context.reply)
977
- this.trace.writeStderr(`WARN actor.delivery_refused actor=${act.context.actor?.identity ?? 0n} handler=${asString(handler)} reason=${JSON.stringify(asString(reason))}\n`, null);
978
- this.completeShim(act, term, UNIT);
979
- return;
980
- }
981
- if (entry &&
982
- (entry.family === "Map" ||
983
- entry.family === "Set" ||
984
- (entry.family === "Array" && entry.detail === "Set") ||
985
- (entry.family === "Vector" &&
986
- ["Contains", "Clear", "Set"].includes(String(entry.detail))))) {
987
- const args = term.args.map((operand) => this.boundary(act, operand));
988
- const iterator = collectionOperation(this.pkg, entry.family === "Array" ? "Vector" : entry.family, String(entry.detail), args, term.callbacks ?? [], term.result_member_shapes?.[1] ?? term.result_shape);
989
- this.runValueProgram(act, term, args, iterator);
990
- return;
991
- }
992
- if (entry &&
993
- [
994
- "RcNew",
995
- "RcClone",
996
- "RcGet",
997
- "RcIsUnique",
998
- "RcSet",
999
- "RcStrongCount",
1000
- "RcWeakCount",
1001
- ].includes(entry.family)) {
1002
- const args = term.args.map((operand) => this.boundary(act, operand));
1003
- this.runValueProgram(act, term, args, sharedOperation(entry.family, args));
1004
- return;
1005
- }
1006
- if (entry?.family === "SupervisorPool") {
1007
- this.supervisorPool(act, term, String(entry.detail));
1008
- return;
1009
- }
1010
- if (entry && ["SinkClose", "StreamClose"].includes(entry.family)) {
1011
- const value = this.boundary(act, term.args[0]);
1012
- this.running = false;
1013
- this.closeValueAsync(value, this.pipeFault(act), (fault) => {
1014
- if (fault)
1015
- this.raiseFault(act, fault, term.unwind);
1016
- else
1017
- this.completeShim(act, term, UNIT);
1018
- this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1019
- });
1020
- return;
1021
- }
1022
- if (entry?.family === "StructuralFormat") {
1023
- this.structuralFormat(act, term);
1158
+ if (!entry)
1159
+ throw new Error("runtime family is absent after admission");
1160
+ const registration = resolveRuntime(entry);
1161
+ if (registration.kind === "ordinary") {
1162
+ this.callShim(act, term, registration.shim);
1024
1163
  return;
1025
1164
  }
1026
- const shim = entry ? resolveRuntimeShim(entry) : undefined;
1027
- if (!entry || !shim) {
1028
- throw new Error(`runtime family ${term.family} resolved no shim after admission`);
1165
+ if (registration.kind === "refused")
1166
+ throw new Error(`runtime family ${registration.capability} was refused after admission`);
1167
+ switch (registration.handler) {
1168
+ case "actor_caller": {
1169
+ const caller = act.context.caller ?? { kind: "Task" };
1170
+ const payload = [];
1171
+ if (caller.kind === "Actor") {
1172
+ const shape = this.pkg.variants[term.result_shape];
1173
+ const tag = runtimeTag(shape, "CallerActor");
1174
+ payload.push({
1175
+ kind: "record",
1176
+ typeId: shape.cases[tag].fields[0],
1177
+ fields: [{ kind: "i64", value: caller.identity }],
1178
+ });
1179
+ }
1180
+ this.completeShim(act, term, this.variant(term.result_shape, caller.kind === "Actor"
1181
+ ? "CallerActor"
1182
+ : caller.kind === "Timer"
1183
+ ? "CallerTimer"
1184
+ : "CallerTask", payload));
1185
+ return;
1186
+ }
1187
+ case "actor_refusal_log": {
1188
+ const [message, handler, reason] = term.args.map((argument) => this.boundary(act, argument));
1189
+ this.trace.snapshot("actor.delivery_refused", {
1190
+ actor_id: act.context.actor?.id ?? null,
1191
+ handler: toJsonValue(handler),
1192
+ message: toJsonValue(message),
1193
+ reason: toJsonValue(reason),
1194
+ });
1195
+ if (!act.context.reply)
1196
+ this.trace.writeStderr(`WARN actor.delivery_refused actor=${act.context.actor?.identity ?? 0n} handler=${asString(handler)} reason=${JSON.stringify(asString(reason))}\n`, null);
1197
+ this.completeShim(act, term, UNIT);
1198
+ return;
1199
+ }
1200
+ case "collection": {
1201
+ const args = term.args.map((operand) => this.boundary(act, operand));
1202
+ const iterator = collectionOperation(this.pkg, registration, args, term.callbacks ?? [], term.result_member_shapes?.[1] ?? term.result_shape);
1203
+ this.runValueProgram(act, term, args, iterator);
1204
+ return;
1205
+ }
1206
+ case "shared": {
1207
+ const args = term.args.map((operand) => this.boundary(act, operand));
1208
+ this.runValueProgram(act, term, args, sharedOperation(registration.operation, args, act.rcOwner, term.shared_payload_type ?? "", (value) => this.variant(term.result_shape, value ? "OptionSome" : "OptionNone", value ? [value] : [])));
1209
+ return;
1210
+ }
1211
+ case "supervisor_pool": {
1212
+ this.supervisorPool(act, term, registration.operation);
1213
+ return;
1214
+ }
1215
+ case "close": {
1216
+ const value = this.boundary(act, term.args[0]);
1217
+ this.running = false;
1218
+ this.closeValueAsync(value, this.pipeFault(act), (fault) => {
1219
+ if (fault)
1220
+ this.raiseFault(act, fault, term.unwind);
1221
+ else
1222
+ this.completeShim(act, term, UNIT);
1223
+ this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1224
+ }, act.context.actor, act.rcOwner);
1225
+ return;
1226
+ }
1227
+ case "structural_format": {
1228
+ this.structuralFormat(act, term);
1229
+ return;
1230
+ }
1231
+ case "utf8_decode": {
1232
+ const value = decodeUtf8Checked(this.boundary(act, term.args[0]));
1233
+ let result;
1234
+ if ("value" in value)
1235
+ result = this.variant(term.result_shape, "ResultOk", [
1236
+ stringValue(value.value),
1237
+ ]);
1238
+ else {
1239
+ const shapes = term.variant_result_shapes;
1240
+ const error = this.pkg.aggregates[shapes.error];
1241
+ result = this.variant(term.result_shape, "ResultErr", [
1242
+ {
1243
+ kind: "record",
1244
+ typeId: error.name,
1245
+ shape: error.id,
1246
+ fields: [
1247
+ { kind: "i64", value: BigInt(value.validUpTo) },
1248
+ this.variant(shapes.error_len, value.errorLen === null ? "OptionNone" : "OptionSome", value.errorLen === null
1249
+ ? []
1250
+ : [{ kind: "i64", value: BigInt(value.errorLen) }]),
1251
+ ],
1252
+ },
1253
+ ]);
1254
+ }
1255
+ this.completeShim(act, term, result);
1256
+ return;
1257
+ }
1258
+ default: {
1259
+ const exhaustive = registration;
1260
+ throw new Error(`unknown runtime continuation ${String(exhaustive)}`);
1261
+ }
1029
1262
  }
1030
- this.callShim(act, term, shim);
1031
- return;
1032
1263
  }
1033
1264
  case "extern.call": {
1034
1265
  const entry = this.pkg.externs[term.extern];
1035
- const shim = entry ? resolveExternShim(entry.symbol) : undefined;
1036
- if (!entry || !shim) {
1037
- throw new Error(`extern ${term.extern} resolved no shim after admission`);
1038
- }
1039
- this.callShim(act, term, shim);
1266
+ if (!entry)
1267
+ throw new Error("external operation is absent after admission");
1268
+ const registration = resolveExtern(entry);
1269
+ if (registration.kind === "refused")
1270
+ throw new Error(`external operation ${registration.capability} was refused after admission`);
1271
+ if (registration.kind === "directory")
1272
+ this.callDirectory(act, term, registration.operation);
1273
+ else
1274
+ this.callShim(act, term, registration.shim);
1040
1275
  return;
1041
1276
  }
1042
1277
  case "wire.codec": {
1043
1278
  const args = term.args.map((operand) => this.boundary(act, operand));
1044
- this.runValueProgram(act, term, args, executeCodec(this.pkg, term, args[0], this.options.codecBridge, this.options.propertyInput ?? []));
1279
+ this.runValueProgram(act, term, args, executeCodec(this.pkg, term, args[0], this.options[CODEC_REGISTRATION.bridge], this.options.propertyInput ?? []));
1045
1280
  return;
1046
1281
  }
1047
1282
  case "value.call": {
@@ -1081,6 +1316,7 @@ class ExecutorV1 {
1081
1316
  this.takeEdge(act, act.fault ? term.fault : term.normal);
1082
1317
  return;
1083
1318
  case "enter_defer":
1319
+ act.context.pollSuppression = (act.context.pollSuppression ?? 0) + 1;
1084
1320
  // The deferred body runs on its own fault state: a body reached while
1085
1321
  // unwinding must not read the fault it is cleaning up after.
1086
1322
  act.parked.push(act.fault);
@@ -1088,6 +1324,7 @@ class ExecutorV1 {
1088
1324
  this.takeEdge(act, term.body);
1089
1325
  return;
1090
1326
  case "finish_defer": {
1327
+ act.context.pollSuppression = (act.context.pollSuppression ?? 0) - 1;
1091
1328
  // Combine the parked fault with whatever the body left, keeping the
1092
1329
  // first: a join must never forget the fault the boundary carried in.
1093
1330
  const parked = act.parked.pop();
@@ -1117,12 +1354,7 @@ class ExecutorV1 {
1117
1354
  typeId: shape.name,
1118
1355
  shape: shape.id,
1119
1356
  tag: deadline ? term.deadline_variant : term.fault_variant,
1120
- payload: [
1121
- {
1122
- kind: "string",
1123
- value: faultReport(fault),
1124
- },
1125
- ],
1357
+ payload: [stringValue(faultReport(fault))],
1126
1358
  };
1127
1359
  act.fault = null;
1128
1360
  if (act.context.actor)
@@ -1155,6 +1387,29 @@ class ExecutorV1 {
1155
1387
  act.block = target;
1156
1388
  act.opIndex = 0;
1157
1389
  }
1390
+ callDirectory(act, term, operation) {
1391
+ const args = term.args.map((operand) => this.boundary(act, operand));
1392
+ const result = this.directories.call(operation, args, term.result_resource);
1393
+ if (!(result instanceof Promise)) {
1394
+ this.completeShim(act, term, result);
1395
+ return;
1396
+ }
1397
+ // Acquisitions are synchronous in the language. Retain borrowed inputs and
1398
+ // publish the new owner before the next authored cancellation safepoint.
1399
+ this.running = false;
1400
+ act.context.parked = true;
1401
+ act.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
1402
+ act.context.pendingCancellation ??= fault;
1403
+ };
1404
+ this.scheduler.submitHost(act.context.id, result, (outcome) => {
1405
+ act.context.parked = false;
1406
+ act.context.fileError = outcome.error;
1407
+ this.completeShim(act, term, outcome.value);
1408
+ this.runFrame(act);
1409
+ }, (error) => {
1410
+ throw error;
1411
+ });
1412
+ }
1158
1413
  /// Run a runtime or extern shim. A fault it raises takes the call's `unwind`
1159
1414
  /// edge so the cleanup the package names runs before the program ends.
1160
1415
  callShim(act, term, shim) {
@@ -1168,7 +1423,9 @@ class ExecutorV1 {
1168
1423
  : (this.pkg.variants[shapeId] ?? null);
1169
1424
  let value;
1170
1425
  try {
1171
- value = shim(this.host, args, shape);
1426
+ const aggregateId = term.aggregate_result_shape;
1427
+ const aggregate = aggregateId == null ? null : (this.pkg.aggregates[aggregateId] ?? null);
1428
+ value = shim(this.host, args, shape, aggregate);
1172
1429
  }
1173
1430
  catch (error) {
1174
1431
  if (error instanceof ShimFault) {
@@ -1217,7 +1474,7 @@ class ExecutorV1 {
1217
1474
  this.keepInputs(act, term, args);
1218
1475
  this.raiseFault(act, fault, term.unwind);
1219
1476
  resume();
1220
- });
1477
+ }, act.context.actor, act.rcOwner);
1221
1478
  };
1222
1479
  const next = (value = UNIT) => {
1223
1480
  let step;
@@ -1235,6 +1492,9 @@ class ExecutorV1 {
1235
1492
  this.completeShim(act, term, step.value);
1236
1493
  resume();
1237
1494
  }
1495
+ else if (step.value.kind === "fault") {
1496
+ fail({ kind: "panic", message: step.value.message });
1497
+ }
1238
1498
  else if (step.value.kind === "release") {
1239
1499
  const request = step.value;
1240
1500
  this.closeValueAsync(request.value, this.pipeFault(act), (fault) => {
@@ -1248,11 +1508,11 @@ class ExecutorV1 {
1248
1508
  act.fault ??= fault;
1249
1509
  }
1250
1510
  next();
1251
- });
1511
+ }, act.context.actor, act.rcOwner);
1252
1512
  }
1253
1513
  else {
1254
1514
  const request = step.value;
1255
- const child = this.invokeFrame(act.context.actor, request.callee, request.args, next, fail);
1515
+ const child = this.invokeFrame(act.context.actor, request.callee, request.args, next, fail, act.rcOwner);
1256
1516
  act.context.cancel = (fault) => child.cancel?.(fault);
1257
1517
  }
1258
1518
  };
@@ -1261,18 +1521,35 @@ class ExecutorV1 {
1261
1521
  }
1262
1522
  supervisorPool(act, term, operation) {
1263
1523
  const [view, index] = term.args.map((arg) => this.boundary(act, arg));
1264
- if (view?.kind !== "vector" || index?.kind !== "i64")
1265
- throw new Error("pool operation has no view and index");
1524
+ if (view?.kind !== "vector")
1525
+ throw new Error("pool operation has no view");
1526
+ if (operation === "Len") {
1527
+ this.completeShim(act, term, {
1528
+ kind: "i64",
1529
+ value: BigInt(view.items.length),
1530
+ });
1531
+ return;
1532
+ }
1533
+ if (index?.kind !== "i64")
1534
+ throw new Error("pool operation has no index");
1266
1535
  const value = view.items[Number(index.value)];
1267
1536
  if (operation === "Get") {
1268
1537
  this.completeShim(act, term, this.variant(term.result_shape, value ? "OptionSome" : "OptionNone", value ? [value] : []));
1269
1538
  return;
1270
1539
  }
1271
- if (!value) {
1272
- this.raiseFault(act, { kind: "trap", trap: "vector_bounds" }, term.unwind);
1273
- return;
1540
+ switch (operation) {
1541
+ case "Member":
1542
+ if (!value) {
1543
+ this.raiseFault(act, { kind: "trap", trap: "vector_bounds" }, term.unwind);
1544
+ return;
1545
+ }
1546
+ this.completeShim(act, term, value);
1547
+ return;
1548
+ default: {
1549
+ const exhaustive = operation;
1550
+ throw new Error(`unknown supervisor pool operation ${String(exhaustive)}`);
1551
+ }
1274
1552
  }
1275
- this.completeShim(act, term, value);
1276
1553
  }
1277
1554
  structuralFormat(act, term) {
1278
1555
  if (term.structural == null)
@@ -1282,7 +1559,7 @@ class ExecutorV1 {
1282
1559
  const continueRendering = () => {
1283
1560
  const next = renderer.next();
1284
1561
  if (typeof next === "string") {
1285
- this.completeShim(act, term, { kind: "string", value: next });
1562
+ this.completeShim(act, term, stringValue(next));
1286
1563
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1287
1564
  return;
1288
1565
  }
@@ -1307,7 +1584,92 @@ class ExecutorV1 {
1307
1584
  }
1308
1585
  this.takeEdge(act, term.normal);
1309
1586
  }
1587
+ sweepRcOwner(owner, actor, done) {
1588
+ let firstFault = null;
1589
+ let count = 0;
1590
+ const byType = new Map();
1591
+ const schedule = (work) => this.scheduler.enqueue(actor?.id ?? "actor:root", work);
1592
+ const next = () => {
1593
+ const blocks = owner.blocks;
1594
+ owner.blocks = new Set();
1595
+ if (!blocks.size) {
1596
+ owner.closed = true;
1597
+ if (actor && count)
1598
+ this.trace.snapshot("actor.rc_sweep", {
1599
+ actor_id: actor.id,
1600
+ rc_sweep: {
1601
+ count: String(count),
1602
+ by_type: [...byType]
1603
+ .sort(([a], [b]) => a.localeCompare(b))
1604
+ .map(([type_name, amount]) => ({
1605
+ type_name,
1606
+ count: String(amount),
1607
+ })),
1608
+ },
1609
+ });
1610
+ done(firstFault);
1611
+ return;
1612
+ }
1613
+ const marking = blocks.values();
1614
+ const payloads = blocks.values();
1615
+ let remaining = 64;
1616
+ const release = () => {
1617
+ if (!remaining--) {
1618
+ remaining = 64;
1619
+ schedule(release);
1620
+ return;
1621
+ }
1622
+ const item = payloads.next();
1623
+ if (item.done) {
1624
+ schedule(next);
1625
+ return;
1626
+ }
1627
+ const cell = item.value;
1628
+ const payload = cell.value;
1629
+ cell.value = UNIT;
1630
+ blocks.delete(cell);
1631
+ this.closeValueAsync(payload, this.faultText(firstFault), (fault) => {
1632
+ firstFault = combineFaults(firstFault, fault);
1633
+ release();
1634
+ }, actor, owner);
1635
+ };
1636
+ const mark = () => {
1637
+ for (let i = 0; i < 64; i++) {
1638
+ const item = marking.next();
1639
+ if (item.done) {
1640
+ release();
1641
+ return;
1642
+ }
1643
+ const cell = item.value;
1644
+ cell.dying = true;
1645
+ cell.refs = 0;
1646
+ cell.owner = null;
1647
+ count += 1;
1648
+ byType.set(cell.typeName, (byType.get(cell.typeName) ?? 0) + 1);
1649
+ }
1650
+ schedule(mark);
1651
+ };
1652
+ mark();
1653
+ };
1654
+ next();
1655
+ }
1656
+ finishConstruction(act, done) {
1657
+ if (!act.fn.actor_construction || !act.rcOwner)
1658
+ return false;
1659
+ const owner = act.rcOwner;
1660
+ act.rcOwner = null;
1661
+ this.running = false;
1662
+ this.sweepRcOwner(owner, act.context.actor, (fault) => {
1663
+ act.fault = combineFaults(act.fault, fault);
1664
+ done();
1665
+ const current = this.current;
1666
+ this.scheduler.enqueue(current.context.id, () => this.runFrame(current));
1667
+ });
1668
+ return true;
1669
+ }
1310
1670
  returnFrom(act, value) {
1671
+ if (this.finishConstruction(act, () => this.returnFrom(act, value)))
1672
+ return;
1311
1673
  const caller = act.caller;
1312
1674
  if (!caller) {
1313
1675
  this.running = false;
@@ -1332,6 +1694,14 @@ class ExecutorV1 {
1332
1694
  this.current = caller;
1333
1695
  this.takeEdge(caller, act.normal);
1334
1696
  }
1697
+ dynamicEntry(receiver, slot) {
1698
+ const table = this.pkg.vtables[receiver.vtable] ??
1699
+ this.pkg.vtables.find((table) => table.id === receiver.vtable);
1700
+ const entry = table?.slots.find((entry) => entry.slot === slot);
1701
+ if (!entry)
1702
+ throw new Error(`vtable ${receiver.vtable} has no slot ${slot}`);
1703
+ return entry;
1704
+ }
1335
1705
  /// The wrapped value on its way to a trait method's `self`. A `copy` receiver
1336
1706
  /// deep-clones, a `move` invalidates the trait object it came out of, and a
1337
1707
  /// borrow passes the live value.
@@ -1359,6 +1729,8 @@ class ExecutorV1 {
1359
1729
  this.takeEdge(act, unwind);
1360
1730
  }
1361
1731
  resumeUnwind(act, handback) {
1732
+ if (this.finishConstruction(act, () => this.resumeUnwind(act, handback)))
1733
+ return;
1362
1734
  const fault = act.fault;
1363
1735
  if (!fault) {
1364
1736
  throw new Error(`${act.fn.name}: resume_unwind with no pending fault`);
@@ -1366,7 +1738,7 @@ class ExecutorV1 {
1366
1738
  const caller = act.caller;
1367
1739
  if (!caller) {
1368
1740
  this.running = false;
1369
- act.context.failed(fault);
1741
+ act.context.failed(fault, handback);
1370
1742
  return;
1371
1743
  }
1372
1744
  if (!act.unwind) {
@@ -1387,7 +1759,11 @@ class ExecutorV1 {
1387
1759
  this.current = caller;
1388
1760
  this.takeEdge(caller, act.unwind);
1389
1761
  }
1390
- suspend(act, term) {
1762
+ suspend(act, input) {
1763
+ const registration = resolveSuspension(input);
1764
+ if (registration.kind === "refused")
1765
+ throw new Error(`suspension ${registration.capability} was refused after admission`);
1766
+ const term = registration.term;
1391
1767
  switch (term.kind) {
1392
1768
  case "Yield": {
1393
1769
  const generator = act.context.generator;
@@ -1436,37 +1812,27 @@ class ExecutorV1 {
1436
1812
  return;
1437
1813
  }
1438
1814
  case "StreamSend": {
1439
- const [sink, value] = term.inputs.map((input) => this.boundary(act, input));
1440
- let cancelWait = () => { };
1441
- let owned = true;
1442
- let closing = false;
1443
- let cancelled = null;
1444
- const wake = this.park(act, term, () => cancelWait());
1815
+ const sink = this.boundary(act, term.inputs[0]);
1816
+ const value = this.boundary(act, term.inputs[1]);
1817
+ const wake = this.park(act, term);
1445
1818
  const cancel = act.context.cancel;
1819
+ const stop = this.sendStream(sink, value, term.detail.park, act.context.actor, (status, fault) => {
1820
+ wake(UNIT, fault, status);
1821
+ }, act.rcOwner);
1446
1822
  act.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
1447
- cancelled ??= fault;
1448
- cancelWait();
1449
- if (owned) {
1450
- owned = false;
1451
- closing = true;
1452
- this.closeValueAsync(value, this.faultText(fault), (closeFault) => cancel(closeFault ?? fault));
1453
- }
1454
- else if (!closing)
1455
- cancel(fault);
1823
+ stop(fault, (closeFault) => cancel?.(closeFault ?? fault));
1456
1824
  };
1457
- cancelWait = this.pipes.send(sink, value, term.detail.park, (status) => {
1458
- owned = false;
1459
- if (status === 0)
1460
- wake(UNIT);
1461
- else {
1462
- closing = true;
1463
- this.closeValueAsync(value, this.pipeFault(act), (fault) => wake(UNIT, fault ?? cancelled, status));
1464
- }
1465
- });
1466
1825
  return;
1467
1826
  }
1468
1827
  case "StreamNext": {
1469
1828
  const stream = this.boundary(act, term.inputs[0]);
1829
+ const adapter = stream.kind === "stream"
1830
+ ? this.generatorStreams.get(stream.channelId)
1831
+ : undefined;
1832
+ if (adapter) {
1833
+ this.receiveGeneratorStream(act, term, adapter);
1834
+ return;
1835
+ }
1470
1836
  let cancelWait = () => { };
1471
1837
  const wake = this.park(act, term, () => cancelWait());
1472
1838
  cancelWait = this.pipes.receive(stream, term.detail.park, (value, fault) => {
@@ -1479,10 +1845,13 @@ class ExecutorV1 {
1479
1845
  }
1480
1846
  case "Await": {
1481
1847
  const task = this.taskFor(this.boundary(act, term.inputs[0]));
1482
- const wake = this.park(act, term);
1848
+ let unwatch = () => { };
1849
+ const wake = this.park(act, term, () => unwatch());
1483
1850
  const ready = () => {
1484
- if (!task.done)
1851
+ if (!task.done) {
1852
+ this.pendingActorWait(act, [task.actorTarget], false, "ask", (fault) => wake(UNIT, fault));
1485
1853
  return;
1854
+ }
1486
1855
  if (task.fault)
1487
1856
  wake(UNIT, task.fault);
1488
1857
  else {
@@ -1492,6 +1861,11 @@ class ExecutorV1 {
1492
1861
  }
1493
1862
  };
1494
1863
  task.changed.push(ready);
1864
+ unwatch = () => {
1865
+ const index = task.changed.indexOf(ready);
1866
+ if (index >= 0)
1867
+ task.changed.splice(index, 1);
1868
+ };
1495
1869
  ready();
1496
1870
  return;
1497
1871
  }
@@ -1554,6 +1928,11 @@ class ExecutorV1 {
1554
1928
  cancelTimer?.();
1555
1929
  wake({ kind: "i64", value: BigInt(ready[0].index) });
1556
1930
  }
1931
+ else {
1932
+ this.pendingActorWait(act, sources.map((value) => value.kind === "stream"
1933
+ ? undefined
1934
+ : this.taskFor(value).actorTarget), hasTimeout, "select", (fault) => wake(UNIT, fault));
1935
+ }
1557
1936
  };
1558
1937
  if (hasTimeout) {
1559
1938
  const duration = values[values.length - 1];
@@ -1581,27 +1960,115 @@ class ExecutorV1 {
1581
1960
  const protocol = term.detail;
1582
1961
  const inputs = term.inputs.map((input) => this.boundary(act, input));
1583
1962
  const wake = this.park(act, term);
1963
+ let target;
1964
+ let checkWait;
1584
1965
  const cancelRequest = this.ask(protocol, inputs, (value, error, closeFault) => {
1585
1966
  act.fault ??= closeFault ?? null;
1586
1967
  return wake(this.completion(term.result_shape, term.error_shape, value, error));
1587
- }, term.request_shapes);
1968
+ }, term.request_shapes, (actor) => {
1969
+ target = actor;
1970
+ checkWait?.();
1971
+ });
1588
1972
  const cancel = act.context.cancel;
1589
1973
  act.context.cancel = (fault) => cancelRequest((closeFault) => cancel(closeFault ?? fault));
1974
+ checkWait = () => this.pendingActorWait(act, [target], false, "ask", (fault) => cancelRequest((closeFault) => wake(UNIT, closeFault ?? fault)));
1975
+ checkWait();
1590
1976
  return;
1591
1977
  }
1592
1978
  case "NativeIo": {
1979
+ if (term.detail.operation === "FileRead" ||
1980
+ term.detail.operation === "FileWrite") {
1981
+ const args = term.inputs.map((operand) => this.boundary(act, operand));
1982
+ const wake = this.park(act, term);
1983
+ const cancel = act.context.cancel;
1984
+ let stopped = act.context.pendingCancellation ?? this.scopeDeadline(act) ?? null;
1985
+ act.context.cancel = (fault = {
1986
+ kind: "panic",
1987
+ message: "task cancelled",
1988
+ cancelled: true,
1989
+ }) => {
1990
+ stopped ??= fault;
1991
+ };
1992
+ this.scheduler.submitHost(act.context.id, this.directories.io(term.detail.operation, args, () => stopped !== null || this.terminating), (outcome) => {
1993
+ // The producer is quiescent. Restore park's latch so cancellation
1994
+ // between readiness and frame resumption still retires the result.
1995
+ act.context.cancel = cancel;
1996
+ act.context.fileError = outcome.error;
1997
+ if (stopped) {
1998
+ this.closeValue(outcome.value, null);
1999
+ cancel(stopped);
2000
+ }
2001
+ else
2002
+ wake(outcome.value);
2003
+ }, (error) => {
2004
+ throw error;
2005
+ });
2006
+ return;
2007
+ }
1593
2008
  if (term.detail?.operation === "SinkFinish") {
1594
- this.host.pipes.close(this.boundary(act, term.inputs[0]));
1595
- this.park(act, term)(UNIT);
2009
+ const wake = this.park(act, term);
2010
+ const cancel = act.context.cancel;
2011
+ let stopped = null;
2012
+ act.context.cancel = (fault = {
2013
+ kind: "panic",
2014
+ message: "task cancelled",
2015
+ cancelled: true,
2016
+ }) => {
2017
+ stopped ??= fault;
2018
+ };
2019
+ this.finishSink(this.boundary(act, term.inputs[0]), null, () => {
2020
+ if (stopped)
2021
+ cancel(stopped);
2022
+ else
2023
+ wake(UNIT);
2024
+ });
1596
2025
  return;
1597
2026
  }
1598
- // Admission passed `StdinReadLine`. The record is the raw line,
2027
+ if (term.detail.operation !== "StdinReadLine") {
2028
+ const exhaustive = term.detail.operation;
2029
+ throw new Error(`unknown native I/O handler ${String(exhaustive)}`);
2030
+ }
2031
+ // The record is the raw line,
1599
2032
  // so the records concatenate to the stdin the program consumed.
1600
2033
  const line = this.stdin.readLine() ?? new Uint8Array();
1601
2034
  this.trace.recordReplayInput({ kind: "stdin", data: new TextDecoder().decode(line) }, false);
1602
2035
  this.park(act, term)(bytesValue([...line]));
1603
2036
  return;
1604
2037
  }
2038
+ case "Safepoint": {
2039
+ if (act.context.pollSuppression) {
2040
+ this.takeEdge(act, term.resumes[0]);
2041
+ return;
2042
+ }
2043
+ if (act.context.pendingCancellation && term.cancel) {
2044
+ act.fault = act.context.pendingCancellation;
2045
+ this.takeEdge(act, term.cancel);
2046
+ return;
2047
+ }
2048
+ const budget = term.detail?.budget;
2049
+ if (!Number.isInteger(budget) || budget <= 0)
2050
+ throw new Error("safepoint requires a positive compiler-authored budget");
2051
+ const countdown = act.context.pollBudget;
2052
+ countdown.remaining = (countdown.remaining || budget) - 1;
2053
+ if (countdown.remaining > 0) {
2054
+ this.takeEdge(act, term.resumes[0]);
2055
+ return;
2056
+ }
2057
+ countdown.remaining = budget;
2058
+ this.scheduler.advanceSlice();
2059
+ const fault = act.context.pendingCancellation ?? this.scopeDeadline(act);
2060
+ if (fault && term.cancel) {
2061
+ act.fault = fault;
2062
+ this.takeEdge(act, term.cancel);
2063
+ }
2064
+ else if (this.scheduler.hasRunnableWork()) {
2065
+ this.park(act, term)(UNIT);
2066
+ }
2067
+ else {
2068
+ this.takeEdge(act, term.resumes[0]);
2069
+ }
2070
+ return;
2071
+ }
1605
2072
  case "Sleep": {
1606
2073
  const duration = term.inputs[0]
1607
2074
  ? this.boundary(act, term.inputs[0])
@@ -1612,17 +2079,40 @@ class ExecutorV1 {
1612
2079
  cancelTimer = this.scheduler.after(nanos, () => wake(UNIT));
1613
2080
  return;
1614
2081
  }
1615
- default:
1616
- throw new Error(`suspension kind ${term.kind} resolved no handler after admission`);
2082
+ default: {
2083
+ const exhaustive = term;
2084
+ throw new Error(`unknown suspension handler ${String(exhaustive)}`);
2085
+ }
1617
2086
  }
1618
2087
  }
2088
+ pendingActorWait(act, targets, externalProgress, operation, fail) {
2089
+ const owner = act.context.actor;
2090
+ // Independent tasks carry actor ownership for cleanup, but do not hold its turn.
2091
+ if (!owner || owner.active !== act.context || !act.context.parked)
2092
+ return;
2093
+ this.actorWaits.ready(owner);
2094
+ if (externalProgress ||
2095
+ this.requestDeadline(act) !== undefined ||
2096
+ targets.some((target) => !target))
2097
+ return;
2098
+ const path = this.actorWaits.pending(owner, targets);
2099
+ if (path)
2100
+ fail({
2101
+ kind: "panic",
2102
+ message: `local actor wait cycle at ${operation}: ${path.map((actor) => actor.identity).join(" -> ")}`,
2103
+ });
2104
+ }
1619
2105
  park(act, term, cleanup = () => { }) {
1620
2106
  this.running = false;
2107
+ act.context.parked = true;
1621
2108
  let pending = null;
1622
2109
  const resume = (value, fault = null, edge = 0) => {
1623
2110
  if (pending)
1624
2111
  return false;
1625
2112
  pending = { value, fault, edge };
2113
+ act.context.parked = false;
2114
+ if (act.context.actor?.active === act.context)
2115
+ this.actorWaits.ready(act.context.actor);
1626
2116
  cleanup();
1627
2117
  this.scheduler.enqueue(act.context.id, () => {
1628
2118
  const outcome = pending;
@@ -1631,7 +2121,8 @@ class ExecutorV1 {
1631
2121
  const fault = closeFault ?? (outcome.fault ? (act.fault ?? outcome.fault) : null);
1632
2122
  if (fault) {
1633
2123
  act.fault = fault;
1634
- this.takeEdge(act, fault.kind === "panic" && fault.cancelled
2124
+ this.takeEdge(act, term.kind === "Safepoint" ||
2125
+ (fault.kind === "panic" && fault.cancelled)
1635
2126
  ? term.cancel
1636
2127
  : term.unwind);
1637
2128
  }
@@ -1649,13 +2140,15 @@ class ExecutorV1 {
1649
2140
  // the cancellation latch live through that window and retire a result
1650
2141
  // whose normal edge the consumer never entered.
1651
2142
  if (outcome.fault)
1652
- this.closeValueAsync(outcome.value, this.faultText(act.fault ?? outcome.fault), finish, act.context.actor);
2143
+ this.closeValueAsync(outcome.value, this.faultText(act.fault ?? outcome.fault), finish, act.context.actor, act.rcOwner);
1653
2144
  else
1654
2145
  finish();
1655
2146
  });
1656
2147
  return true;
1657
2148
  };
1658
2149
  act.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
2150
+ if (term.cancel == null)
2151
+ return;
1659
2152
  if (pending)
1660
2153
  pending.fault ??= fault;
1661
2154
  else
@@ -1663,32 +2156,248 @@ class ExecutorV1 {
1663
2156
  };
1664
2157
  return resume;
1665
2158
  }
1666
- newPipe(capacity) {
1667
- const id = this.ids.channel();
1668
- this.trace.allocateId("channel", id, this.current?.context.id ?? "actor:root");
1669
- return this.pipes.create(id, capacity);
1670
- }
1671
- faultText(fault) {
1672
- return !fault ||
1673
- (fault.kind === "panic" && (fault.cancelled || fault.deadline))
1674
- ? null
1675
- : fault.kind === "panic"
1676
- ? fault.message
1677
- : (fault.message ?? trapMessage(fault.trap));
2159
+ sinkClosed(value) {
2160
+ const endpoint = value.kind === "sink"
2161
+ ? this.senderEndpoints.get(value.channelId)
2162
+ : undefined;
2163
+ return endpoint ? !endpoint.open : this.pipes.isClosed(value);
1678
2164
  }
1679
- pipeFault(act) {
1680
- return this.faultText(act.context.actor?.crashing ?? act.fault);
2165
+ senderEndpoint(owner, open = true) {
2166
+ const id = this.ids.channel();
2167
+ owner.references++;
2168
+ if (open)
2169
+ owner.endpoints++;
2170
+ this.senderEndpoints.set(id, { owner, open });
2171
+ return { kind: "sink", channelId: id };
1681
2172
  }
1682
- releaseOperation(act, value, finish) {
1683
- let synchronous = true;
1684
- let completed = false;
1685
- this.closeValueAsync(value, this.pipeFault(act), (fault) => {
1686
- completed = true;
1687
- finish?.();
1688
- act.fault = combineFaults(act.fault, fault);
1689
- if (!synchronous)
2173
+ finishSink(value, fault, done) {
2174
+ const endpoint = value.kind === "sink"
2175
+ ? this.senderEndpoints.get(value.channelId)
2176
+ : undefined;
2177
+ if (!endpoint) {
2178
+ this.pipes.close(value, fault);
2179
+ done();
2180
+ return;
2181
+ }
2182
+ if (!endpoint.open) {
2183
+ done();
2184
+ return;
2185
+ }
2186
+ endpoint.open = false;
2187
+ endpoint.owner.endpoints--;
2188
+ if (fault || endpoint.owner.endpoints === 0) {
2189
+ const finish = () => {
2190
+ if (endpoint.owner.busy) {
2191
+ endpoint.owner.waiting.add(finish);
2192
+ return;
2193
+ }
2194
+ this.finishSink(endpoint.owner.sink, fault, done);
2195
+ };
2196
+ finish();
2197
+ }
2198
+ else
2199
+ done();
2200
+ }
2201
+ sendStream(sink, value, park, actor, complete, rcOwner) {
2202
+ const endpoint = sink.kind === "sink"
2203
+ ? this.senderEndpoints.get(sink.channelId)
2204
+ : undefined;
2205
+ let owned = true;
2206
+ let finished = false;
2207
+ let closing = false;
2208
+ let claimed = false;
2209
+ let stopped = null;
2210
+ let encoder = null;
2211
+ let child = null;
2212
+ let withdraw = () => { };
2213
+ const drained = [];
2214
+ const release = () => {
2215
+ if (!claimed || !endpoint)
2216
+ return;
2217
+ claimed = false;
2218
+ endpoint.owner.busy = false;
2219
+ for (const wake of endpoint.owner.waiting)
2220
+ this.scheduler.enqueue(actor?.id ?? "actor:root", wake);
2221
+ endpoint.owner.waiting.clear();
2222
+ };
2223
+ const finish = (status, fault = null) => {
2224
+ if (finished || closing)
2225
+ return;
2226
+ closing = true;
2227
+ release();
2228
+ const done = (closeFault = null) => {
2229
+ finished = true;
2230
+ const failure = combineFaults(fault, closeFault);
2231
+ if (stopped)
2232
+ for (const wake of drained.splice(0))
2233
+ wake(failure);
2234
+ else
2235
+ complete(status, failure);
2236
+ };
2237
+ if (owned) {
2238
+ owned = false;
2239
+ this.closeValueAsync(value, this.faultText(fault ?? stopped), done, actor, rcOwner);
2240
+ }
2241
+ else
2242
+ done();
2243
+ };
2244
+ const start = () => {
2245
+ if (stopped || finished || closing)
2246
+ return;
2247
+ if (!endpoint) {
2248
+ withdraw = this.pipes.send(sink, value, park, (status) => {
2249
+ if (status === 0)
2250
+ owned = false;
2251
+ finish(status);
2252
+ });
2253
+ return;
2254
+ }
2255
+ if (!endpoint.open) {
2256
+ finish(1);
2257
+ return;
2258
+ }
2259
+ const owner = endpoint.owner;
2260
+ if (owner.busy) {
2261
+ if (!park) {
2262
+ finish(2);
2263
+ return;
2264
+ }
2265
+ owner.waiting.add(start);
2266
+ withdraw = () => {
2267
+ owner.waiting.delete(start);
2268
+ };
2269
+ return;
2270
+ }
2271
+ owner.busy = true;
2272
+ claimed = true;
2273
+ const callable = owner.encoder;
2274
+ encoder = this.invokeFrame(actor, calleeFunction(callable, null), callable.kind === "closure" ? [callable.environment, value] : [value], (encoded) => {
2275
+ encoder = null;
2276
+ if (stopped) {
2277
+ this.closeValueAsync(encoded, this.faultText(stopped), (fault) => finish(0, fault), actor, rcOwner);
2278
+ }
2279
+ else {
2280
+ child = this.sendStream(owner.sink, encoded, park, actor, (status, fault) => {
2281
+ child = null;
2282
+ finish(status, fault);
2283
+ }, rcOwner);
2284
+ }
2285
+ }, (fault) => {
2286
+ encoder = null;
2287
+ finish(3, fault);
2288
+ }, rcOwner);
2289
+ };
2290
+ start();
2291
+ return (fault, done) => {
2292
+ if (finished) {
2293
+ done(null);
2294
+ return;
2295
+ }
2296
+ stopped ??= fault;
2297
+ drained.push(done);
2298
+ withdraw();
2299
+ if (closing)
2300
+ return;
2301
+ if (encoder)
2302
+ encoder.cancel?.(fault);
2303
+ else if (child)
2304
+ child(fault, (failure) => {
2305
+ child = null;
2306
+ finish(0, failure);
2307
+ });
2308
+ else
2309
+ finish(0);
2310
+ };
2311
+ }
2312
+ receiveGeneratorStream(act, term, adapter) {
2313
+ const wake = this.park(act, term);
2314
+ const receive = (value, fault) => {
2315
+ if (fault)
2316
+ wake(UNIT, fault);
2317
+ else
2318
+ wake(this.variant(term.result_shape, value === null ? "OptionNone" : "OptionSome", value === null ? [] : [value]));
2319
+ };
2320
+ if (adapter.pending !== null) {
2321
+ const value = adapter.pending;
2322
+ adapter.pending = null;
2323
+ receive(value, null);
2324
+ return;
2325
+ }
2326
+ const generator = adapter.generator;
2327
+ if (generator.done) {
2328
+ receive(null, adapter.fault ?? generator.fault);
2329
+ return;
2330
+ }
2331
+ if (term.detail.park)
2332
+ adapter.waiting = receive;
2333
+ else
2334
+ receive(null, null);
2335
+ const cancel = act.context.cancel;
2336
+ if (term.detail.park)
2337
+ act.context.cancel = (fault) => {
2338
+ adapter.waiting = undefined;
2339
+ this.closeGenerator(generator, (closeFault) => cancel?.(closeFault ?? fault));
2340
+ };
2341
+ if (adapter.receiving)
2342
+ return;
2343
+ adapter.receiving = true;
2344
+ generator.receive = (value, fault) => {
2345
+ adapter.receiving = false;
2346
+ if (adapter.closed) {
2347
+ if (value !== null)
2348
+ this.closeValueAsync(value, null, () => { });
2349
+ return;
2350
+ }
2351
+ adapter.fault = fault;
2352
+ const waiting = adapter.waiting;
2353
+ adapter.waiting = undefined;
2354
+ if (waiting)
2355
+ waiting(value, fault);
2356
+ else
2357
+ adapter.pending = value;
2358
+ };
2359
+ if (generator.started) {
2360
+ const resume = generator.resume;
2361
+ generator.resume = undefined;
2362
+ if (!resume)
2363
+ throw new Error("generator stream lost its suspended yield");
2364
+ resume();
2365
+ }
2366
+ else {
2367
+ this.scheduler.enqueue(generator.id, () => {
2368
+ if (generator.done)
2369
+ return;
2370
+ generator.started = true;
2371
+ this.runFrame(generator.frame);
2372
+ });
2373
+ }
2374
+ }
2375
+ newPipe(capacity) {
2376
+ const id = this.ids.channel();
2377
+ this.trace.allocateId("channel", id, this.current?.context.id ?? "actor:root");
2378
+ return this.pipes.create(id, capacity);
2379
+ }
2380
+ faultText(fault) {
2381
+ return !fault ||
2382
+ (fault.kind === "panic" && (fault.cancelled || fault.deadline))
2383
+ ? null
2384
+ : fault.kind === "panic"
2385
+ ? fault.message
2386
+ : (fault.message ?? trapMessage(fault.trap));
2387
+ }
2388
+ pipeFault(act) {
2389
+ return this.faultText(act.context.actor?.crashing ?? act.fault);
2390
+ }
2391
+ releaseOperation(act, value, finish) {
2392
+ let synchronous = true;
2393
+ let completed = false;
2394
+ this.closeValueAsync(value, this.pipeFault(act), (fault) => {
2395
+ completed = true;
2396
+ finish?.();
2397
+ act.fault = combineFaults(act.fault, fault);
2398
+ if (!synchronous)
1690
2399
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1691
- });
2400
+ }, act.context.actor, act.rcOwner);
1692
2401
  synchronous = false;
1693
2402
  if (!completed)
1694
2403
  this.running = false;
@@ -1713,7 +2422,7 @@ class ExecutorV1 {
1713
2422
  receive?.(null, generator.fault);
1714
2423
  for (const closed of generator.closed.splice(0))
1715
2424
  closed(generator.fault);
1716
- });
2425
+ }, generator.frame.context.actor, generator.frame.rcOwner);
1717
2426
  }
1718
2427
  closeGenerator(generator, done) {
1719
2428
  if (generator.done) {
@@ -1726,7 +2435,7 @@ class ExecutorV1 {
1726
2435
  else
1727
2436
  generator.frame.context.cancel?.();
1728
2437
  }
1729
- closeValueAsync(value, fault, done, actor = this.current.context.actor) {
2438
+ closeValueAsync(value, fault, done, actor = this.current.context.actor, rcOwner = actor?.rcOwner ?? this.current.rcOwner) {
1730
2439
  const pending = [value];
1731
2440
  let firstFault = null;
1732
2441
  const next = (failure = null) => {
@@ -1737,15 +2446,41 @@ class ExecutorV1 {
1737
2446
  while (pending.length) {
1738
2447
  const value = pending.pop();
1739
2448
  switch (value.kind) {
2449
+ case "string":
2450
+ value.release();
2451
+ break;
1740
2452
  case "rc":
1741
2453
  if (!value.closed) {
1742
2454
  value.closed = true;
1743
- if (--value.cell.refs === 0) {
2455
+ if (value.cell && !value.cell.dying && --value.cell.refs === 0) {
2456
+ value.cell.owner?.blocks.delete(value.cell);
2457
+ value.cell.owner = null;
1744
2458
  pending.push(value.cell.value);
1745
2459
  value.cell.value = UNIT;
1746
2460
  }
1747
2461
  }
1748
2462
  break;
2463
+ case "weak":
2464
+ if (!value.closed) {
2465
+ value.closed = true;
2466
+ value.cell.weak -= 1;
2467
+ }
2468
+ break;
2469
+ case "host_resource": {
2470
+ if (value.owner.closed)
2471
+ break;
2472
+ this.closeHostResource(value);
2473
+ break;
2474
+ }
2475
+ case "timer": {
2476
+ if (value.closed)
2477
+ break;
2478
+ const resource = this.pkg.resources?.[value.resource];
2479
+ if (resource?.kind !== "opaque")
2480
+ throw new Error("opaque owner lost its selected close");
2481
+ this.invokeFrame(actor, resource.close, [value], () => next(), next, rcOwner);
2482
+ return;
2483
+ }
1749
2484
  case "task": {
1750
2485
  // Dropping a handle does not cancel scoped work. Join drains the
1751
2486
  // task, and scope close releases any result that was never taken.
@@ -1753,7 +2488,7 @@ class ExecutorV1 {
1753
2488
  if (task?.done) {
1754
2489
  const result = task.value;
1755
2490
  task.value = UNIT;
1756
- this.closeValueAsync(result, currentFault, (fault) => next(fault ?? (task.close ? task.fault : null)), actor);
2491
+ this.closeValueAsync(result, currentFault, (fault) => next(fault ?? (task.close ? task.fault : null)), actor, rcOwner);
1757
2492
  return;
1758
2493
  }
1759
2494
  if (task?.close) {
@@ -1777,11 +2512,11 @@ class ExecutorV1 {
1777
2512
  break;
1778
2513
  case "record": {
1779
2514
  const resource = (this.pkg.resources ?? []).find((resource) => resource.kind === "record" && resource.shape === value.shape);
1780
- if (resource?.close !== undefined) {
2515
+ if (resource?.kind === "record") {
1781
2516
  if (this.consumedResources.has(value))
1782
2517
  break;
1783
2518
  if (!this.closingResources.has(value)) {
1784
- this.invokeFrame(actor, resource.close, [value], () => next(), next);
2519
+ this.invokeFrame(actor, resource.close, [value], () => next(), next, rcOwner);
1785
2520
  return;
1786
2521
  }
1787
2522
  this.consumedResources.add(value);
@@ -1795,17 +2530,58 @@ class ExecutorV1 {
1795
2530
  case "vector":
1796
2531
  pending.push(...[...value.items].reverse());
1797
2532
  break;
1798
- case "sink":
1799
- case "stream":
2533
+ case "stream": {
2534
+ const adapter = this.generatorStreams.get(value.channelId);
2535
+ if (adapter) {
2536
+ this.generatorStreams.delete(value.channelId);
2537
+ adapter.closed = true;
2538
+ if (adapter.pending !== null)
2539
+ pending.push(adapter.pending);
2540
+ adapter.pending = null;
2541
+ adapter.waiting = undefined;
2542
+ this.closeGenerator(adapter.generator, next);
2543
+ return;
2544
+ }
1800
2545
  pending.push(...this.pipes.close(value, currentFault).reverse());
1801
2546
  break;
2547
+ }
2548
+ case "sink": {
2549
+ const endpoint = this.senderEndpoints.get(value.channelId);
2550
+ if (endpoint) {
2551
+ this.finishSink(value, currentFault, () => {
2552
+ this.senderEndpoints.delete(value.channelId);
2553
+ if (--endpoint.owner.references === 0)
2554
+ pending.push(endpoint.owner.sink, endpoint.owner.encoder);
2555
+ next();
2556
+ });
2557
+ return;
2558
+ }
2559
+ else
2560
+ pending.push(...this.pipes.close(value, currentFault).reverse());
2561
+ break;
2562
+ }
1802
2563
  }
1803
2564
  }
1804
2565
  done(firstFault);
1805
2566
  };
1806
2567
  next();
1807
2568
  }
2569
+ closeHostResource(value) {
2570
+ if (value.owner.closed)
2571
+ return;
2572
+ const resource = this.pkg.resources?.[value.resource];
2573
+ const release = resource?.kind === "nominal"
2574
+ ? resolveExtern({ symbol: resource.release })
2575
+ : null;
2576
+ if (release?.kind !== "directory" || release.operation.action !== "close")
2577
+ throw new Error("filesystem owner lost its selected release");
2578
+ this.directories.call(release.operation, [value]);
2579
+ }
1808
2580
  closeValue(value, fault) {
2581
+ if (value.kind === "host_resource")
2582
+ this.closeHostResource(value);
2583
+ if (value.kind === "string")
2584
+ value.release();
1809
2585
  if (value.kind === "closure")
1810
2586
  this.closeValue(value.environment, fault);
1811
2587
  else if (value.kind === "sink" || value.kind === "stream")
@@ -1854,25 +2630,33 @@ class ExecutorV1 {
1854
2630
  for (const changed of [...task.changed])
1855
2631
  changed();
1856
2632
  }
1857
- invokeFrame(actor, callable, args, returned, failed) {
1858
- const frame = this.activate(this.functionAt(callable), args, null, null, undefined, null, {
2633
+ invokeFrame(actor, callable, args, returned, failed, rcOwner = actor?.rcOwner ?? null, inline = false) {
2634
+ const context = {
1859
2635
  id: actor?.id ?? "actor:root",
1860
2636
  actor,
2637
+ rcOwner,
1861
2638
  returned,
1862
- failed,
1863
- });
2639
+ failed: (fault) => failed(fault),
2640
+ };
2641
+ const frame = this.activate(this.functionAt(callable), args, null, null, undefined, null, context);
1864
2642
  let cancelled = null;
1865
- frame.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
2643
+ context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
1866
2644
  cancelled = fault;
1867
2645
  };
1868
- this.scheduler.enqueue(frame.context.id, () => {
1869
- frame.context.cancel = undefined;
1870
- if (cancelled)
1871
- frame.context.failed(cancelled);
1872
- else
1873
- this.runFrame(frame);
1874
- });
1875
- return frame.context;
2646
+ if (inline) {
2647
+ this.current = frame;
2648
+ this.running = true;
2649
+ }
2650
+ else {
2651
+ this.scheduler.enqueue(frame.context.id, () => {
2652
+ context.cancel = undefined;
2653
+ if (cancelled && frame.context === context)
2654
+ context.failed(cancelled);
2655
+ else
2656
+ this.runFrame(frame);
2657
+ });
2658
+ }
2659
+ return context;
1876
2660
  }
1877
2661
  variant(shapeId, role, payload = []) {
1878
2662
  const shape = shapeId == null ? undefined : this.pkg.variants[shapeId];
@@ -1920,12 +2704,21 @@ class ExecutorV1 {
1920
2704
  ? { kind: "Actor", identity: act.context.actor.identity }
1921
2705
  : { kind: "Task" };
1922
2706
  switch (operation.op) {
2707
+ case "identity": {
2708
+ const [left, right] = args;
2709
+ this.completeShim(act, term, {
2710
+ kind: "bool",
2711
+ value: actorIdentical(left, right),
2712
+ });
2713
+ return;
2714
+ }
1923
2715
  case "spawn": {
1924
2716
  const layout = this.pkg.actors?.find((actor) => actor.id === operation.actor);
1925
2717
  if (!layout)
1926
2718
  throw new Error(`actor layout ${operation.actor} is missing`);
1927
2719
  let supplied = 0;
1928
2720
  const actor = {
2721
+ rcOwner: act.rcOwner ?? { blocks: new Set(), closed: false },
1929
2722
  identity: ++this.actorIdentity,
1930
2723
  id: this.ids.actor(),
1931
2724
  layout,
@@ -1935,9 +2728,11 @@ class ExecutorV1 {
1935
2728
  fields: layout.state_fields.map((field) => field.deferred ? UNIT : (args[supplied++] ?? UNIT)),
1936
2729
  },
1937
2730
  mailbox: [],
2731
+ timers: new Set(),
1938
2732
  disposals: [],
1939
2733
  admission: [],
1940
2734
  busy: true,
2735
+ constructing: true,
1941
2736
  alive: true,
1942
2737
  completed: false,
1943
2738
  closing: false,
@@ -1947,44 +2742,194 @@ class ExecutorV1 {
1947
2742
  closingFault: null,
1948
2743
  crashing: null,
1949
2744
  active: null,
1950
- stopHook: null,
2745
+ terminalHook: null,
1951
2746
  closed: [],
1952
2747
  };
1953
2748
  this.actors.set(actor.id, actor);
1954
2749
  this.trace.allocateId("actor", actor.id, act.context.id);
1955
2750
  this.trace.snapshot("actor.spawn", { actor_id: actor.id });
1956
2751
  const handle = { kind: "actor", id: actor.id };
1957
- this.running = false;
2752
+ act.context.cancel = (fault = {
2753
+ kind: "panic",
2754
+ message: "actor construction cancelled",
2755
+ cancelled: true,
2756
+ }) => {
2757
+ actor.closing = true;
2758
+ if (actor.active) {
2759
+ actor.active.pendingCancellation ??= fault;
2760
+ actor.active.cancel?.(actor.active.pendingCancellation);
2761
+ }
2762
+ };
1958
2763
  const failed = (fault) => {
2764
+ act.context.cancel = undefined;
2765
+ actor.active = null;
1959
2766
  actor.alive = false;
1960
- this.closeValueAsync(actor.state, this.faultText(fault), () => {
1961
- actor.completed = true;
1962
- for (const message of actor.mailbox.splice(0))
1963
- message.complete(null, "Dead");
1964
- for (const closed of actor.closed.splice(0))
1965
- closed();
1966
- this.raiseFault(act, fault, term.unwind);
1967
- this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2767
+ actor.constructing = false;
2768
+ act.rcOwner = null;
2769
+ this.cancelActorTimers(actor);
2770
+ this.drainMailbox(actor, this.faultText(fault), (payloadFault) => {
2771
+ const primary = combineFaults(fault, payloadFault);
2772
+ this.closeValueAsync(actor.state, this.faultText(primary), (stateFault) => {
2773
+ const retained = combineFaults(primary, stateFault);
2774
+ this.sweepRcOwner(actor.rcOwner, actor, (sweepFault) => {
2775
+ actor.completed = true;
2776
+ for (const closed of actor.closed.splice(0))
2777
+ closed();
2778
+ this.raiseFault(act, combineFaults(retained, sweepFault), term.unwind);
2779
+ this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2780
+ });
2781
+ }, actor, actor.rcOwner);
1968
2782
  });
1969
2783
  };
1970
2784
  const started = () => {
2785
+ if (actor.closing) {
2786
+ failed(actor.active?.pendingCancellation ?? {
2787
+ kind: "panic",
2788
+ message: "actor construction cancelled",
2789
+ cancelled: true,
2790
+ });
2791
+ return;
2792
+ }
2793
+ actor.constructing = false;
2794
+ actor.active = null;
2795
+ act.context.cancel = undefined;
2796
+ act.rcOwner = null;
1971
2797
  actor.busy = false;
2798
+ this.startPeriodicTimers(actor);
1972
2799
  this.dispatchActor(actor);
1973
2800
  this.completeShim(act, term, handle);
1974
- this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2801
+ this.current = act;
2802
+ this.running = true;
1975
2803
  };
2804
+ // Construction enters the same interpreter loop as its caller. Only
2805
+ // a real suspension queues work and advances deterministic scheduling.
1976
2806
  const start = () => {
1977
- if (layout.start !== undefined)
1978
- this.invokeFrame(actor, layout.start, [actor.state], started, failed);
2807
+ if (layout.start != null)
2808
+ actor.active = this.invokeFrame(actor, layout.start, [actor.state], started, failed, actor.rcOwner, true);
1979
2809
  else
1980
2810
  started();
1981
2811
  };
1982
- if (layout.init !== undefined)
1983
- this.invokeFrame(actor, layout.init, [actor.state, ...args.slice(supplied)], start, failed);
2812
+ if (layout.init != null)
2813
+ actor.active = this.invokeFrame(actor, layout.init, [actor.state, ...args.slice(supplied)], start, failed, actor.rcOwner, true);
1984
2814
  else
1985
2815
  start();
1986
2816
  return;
1987
2817
  }
2818
+ case "timer_schedule": {
2819
+ const [target, after, overdue, hasOverdue, payload] = args;
2820
+ const actor = this.actorFor(target);
2821
+ if (actor !== act.context.actor ||
2822
+ after?.kind !== "i64" ||
2823
+ overdue?.kind !== "i64" ||
2824
+ hasOverdue?.kind !== "bool" ||
2825
+ payload?.kind !== "record")
2826
+ throw new Error("timer schedule lost its checked owner or timing");
2827
+ const handler = actor.layout.handlers.find((handler) => handler.message_id === operation.message);
2828
+ if (!handler)
2829
+ throw new Error("timer schedule lost its handler");
2830
+ if (this.rootComplete || actor.closing) {
2831
+ this.running = false;
2832
+ const refused = {
2833
+ kind: "panic",
2834
+ actorSendFailed: true,
2835
+ message: "actor timer admission is closed",
2836
+ };
2837
+ this.closeValueAsync(payload, faultReport(refused), (fault) => {
2838
+ this.raiseFault(act, combineFaults(refused, fault), term.unwind);
2839
+ this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2840
+ }, actor, act.rcOwner);
2841
+ return;
2842
+ }
2843
+ let phase = "scheduled";
2844
+ let pending = payload;
2845
+ let overdueAt = null;
2846
+ let disarm = null;
2847
+ const forget = () => {
2848
+ disarm?.();
2849
+ disarm = null;
2850
+ actor.timers.delete(timer);
2851
+ };
2852
+ const miss = (discarded) => {
2853
+ if (phase !== "fired")
2854
+ return;
2855
+ phase = hasOverdue.value ? "overdue" : "cancelled";
2856
+ forget();
2857
+ if (discarded && !hasOverdue.value)
2858
+ this.trace.writeStderr("actor.timer_discarded: the one-shot handler did not start\n", null);
2859
+ if (!hasOverdue.value || !actor.alive)
2860
+ return;
2861
+ const fault = {
2862
+ kind: "panic",
2863
+ timerOverdue: true,
2864
+ message: "actor timer did not start before its overdue deadline",
2865
+ };
2866
+ if (actor.active) {
2867
+ actor.active.pendingCancellation ??= fault;
2868
+ actor.active.cancel?.(actor.active.pendingCancellation);
2869
+ }
2870
+ else
2871
+ this.crashActor(actor, fault, handler);
2872
+ };
2873
+ const timer = {
2874
+ cancel: () => {
2875
+ if (phase !== "overdue")
2876
+ phase = "cancelled";
2877
+ forget();
2878
+ if (pending) {
2879
+ actor.disposals.push(pending);
2880
+ pending = null;
2881
+ this.dispatchActor(actor);
2882
+ }
2883
+ },
2884
+ started: () => {
2885
+ if (phase === "fired" &&
2886
+ overdueAt !== null &&
2887
+ this.trace.virtualTimeMs >= overdueAt)
2888
+ miss(false);
2889
+ const allowed = phase !== "overdue";
2890
+ if (phase === "fired")
2891
+ phase = "started";
2892
+ forget();
2893
+ return allowed;
2894
+ },
2895
+ discarded: () => miss(true),
2896
+ };
2897
+ actor.timers.add(timer);
2898
+ disarm = this.scheduler.after(after.value, () => {
2899
+ disarm = null;
2900
+ if (phase !== "scheduled")
2901
+ return;
2902
+ phase = "fired";
2903
+ pending = null;
2904
+ if (hasOverdue.value) {
2905
+ overdueAt =
2906
+ this.trace.virtualTimeMs + Number(overdue.value) / 1_000_000;
2907
+ disarm = this.scheduler.after(overdue.value, () => miss(false));
2908
+ }
2909
+ const message = {
2910
+ timer,
2911
+ caller: { kind: "Timer" },
2912
+ handler,
2913
+ payload: payload.fields,
2914
+ reply: false,
2915
+ obligated: false,
2916
+ complete: (_value, _error, drained) => drained?.(null),
2917
+ };
2918
+ const status = this.admitActorMessage(actor, message);
2919
+ if (status === "full" || status === "closed") {
2920
+ actor.disposals.push(payload);
2921
+ timer.discarded();
2922
+ this.dispatchActor(actor);
2923
+ }
2924
+ });
2925
+ this.completeShim(act, term, {
2926
+ kind: "timer",
2927
+ resource: operation.resource,
2928
+ cancel: () => timer.cancel(),
2929
+ closed: false,
2930
+ });
2931
+ return;
2932
+ }
1988
2933
  case "stream_start": {
1989
2934
  const [target, payload] = args;
1990
2935
  const fields = fieldsOf(payload);
@@ -2001,7 +2946,7 @@ class ExecutorV1 {
2001
2946
  this.raiseFault(act, { kind: "panic", message: "stream producer is closed" }, term.unwind);
2002
2947
  }
2003
2948
  else {
2004
- if (actor.layout.mailbox_capacity !== undefined &&
2949
+ if (actor.layout.mailbox_capacity != null &&
2005
2950
  actor.mailbox.length >= actor.layout.mailbox_capacity) {
2006
2951
  actor.admission.push(admit);
2007
2952
  return;
@@ -2035,6 +2980,10 @@ class ExecutorV1 {
2035
2980
  task.actorError = error;
2036
2981
  this.settleTask(task, value ?? UNIT, closeFault ?? null);
2037
2982
  return true;
2983
+ }, undefined, (actor) => {
2984
+ task.actorTarget = actor;
2985
+ for (const changed of [...task.changed])
2986
+ changed();
2038
2987
  });
2039
2988
  task.close = (done) => cancelRequest((fault) => {
2040
2989
  this.settleTask(task, UNIT, fault);
@@ -2071,15 +3020,17 @@ class ExecutorV1 {
2071
3020
  this.variant(shapes.success, discarded ? "DeliveryDiscarded" : "DeliveryAccepted"),
2072
3021
  ]);
2073
3022
  const rejected = (reason) => this.variant(term.result_shape, "ResultErr", [
2074
- {
2075
- kind: "record",
2076
- typeId: this.aggregateName(shapes.failure),
2077
- shape: shapes.failure,
2078
- fields: [
2079
- this.variant(shapes.reason, sendErrorRole(reason)),
2080
- request,
2081
- ],
2082
- },
3023
+ this.variant(term.error_shape, "ActorErrorRejected", [
3024
+ {
3025
+ kind: "record",
3026
+ typeId: this.aggregateName(shapes.failure),
3027
+ shape: shapes.failure,
3028
+ fields: [
3029
+ this.variant(shapes.reason, sendErrorRole(reason)),
3030
+ request,
3031
+ ],
3032
+ },
3033
+ ]),
2083
3034
  ]);
2084
3035
  let parked = false;
2085
3036
  const finish = (value) => {
@@ -2092,87 +3043,38 @@ class ExecutorV1 {
2092
3043
  finish(rejected("Dead"));
2093
3044
  return;
2094
3045
  }
2095
- const full = actor.layout.mailbox_capacity !== undefined &&
2096
- actor.mailbox.length >= actor.layout.mailbox_capacity;
2097
- // Declared loss transfers cleanup to the receiver. Sender-selected
2098
- // loss still closes the sender's unadmitted request in its own frame.
2099
- const discardAtReceiver = (value, report) => {
2100
- actor.disposals.push(value);
2101
- this.dispatchActor(actor);
2102
- finish(report);
2103
- };
2104
- const discard = (value, report) => {
2105
- let settled = false;
2106
- let waiting = false;
2107
- this.closeValueAsync(value, this.pipeFault(act), (fault) => {
2108
- act.fault ??= fault;
2109
- settled = true;
2110
- if (waiting)
2111
- finish(report);
2112
- else
2113
- this.completeShim(act, term, report);
2114
- }, act.context.actor);
2115
- if (!settled) {
2116
- waiting = true;
2117
- parked = true;
2118
- this.running = false;
2119
- }
3046
+ const message = {
3047
+ caller,
3048
+ handler,
3049
+ payload: payload.fields,
3050
+ reply: false,
3051
+ obligated: false,
3052
+ complete: (_value, _error, drained) => drained?.(null),
2120
3053
  };
2121
- let declared = full ? actor.layout.overflow : null;
2122
- const coalesce = actor.layout.coalesce;
2123
- if (declared === "coalesce" && coalesce) {
2124
- const key = coalesce.keys.find((key) => key.message === handler.message_id);
2125
- const queued = key
2126
- ? actor.mailbox.findIndex((message) => !message.obligated &&
2127
- message.handler.message_id === handler.message_id &&
2128
- equals(message.payload[key.param], payload.fields[key.param]))
2129
- : -1;
2130
- if (queued >= 0) {
2131
- const superseded = actor.mailbox[queued];
2132
- actor.mailbox[queued] = {
2133
- ...superseded,
2134
- caller,
2135
- payload: payload.fields,
2136
- };
2137
- discardAtReceiver({ kind: "record", typeId: "", fields: superseded.payload }, accepted(false));
2138
- return;
2139
- }
2140
- declared = coalesce.fallback;
2141
- }
2142
- if (declared === "drop_new") {
2143
- discardAtReceiver(payload, accepted(true));
2144
- return;
2145
- }
2146
- // A queued completion call owes its caller a reply, so `drop_old`
2147
- // evicts only a one-way message and refuses when there is none.
2148
- const oldest = declared === "drop_old"
2149
- ? actor.mailbox.findIndex((message) => !message.obligated)
2150
- : -1;
2151
- if (oldest >= 0) {
2152
- const [evicted] = actor.mailbox.splice(oldest, 1);
2153
- actor.mailbox.push({
2154
- caller,
2155
- handler,
2156
- payload: payload.fields,
2157
- reply: false,
2158
- obligated: false,
2159
- complete: (_value, _error, drained) => drained?.(null),
2160
- });
2161
- this.dispatchActor(actor);
2162
- discardAtReceiver({ kind: "record", typeId: "", fields: evicted.payload }, accepted(false));
2163
- return;
2164
- }
2165
- if (full) {
3054
+ const status = this.admitActorMessage(actor, message);
3055
+ if (status === "full") {
2166
3056
  if (operation.policy === "drop_newest") {
2167
- discard(payload, accepted(true));
3057
+ let settled = false;
3058
+ let waiting = false;
3059
+ this.closeValueAsync(payload, this.pipeFault(act), (fault) => {
3060
+ act.fault ??= fault;
3061
+ settled = true;
3062
+ if (waiting)
3063
+ finish(accepted(true));
3064
+ else
3065
+ this.completeShim(act, term, accepted(true));
3066
+ }, act.context.actor, act.rcOwner);
3067
+ if (!settled) {
3068
+ waiting = true;
3069
+ parked = true;
3070
+ this.running = false;
3071
+ }
2168
3072
  return;
2169
3073
  }
2170
3074
  if (operation.policy === "reject") {
2171
3075
  finish(rejected("Full"));
2172
3076
  return;
2173
3077
  }
2174
- // The checker already resolved declaration defaults and explicit
2175
- // overrides; the declaration's own resolution happened above.
2176
3078
  if (operation.policy !== "wait")
2177
3079
  throw new Error("submission has no admitted policy");
2178
3080
  parked = true;
@@ -2180,20 +3082,10 @@ class ExecutorV1 {
2180
3082
  actor.admission.push(submit);
2181
3083
  return;
2182
3084
  }
2183
- actor.mailbox.push({
2184
- caller,
2185
- handler,
2186
- payload: payload.fields,
2187
- reply: false,
2188
- obligated: false,
2189
- complete: (_value, _error, drained) => drained?.(null),
2190
- });
2191
- this.trace.snapshot("actor.send", {
2192
- actor_id: actor.id,
2193
- handler: handler.name,
2194
- });
2195
- this.dispatchActor(actor);
2196
- finish(accepted(false));
3085
+ if (status === "closed")
3086
+ finish(rejected("Dead"));
3087
+ else
3088
+ finish(accepted(status === "discarded"));
2197
3089
  };
2198
3090
  submit();
2199
3091
  return;
@@ -2202,17 +3094,35 @@ class ExecutorV1 {
2202
3094
  const layout = this.pkg.supervisors?.find((layout) => layout.id === operation.supervisor);
2203
3095
  if (!layout)
2204
3096
  throw new Error("supervisor descriptor is missing");
3097
+ const isPool = layout.children.some((child) => child.is_pool);
3098
+ const count = isPool ? args[args.length - 1] : undefined;
3099
+ if (isPool && count?.kind !== "i64")
3100
+ throw new Error("pool construction has no count");
3101
+ const config = isPool ? args.slice(0, -1) : args;
3102
+ if (count?.kind === "i64" &&
3103
+ (count.value <= 0n || count.value > 2147483647n)) {
3104
+ const fault = {
3105
+ kind: "panic",
3106
+ message: "supervisor pool count must be between 1 and 2147483647",
3107
+ };
3108
+ this.running = false;
3109
+ this.closeValueAsync({ kind: "record", typeId: "", fields: config }, fault.message, (closeFault) => {
3110
+ this.raiseFault(act, combineFaults(fault, closeFault), term.unwind);
3111
+ this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
3112
+ }, act.context.actor, act.rcOwner);
3113
+ return;
3114
+ }
3115
+ const members = count?.kind === "i64" ? Number(count.value) : 1;
2205
3116
  const owner = {
2206
3117
  id: this.ids.supervisor(),
2207
3118
  layout,
2208
- config: args,
2209
- children: layout.children.flatMap((child) => Array.from({ length: child.pool_count ?? 1 }, () => null)),
2210
- specs: layout.children.flatMap((child) => Array.from({ length: child.pool_count ?? 1 }, () => child)),
2211
- offsets: layout.children.map((_child, index) => layout.children
2212
- .slice(0, index)
2213
- .reduce((sum, child) => sum + (child.pool_count ?? 1), 0)),
3119
+ config,
3120
+ children: layout.children.flatMap(() => Array.from({ length: members }, () => null)),
3121
+ specs: layout.children.flatMap((child) => Array.from({ length: members }, () => child)),
3122
+ offsets: layout.children.map((_child, index) => index),
2214
3123
  recovering: new Set(),
2215
- restartBusy: false,
3124
+ restartBusy: true,
3125
+ constructor: null,
2216
3126
  pendingRestarts: [],
2217
3127
  spent: new Set(),
2218
3128
  completed: false,
@@ -2227,20 +3137,39 @@ class ExecutorV1 {
2227
3137
  this.trace.allocateId("supervisor", owner.id, act.context.id);
2228
3138
  this.trace.snapshot("supervisor.spawn", { supervisor_id: owner.id });
2229
3139
  this.running = false;
2230
- let child = 0;
3140
+ const order = this.supervisorStartupOrder(owner);
3141
+ let fault = null;
3142
+ const fail = (cause) => {
3143
+ if (fault)
3144
+ return;
3145
+ fault = cause;
3146
+ owner.closed.push(() => {
3147
+ act.context.cancel = undefined;
3148
+ this.raiseFault(act, fault, term.unwind);
3149
+ this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
3150
+ });
3151
+ if (owner.constructor)
3152
+ owner.constructor.pendingCancellation ??= cause;
3153
+ this.stopSupervisor(owner);
3154
+ };
3155
+ act.context.cancel = (cause = {
3156
+ kind: "panic",
3157
+ message: "construction cancelled",
3158
+ cancelled: true,
3159
+ }) => fail(cause);
2231
3160
  const next = () => {
2232
- if (child < owner.children.length) {
2233
- this.startChild(owner, child++, next, (fault) => {
2234
- owner.closed.push(() => {
2235
- this.raiseFault(act, fault, term.unwind);
2236
- this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2237
- });
2238
- this.stopSupervisor(owner);
2239
- });
3161
+ const child = order.shift();
3162
+ if (child !== undefined) {
3163
+ this.startChild(owner, child, next, fail);
2240
3164
  }
2241
3165
  else {
3166
+ act.context.cancel = undefined;
3167
+ owner.restartBusy = false;
2242
3168
  this.completeShim(act, term, { kind: "supervisor", id: owner.id });
2243
3169
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
3170
+ const pending = owner.pendingRestarts.shift();
3171
+ if (pending)
3172
+ this.restartChild(owner, pending.child, pending.debts);
2244
3173
  }
2245
3174
  };
2246
3175
  next();
@@ -2269,7 +3198,7 @@ class ExecutorV1 {
2269
3198
  this.completeShim(act, term, {
2270
3199
  kind: "vector",
2271
3200
  elementType: "role",
2272
- items: Array.from({ length: spec.pool_count ?? 1 }, (_, index) => makeRole(offset + index)),
3201
+ items: Array.from({ length: owner.children.length }, (_, index) => makeRole(offset + index)),
2273
3202
  });
2274
3203
  return;
2275
3204
  }
@@ -2279,12 +3208,16 @@ class ExecutorV1 {
2279
3208
  case "await_restarted": {
2280
3209
  const target = args[0];
2281
3210
  const role = "id" in target ? this.roles.get(target.id) : undefined;
2282
- if (!role)
2283
- throw new Error("restarted requires a supervised role");
2284
- if (this.rolePending(role)) {
3211
+ if (!role) {
3212
+ if (target.kind !== "actor" && target.kind !== "supervisor")
3213
+ throw new Error("restarted requires an actor or supervisor handle");
3214
+ this.completeShim(act, term, UNIT);
3215
+ return;
3216
+ }
3217
+ if (this.roleRestartPending(role)) {
2285
3218
  this.running = false;
2286
3219
  const settled = () => {
2287
- if (this.rolePending(role)) {
3220
+ if (this.roleRestartPending(role)) {
2288
3221
  role.waiting.push(settled);
2289
3222
  return;
2290
3223
  }
@@ -2324,7 +3257,10 @@ class ExecutorV1 {
2324
3257
  if (target.kind === "actor") {
2325
3258
  const actor = this.actorFor(target);
2326
3259
  const complete = () => {
2327
- const fault = actor.crashing ?? actor.closingFault;
3260
+ // A crash was reported when it happened and already counts toward
3261
+ // the exit status; only a fault raised while the actor was being
3262
+ // stopped passes to the caller that waits for it.
3263
+ const fault = actor.closingFault;
2328
3264
  if (actor.closeFaultDebt !== undefined)
2329
3265
  this.faultDebts.delete(actor.closeFaultDebt);
2330
3266
  if (fault)
@@ -2366,9 +3302,21 @@ class ExecutorV1 {
2366
3302
  id: act.context.actor.id,
2367
3303
  });
2368
3304
  return;
2369
- default:
2370
- throw new Error(`actor operation ${operation.op} has no executor`);
3305
+ case "local_observation":
3306
+ throw new Error("actor observations are not admitted in the VM");
3307
+ default: {
3308
+ const exhaustive = operation;
3309
+ throw new Error("unhandled actor operation: " + String(exhaustive));
3310
+ }
3311
+ }
3312
+ }
3313
+ scopeDeadline(act, expected) {
3314
+ for (let frame = act; frame?.context === act.context; frame = frame.caller) {
3315
+ for (const group of frame.scopes.values())
3316
+ if (group.deadline && (!expected || group.deadline === expected))
3317
+ return group.deadline;
2371
3318
  }
3319
+ return undefined;
2372
3320
  }
2373
3321
  requestDeadline(act) {
2374
3322
  let deadline = act.context.requestDeadline;
@@ -2380,11 +3328,12 @@ class ExecutorV1 {
2380
3328
  }
2381
3329
  return deadline;
2382
3330
  }
2383
- ask(protocol, inputs, complete, shapes) {
3331
+ ask(protocol, inputs, complete, shapes, selectedTarget) {
2384
3332
  let settled = false;
2385
3333
  let admitted = false;
2386
3334
  let closing = false;
2387
3335
  const owner = this.current.context.actor;
3336
+ const rcOwner = this.current.rcOwner;
2388
3337
  const deadline = this.requestDeadline(this.current);
2389
3338
  let cancelTimer;
2390
3339
  let message;
@@ -2406,7 +3355,7 @@ class ExecutorV1 {
2406
3355
  this.crashActor(owner, fault);
2407
3356
  else if (fault)
2408
3357
  this.rootFault ??= fault;
2409
- }, admitted ? receiver : owner);
3358
+ }, admitted ? receiver : owner, admitted ? (receiver?.rcOwner ?? null) : rcOwner);
2410
3359
  }
2411
3360
  else
2412
3361
  drained?.(null);
@@ -2430,7 +3379,7 @@ class ExecutorV1 {
2430
3379
  closeFault = fault;
2431
3380
  for (const waiter of closeWaiters.splice(0))
2432
3381
  waiter(fault);
2433
- }, owner);
3382
+ }, owner, rcOwner);
2434
3383
  };
2435
3384
  const reject = (reason) => {
2436
3385
  if (settled || closing)
@@ -2463,7 +3412,10 @@ class ExecutorV1 {
2463
3412
  const handler = actor.layout.handlers.find((handler) => handler.message_id === protocol.message);
2464
3413
  if (!handler)
2465
3414
  throw new Error(`actor ${actor.id} has no message ${protocol.message}`);
2466
- const full = actor.layout.mailbox_capacity !== undefined &&
3415
+ selectedTarget?.(actor);
3416
+ if (settled || closing)
3417
+ return;
3418
+ const full = actor.layout.mailbox_capacity != null &&
2467
3419
  actor.mailbox.length >= actor.layout.mailbox_capacity;
2468
3420
  if (full) {
2469
3421
  if (protocol.policy === "reject") {
@@ -2552,6 +3504,114 @@ class ExecutorV1 {
2552
3504
  closeRequest(done);
2553
3505
  };
2554
3506
  }
3507
+ admitActorMessage(actor, message) {
3508
+ if (!actor.alive || actor.closing)
3509
+ return "closed";
3510
+ const full = actor.layout.mailbox_capacity != null &&
3511
+ actor.mailbox.length >= actor.layout.mailbox_capacity;
3512
+ let declared = full ? actor.layout.overflow : null;
3513
+ const discard = (lost) => {
3514
+ actor.disposals.push({
3515
+ kind: "record",
3516
+ typeId: "",
3517
+ fields: lost.payload,
3518
+ });
3519
+ lost.timer?.discarded();
3520
+ this.dispatchActor(actor);
3521
+ };
3522
+ const coalesce = actor.layout.coalesce;
3523
+ if (declared === "coalesce" && coalesce) {
3524
+ const key = coalesce.keys.find((key) => key.message === message.handler.message_id);
3525
+ const queued = key
3526
+ ? actor.mailbox.findIndex((queued) => !queued.obligated &&
3527
+ queued.handler.message_id === message.handler.message_id &&
3528
+ equals(queued.payload[key.param], message.payload[key.param]))
3529
+ : -1;
3530
+ if (queued >= 0) {
3531
+ const superseded = actor.mailbox[queued];
3532
+ actor.mailbox[queued] = message;
3533
+ discard(superseded);
3534
+ return "accepted";
3535
+ }
3536
+ declared = coalesce.fallback;
3537
+ }
3538
+ if (declared === "drop_new") {
3539
+ discard(message);
3540
+ return "discarded";
3541
+ }
3542
+ const oldest = declared === "drop_old"
3543
+ ? actor.mailbox.findIndex((queued) => !queued.obligated)
3544
+ : -1;
3545
+ if (oldest >= 0) {
3546
+ const [evicted] = actor.mailbox.splice(oldest, 1);
3547
+ actor.mailbox.push(message);
3548
+ discard(evicted);
3549
+ return "accepted";
3550
+ }
3551
+ if (full)
3552
+ return "full";
3553
+ actor.mailbox.push(message);
3554
+ this.trace.snapshot("actor.send", {
3555
+ actor_id: actor.id,
3556
+ handler: message.handler.name,
3557
+ });
3558
+ this.dispatchActor(actor);
3559
+ return "accepted";
3560
+ }
3561
+ completeRoot() {
3562
+ this.rootComplete = true;
3563
+ // Quiesce actor timers before draining accepted work; sleeps still wake.
3564
+ for (const actor of this.actors.values())
3565
+ this.cancelActorTimers(actor);
3566
+ }
3567
+ startPeriodicTimers(actor) {
3568
+ if (this.rootComplete || actor.closing)
3569
+ return;
3570
+ for (const handler of actor.layout.handlers) {
3571
+ if (handler.every_ns == null)
3572
+ continue;
3573
+ const interval = Math.floor(handler.every_ns / 1_000_000);
3574
+ let next = this.trace.virtualTimeMs + interval;
3575
+ let disarm = null;
3576
+ const timer = {
3577
+ cancel: () => {
3578
+ disarm?.();
3579
+ disarm = null;
3580
+ actor.timers.delete(timer);
3581
+ },
3582
+ };
3583
+ const fire = () => {
3584
+ disarm = null;
3585
+ if (!actor.timers.has(timer))
3586
+ return;
3587
+ this.admitActorMessage(actor, {
3588
+ caller: { kind: "Timer" },
3589
+ handler,
3590
+ payload: [],
3591
+ reply: false,
3592
+ obligated: false,
3593
+ complete: (_value, _error, drained) => drained?.(null),
3594
+ });
3595
+ if (!actor.timers.has(timer))
3596
+ return;
3597
+ next += interval;
3598
+ if (next <= this.trace.virtualTimeMs)
3599
+ next +=
3600
+ (Math.floor((this.trace.virtualTimeMs - next) / interval) + 1) *
3601
+ interval;
3602
+ arm();
3603
+ };
3604
+ const arm = () => {
3605
+ disarm = this.scheduler.after(BigInt(Math.round((next - this.trace.virtualTimeMs) * 1_000_000)), fire);
3606
+ };
3607
+ actor.timers.add(timer);
3608
+ arm();
3609
+ }
3610
+ }
3611
+ cancelActorTimers(actor) {
3612
+ for (const timer of [...actor.timers])
3613
+ timer.cancel();
3614
+ }
2555
3615
  dispatchActor(actor) {
2556
3616
  if (actor.busy || !actor.alive || actor.drainingMailbox)
2557
3617
  return;
@@ -2587,13 +3647,21 @@ class ExecutorV1 {
2587
3647
  this.dispatchActor(actor);
2588
3648
  return;
2589
3649
  }
2590
- const message = actor.mailbox.shift();
3650
+ const message = actor.mailbox[0];
2591
3651
  if (!message || !actor.alive) {
2592
3652
  actor.busy = false;
2593
3653
  if (actor.closing && actor.alive)
2594
3654
  this.dispatchActor(actor);
2595
3655
  return;
2596
3656
  }
3657
+ // An overdue claim can synchronously begin terminal cleanup. Keep the
3658
+ // payload in its mailbox until that claim succeeds.
3659
+ if (message.timer && !message.timer.started()) {
3660
+ actor.busy = false;
3661
+ this.dispatchActor(actor);
3662
+ return;
3663
+ }
3664
+ actor.mailbox.shift();
2597
3665
  for (const admit of actor.admission.splice(0))
2598
3666
  admit();
2599
3667
  if (message.requestDeadline !== undefined &&
@@ -2623,7 +3691,7 @@ class ExecutorV1 {
2623
3691
  const drained = (fault) => {
2624
3692
  actor.busy = false;
2625
3693
  if (fault)
2626
- this.crashActor(actor, fault);
3694
+ this.crashActor(actor, fault, message.handler);
2627
3695
  else
2628
3696
  this.dispatchActor(actor);
2629
3697
  };
@@ -2639,8 +3707,9 @@ class ExecutorV1 {
2639
3707
  else {
2640
3708
  this.crashActor(actor, {
2641
3709
  kind: "panic",
3710
+ handlerFailed: true,
2642
3711
  message: "unhandled declared handler failure",
2643
- });
3712
+ }, message.handler);
2644
3713
  return;
2645
3714
  }
2646
3715
  }
@@ -2667,7 +3736,7 @@ class ExecutorV1 {
2667
3736
  this.dispatchActor(actor);
2668
3737
  }
2669
3738
  else {
2670
- this.crashActor(actor, fault);
3739
+ this.crashActor(actor, fault, message.handler);
2671
3740
  message.complete(null, "Trapped");
2672
3741
  }
2673
3742
  },
@@ -2676,7 +3745,12 @@ class ExecutorV1 {
2676
3745
  this.runFrame(frame);
2677
3746
  });
2678
3747
  }
2679
- crashActor(actor, fault) {
3748
+ crashActor(actor, fault, handler) {
3749
+ this.reportFault(fault);
3750
+ if (handler) {
3751
+ const context = `${actor.layout.name}.${handler.name}`;
3752
+ this.trace.writeStderr(`hew: actor crash in ${context} (actor ${actor.identity}): ${faultInfo(fault)[1]}\n`, null);
3753
+ }
2680
3754
  const debt = ++this.nextFaultDebt;
2681
3755
  this.faultDebts.add(debt);
2682
3756
  if (actor.supervisor)
@@ -2684,6 +3758,7 @@ class ExecutorV1 {
2684
3758
  actor.crashing ??= fault;
2685
3759
  actor.alive = false;
2686
3760
  actor.busy = false;
3761
+ this.cancelActorTimers(actor);
2687
3762
  this.trace.snapshot("actor.crash", {
2688
3763
  actor_id: actor.id,
2689
3764
  message: this.faultText(fault),
@@ -2693,47 +3768,114 @@ class ExecutorV1 {
2693
3768
  actor.mailboxDrained.push(() => finished(action));
2694
3769
  return;
2695
3770
  }
2696
- this.closeValueAsync(actor.state, this.faultText(fault), () => this.drainMailbox(actor, this.faultText(fault), () => {
2697
- actor.completed = true;
2698
- for (const wake of actor.closed.splice(0))
2699
- wake();
2700
- for (const admit of actor.admission.splice(0))
2701
- admit();
2702
- if (actor.supervisor) {
2703
- const { owner, child } = actor.supervisor;
2704
- if (action === "Restart")
2705
- this.restartChild(owner, child, [debt]);
2706
- else if (action === "Escalate")
2707
- this.escalateSupervisor(owner, [debt]);
2708
- else {
2709
- owner.spent.add(child);
2710
- owner.recovering.delete(child);
2711
- this.wakeRoles();
2712
- }
2713
- }
2714
- }), actor);
3771
+ this.closeValueAsync(actor.state, this.faultText(fault), (stateFault) => {
3772
+ actor.closingFault = combineFaults(actor.closingFault, stateFault);
3773
+ fault = combineFaults(fault, stateFault);
3774
+ actor.crashing = fault;
3775
+ this.reportFault(fault);
3776
+ this.drainMailbox(actor, this.faultText(fault), (payloadFault) => {
3777
+ actor.closingFault = combineFaults(actor.closingFault, payloadFault);
3778
+ fault = combineFaults(fault, payloadFault);
3779
+ actor.crashing = fault;
3780
+ this.reportFault(fault);
3781
+ this.sweepRcOwner(actor.rcOwner, actor, (sweepFault) => {
3782
+ actor.closingFault = combineFaults(actor.closingFault, sweepFault);
3783
+ actor.crashing = combineFaults(actor.crashing, sweepFault);
3784
+ this.reportFault(actor.crashing);
3785
+ if (actor.closingFault) {
3786
+ this.reportFault(actor.closingFault);
3787
+ }
3788
+ actor.completed = true;
3789
+ for (const wake of actor.closed.splice(0))
3790
+ wake();
3791
+ for (const admit of actor.admission.splice(0))
3792
+ admit();
3793
+ if (actor.supervisor) {
3794
+ const { owner, child } = actor.supervisor;
3795
+ if (action === "Restart")
3796
+ this.restartChild(owner, child, [debt]);
3797
+ else if (action === "Escalate")
3798
+ this.escalateSupervisor(owner, [debt]);
3799
+ else {
3800
+ owner.spent.add(child);
3801
+ owner.recovering.delete(child);
3802
+ this.wakeRoles();
3803
+ }
3804
+ }
3805
+ });
3806
+ });
3807
+ }, actor);
2715
3808
  };
2716
- if (actor.layout.crash !== undefined) {
2717
- const [code, message] = faultInfo(fault);
2718
- const info = {
2719
- kind: "record",
2720
- typeId: this.aggregateName(actor.layout.crash_info),
2721
- shape: actor.layout.crash_info,
2722
- fields: [
2723
- { kind: "i64", value: BigInt(code) },
2724
- { kind: "string", value: String(message) },
2725
- ],
2726
- };
2727
- this.invokeFrame(actor, actor.layout.crash, [actor.state, info], (value) => {
2728
- const shape = this.pkg.variants[actor.layout.crash_action];
2729
- const action = value.kind === "enum" ? shape?.cases[value.tag]?.name : undefined;
2730
- if (!action)
2731
- throw new Error("crash hook returned no declared action");
2732
- finished(action);
2733
- }, () => finished("Kill"));
2734
- }
2735
- else
2736
- finished("Restart");
3809
+ const runHook = () => {
3810
+ if (actor.layout.crash != null) {
3811
+ const [code, message] = faultInfo(fault);
3812
+ const info = {
3813
+ kind: "record",
3814
+ typeId: this.aggregateName(actor.layout.crash_info),
3815
+ shape: actor.layout.crash_info,
3816
+ fields: [
3817
+ {
3818
+ kind: "enum",
3819
+ typeId: this.variantName(actor.layout.crash_kind),
3820
+ shape: actor.layout.crash_kind,
3821
+ tag: runtimeTag(this.pkg.variants[actor.layout.crash_kind], code === 214
3822
+ ? "CrashKindTimerOverdue"
3823
+ : code === 213
3824
+ ? "CrashKindHandlerFailed"
3825
+ : "CrashKindFault"),
3826
+ payload: [],
3827
+ },
3828
+ stringValue(String(message)),
3829
+ ],
3830
+ };
3831
+ let cancelTimer;
3832
+ const complete = (action) => {
3833
+ cancelTimer?.();
3834
+ actor.terminalHook = null;
3835
+ finished(action);
3836
+ };
3837
+ const hook = this.invokeFrame(actor, actor.layout.crash, [actor.state, info], (value) => {
3838
+ const shape = this.pkg.variants[actor.layout.crash_action];
3839
+ const action = value.kind === "enum" ? shape?.cases[value.tag]?.name : undefined;
3840
+ if (!action)
3841
+ throw new Error("crash hook returned no declared action");
3842
+ complete(action);
3843
+ }, (hookFault) => {
3844
+ actor.closingFault = combineFaults(actor.closingFault, hookFault);
3845
+ fault = combineFaults(fault, hookFault);
3846
+ actor.crashing = fault;
3847
+ this.reportFault(fault);
3848
+ complete("Restart");
3849
+ });
3850
+ actor.terminalHook = hook;
3851
+ const parent = actor.supervisor;
3852
+ const stopNs = parent
3853
+ ? this.childStopBudget(parent.owner, parent.child)
3854
+ : DEFAULT_ACTOR_STOP_NS;
3855
+ hook.requestDeadline = this.trace.virtualTimeMs + stopNs / 1_000_000;
3856
+ const expire = () => {
3857
+ hook.pendingCancellation ??= {
3858
+ kind: "panic",
3859
+ message: "crash hook deadline elapsed",
3860
+ deadline: true,
3861
+ };
3862
+ hook.cancel?.(hook.pendingCancellation);
3863
+ };
3864
+ if (stopNs <= 0)
3865
+ expire();
3866
+ else
3867
+ cancelTimer = this.scheduler.after(BigInt(stopNs), expire);
3868
+ }
3869
+ else
3870
+ finished("Restart");
3871
+ };
3872
+ this.drainMailbox(actor, this.faultText(fault), (cleanupFault) => {
3873
+ actor.closingFault = combineFaults(actor.closingFault, cleanupFault);
3874
+ fault = combineFaults(fault, cleanupFault);
3875
+ actor.crashing = fault;
3876
+ this.reportFault(fault);
3877
+ runHook();
3878
+ });
2737
3879
  }
2738
3880
  supervisorFor(value) {
2739
3881
  if (value.kind !== "supervisor")
@@ -2746,9 +3888,42 @@ class ExecutorV1 {
2746
3888
  throw new Error("supervisor does not exist");
2747
3889
  return owner;
2748
3890
  }
3891
+ supervisorStartupOrder(owner) {
3892
+ const declarations = owner.layout.startup_order ??
3893
+ owner.layout.children.map((_child, index) => index);
3894
+ return declarations.flatMap((declaration) => {
3895
+ const offset = owner.offsets[declaration];
3896
+ const end = owner.offsets[declaration + 1] ?? owner.children.length;
3897
+ return Array.from({ length: end - offset }, (_value, index) => offset + index);
3898
+ });
3899
+ }
2749
3900
  startChild(owner, child, done, failed) {
2750
3901
  const spec = owner.specs[child];
2751
- this.invokeFrame(null, spec.spawn, owner.config, (value) => {
3902
+ const arity = this.functionAt(spec.spawn).params.length;
3903
+ // Packages predating sibling wiring have config-only constructors. Their
3904
+ // actual callable signature, rather than a source name, selects that ABI.
3905
+ const args = arity === owner.config.length + 1
3906
+ ? [{ kind: "supervisor", id: owner.id }, ...owner.config]
3907
+ : arity === owner.config.length
3908
+ ? owner.config
3909
+ : null;
3910
+ if (!args)
3911
+ throw new Error("supervisor constructor signature does not match its config");
3912
+ owner.constructor = this.invokeFrame(null, spec.spawn, args, (value) => {
3913
+ const cancellation = owner.constructor?.pendingCancellation;
3914
+ owner.constructor = null;
3915
+ if (!owner.alive) {
3916
+ // The result is still owned here. Its declaration slot hands it to
3917
+ // canonical reverse-order teardown, including the configured budget.
3918
+ owner.children[child] = value;
3919
+ failed(cancellation ?? {
3920
+ kind: "panic",
3921
+ message: "supervisor construction cancelled",
3922
+ cancelled: true,
3923
+ });
3924
+ this.finishSupervisorStop(owner);
3925
+ return;
3926
+ }
2752
3927
  owner.children[child] = value;
2753
3928
  if (value.kind === "actor")
2754
3929
  this.actorFor(value).supervisor = { owner, child };
@@ -2761,7 +3936,15 @@ class ExecutorV1 {
2761
3936
  child,
2762
3937
  });
2763
3938
  done();
2764
- }, failed);
3939
+ }, (fault) => {
3940
+ const stopped = !owner.alive;
3941
+ owner.constructor = null;
3942
+ // The initial caller still needs one terminal notification if the
3943
+ // supervisor was stopped directly rather than through caller cancel.
3944
+ failed(fault);
3945
+ if (stopped)
3946
+ this.finishSupervisorStop(owner);
3947
+ });
2765
3948
  }
2766
3949
  roleOwner(role) {
2767
3950
  const parentRole = this.roles.get(role.ownerId);
@@ -2786,6 +3969,23 @@ class ExecutorV1 {
2786
3969
  return (!!owner?.alive &&
2787
3970
  (owner.recovering.has(role.child) || owner.children[role.child] === null));
2788
3971
  }
3972
+ roleRestartPending(role) {
3973
+ const parent = this.roles.get(role.ownerId);
3974
+ if (parent && this.roleRestartPending(parent))
3975
+ return true;
3976
+ if (this.rolePending(role))
3977
+ return true;
3978
+ const value = this.roleValue(role) ?? role.latest;
3979
+ if (value?.kind === "actor") {
3980
+ const actor = this.actorFor(value);
3981
+ return !actor.completed && (actor.closing || !actor.alive);
3982
+ }
3983
+ if (value?.kind === "supervisor") {
3984
+ const owner = this.supervisorFor(value);
3985
+ return !owner.completed && !owner.alive;
3986
+ }
3987
+ return false;
3988
+ }
2789
3989
  wakeRoles() {
2790
3990
  for (const role of this.roles.values()) {
2791
3991
  this.roleValue(role);
@@ -2839,7 +4039,7 @@ class ExecutorV1 {
2839
4039
  stop();
2840
4040
  });
2841
4041
  };
2842
- const starting = [...affected];
4042
+ const starting = this.supervisorStartupOrder(owner).filter((index) => affected.includes(index));
2843
4043
  const start = () => {
2844
4044
  if (!owner.alive) {
2845
4045
  owner.restartBusy = false;
@@ -2918,6 +4118,16 @@ class ExecutorV1 {
2918
4118
  else if (value.kind === "supervisor")
2919
4119
  this.stopSupervisor(this.supervisorFor(value), true);
2920
4120
  }
4121
+ childStopBudget(owner, child) {
4122
+ const spec = owner.specs[child];
4123
+ if (spec.stop_config != null) {
4124
+ const duration = owner.config[spec.stop_config];
4125
+ if (duration?.kind !== "i64" || duration.value < 0n)
4126
+ throw new Error("child stop budget is not a non-negative duration");
4127
+ return Number(duration.value);
4128
+ }
4129
+ return (spec.stop_ns ?? ("actor" in spec.role ? DEFAULT_ACTOR_STOP_NS : null));
4130
+ }
2921
4131
  stopSupervisor(owner, force = false) {
2922
4132
  if (force) {
2923
4133
  owner.terminating = true;
@@ -2928,6 +4138,18 @@ class ExecutorV1 {
2928
4138
  if (!owner.alive)
2929
4139
  return;
2930
4140
  owner.alive = false;
4141
+ if (owner.constructor) {
4142
+ owner.constructor.pendingCancellation ??= {
4143
+ kind: "panic",
4144
+ message: "supervisor construction cancelled",
4145
+ cancelled: true,
4146
+ };
4147
+ owner.constructor.cancel?.(owner.constructor.pendingCancellation);
4148
+ return;
4149
+ }
4150
+ this.finishSupervisorStop(owner);
4151
+ }
4152
+ finishSupervisorStop(owner) {
2931
4153
  let childIndex = owner.children.length - 1;
2932
4154
  const next = () => {
2933
4155
  owner.teardownCancelTimer?.();
@@ -2937,12 +4159,7 @@ class ExecutorV1 {
2937
4159
  const index = childIndex--;
2938
4160
  const child = owner.children[index];
2939
4161
  owner.teardownChild = child;
2940
- const spec = owner.specs[index];
2941
- const stopNs = spec.stop_ns === undefined
2942
- ? "actor" in spec.role
2943
- ? 5_000_000_000
2944
- : null
2945
- : spec.stop_ns;
4162
+ const stopNs = this.childStopBudget(owner, index);
2946
4163
  if (!owner.terminating && stopNs !== null && stopNs > 0) {
2947
4164
  owner.teardownCancelTimer = this.scheduler.after(BigInt(stopNs), () => this.terminateChild(child));
2948
4165
  }
@@ -2950,9 +4167,11 @@ class ExecutorV1 {
2950
4167
  return;
2951
4168
  }
2952
4169
  this.closeValueAsync({ kind: "record", typeId: "", fields: owner.config }, null, (fault) => {
2953
- if (fault)
2954
- this.faultDebts.add(++this.nextFaultDebt);
4170
+ const debt = fault ? ++this.nextFaultDebt : undefined;
4171
+ if (debt !== undefined)
4172
+ this.faultDebts.add(debt);
2955
4173
  owner.completed = true;
4174
+ this.settleSupervisedEnd(owner, fault ? "Crashed" : owner.terminating ? "Terminated" : "Stopped", debt === undefined ? [] : [debt]);
2956
4175
  for (const wake of owner.closed.splice(0))
2957
4176
  wake();
2958
4177
  this.wakeRoles();
@@ -2987,6 +4206,15 @@ class ExecutorV1 {
2987
4206
  if (actor.closing || actor.completed)
2988
4207
  return;
2989
4208
  actor.closing = true;
4209
+ this.cancelActorTimers(actor);
4210
+ if (actor.constructing && actor.active) {
4211
+ actor.active.pendingCancellation ??= {
4212
+ kind: "panic",
4213
+ message: "actor construction stopped",
4214
+ cancelled: true,
4215
+ };
4216
+ actor.active.cancel?.(actor.active.pendingCancellation);
4217
+ }
2990
4218
  // Admission closes immediately. The active turn and every accepted queued
2991
4219
  // turn finish before stop hooks and state release.
2992
4220
  this.dispatchActor(actor);
@@ -2998,8 +4226,17 @@ class ExecutorV1 {
2998
4226
  return;
2999
4227
  actor.closing = true;
3000
4228
  actor.terminating = true;
3001
- actor.active?.cancel?.();
3002
- actor.stopHook?.cancel?.();
4229
+ this.cancelActorTimers(actor);
4230
+ for (const context of [actor.active, actor.terminalHook]) {
4231
+ if (!context)
4232
+ continue;
4233
+ context.pendingCancellation ??= {
4234
+ kind: "panic",
4235
+ message: "actor terminated",
4236
+ cancelled: true,
4237
+ };
4238
+ context.cancel?.(context.pendingCancellation);
4239
+ }
3003
4240
  this.dispatchActor(actor);
3004
4241
  for (const admit of actor.admission.splice(0))
3005
4242
  admit();
@@ -3010,11 +4247,11 @@ class ExecutorV1 {
3010
4247
  const next = () => {
3011
4248
  const hook = actor.terminating ? undefined : hooks.shift();
3012
4249
  if (hook !== undefined) {
3013
- actor.stopHook = this.invokeFrame(actor, hook, [actor.state], () => {
3014
- actor.stopHook = null;
4250
+ actor.terminalHook = this.invokeFrame(actor, hook, [actor.state], () => {
4251
+ actor.terminalHook = null;
3015
4252
  next();
3016
4253
  }, (fault) => {
3017
- actor.stopHook = null;
4254
+ actor.terminalHook = null;
3018
4255
  if (!(actor.terminating && fault.kind === "panic" && fault.cancelled))
3019
4256
  actor.closingFault = combineFaults(actor.closingFault, fault);
3020
4257
  hooks.length = 0;
@@ -3024,31 +4261,37 @@ class ExecutorV1 {
3024
4261
  else {
3025
4262
  this.closeValueAsync(actor.state, this.faultText(actor.closingFault), (fault) => {
3026
4263
  actor.closingFault = combineFaults(actor.closingFault, fault);
3027
- // Stopped cleanup belongs to the completion barrier, independently
3028
- // of handler-crash debt and supervisor restart decisions.
3029
- actor.crashing = null;
3030
- if (actor.closingFault) {
3031
- actor.closeFaultDebt = ++this.nextFaultDebt;
3032
- this.faultDebts.add(actor.closeFaultDebt);
3033
- }
3034
- actor.completed = true;
3035
- actor.alive = false;
3036
- actor.busy = false;
3037
- this.trace.snapshot(actor.terminating ? "actor.terminate" : "actor.stop", { actor_id: actor.id });
3038
- this.settleSupervisedEnd(actor, actor.closingFault
3039
- ? "Crashed"
3040
- : actor.terminating
3041
- ? "Terminated"
3042
- : "Stopped");
3043
- for (const wake of actor.closed.splice(0))
3044
- wake();
4264
+ this.sweepRcOwner(actor.rcOwner, actor, (sweepFault) => {
4265
+ actor.closingFault = combineFaults(actor.closingFault, sweepFault);
4266
+ // Stopped cleanup belongs to the completion barrier, independently
4267
+ // of handler-crash debt and supervisor restart decisions.
4268
+ actor.crashing = null;
4269
+ if (actor.closingFault) {
4270
+ this.reportFault(actor.closingFault);
4271
+ actor.closeFaultDebt = ++this.nextFaultDebt;
4272
+ this.faultDebts.add(actor.closeFaultDebt);
4273
+ }
4274
+ actor.completed = true;
4275
+ actor.alive = false;
4276
+ actor.busy = false;
4277
+ this.trace.snapshot(actor.terminating ? "actor.terminate" : "actor.stop", { actor_id: actor.id });
4278
+ this.settleSupervisedEnd(actor, actor.closingFault
4279
+ ? "Crashed"
4280
+ : actor.terminating
4281
+ ? "Terminated"
4282
+ : "Stopped", actor.closeFaultDebt === undefined
4283
+ ? []
4284
+ : [actor.closeFaultDebt]);
4285
+ for (const wake of actor.closed.splice(0))
4286
+ wake();
4287
+ });
3045
4288
  }, actor);
3046
4289
  }
3047
4290
  };
3048
4291
  next();
3049
4292
  }
3050
- settleSupervisedEnd(actor, reason) {
3051
- const parent = actor.supervisor;
4293
+ settleSupervisedEnd(subject, reason, debts) {
4294
+ const parent = subject.supervisor;
3052
4295
  if (!parent ||
3053
4296
  !parent.owner.alive ||
3054
4297
  parent.owner.recovering.has(parent.child))
@@ -3057,7 +4300,7 @@ class ExecutorV1 {
3057
4300
  const policy = owner.specs[child].restart;
3058
4301
  if (policy === "permanent" ||
3059
4302
  (policy === "transient" && reason !== "Stopped")) {
3060
- this.restartChild(owner, child, actor.closeFaultDebt === undefined ? [] : [actor.closeFaultDebt]);
4303
+ this.restartChild(owner, child, debts);
3061
4304
  }
3062
4305
  else {
3063
4306
  owner.spent.add(child);
@@ -3065,6 +4308,16 @@ class ExecutorV1 {
3065
4308
  }
3066
4309
  }
3067
4310
  // ── exit and faults ──────────────────────────────────────────────────────
4311
+ reportFault(fault) {
4312
+ const diagnostics = [fault.primary ?? fault, ...(fault.secondary ?? [])];
4313
+ diagnostics.forEach((diagnostic, index) => {
4314
+ const origin = diagnostic.primary ?? diagnostic;
4315
+ if (origin.reported)
4316
+ return;
4317
+ this.trace.writeStderr(faultDiagnostic(origin, index !== 0), null);
4318
+ origin.reported = true;
4319
+ });
4320
+ }
3068
4321
  haltWithFault(fault) {
3069
4322
  const isPanic = fault.kind === "panic";
3070
4323
  const status = isPanic ? "panic" : "trap";
@@ -3074,7 +4327,7 @@ class ExecutorV1 {
3074
4327
  : (fault.message ?? trapMessage(fault.trap));
3075
4328
  const [code, kind] = faultInfo(fault);
3076
4329
  // Native writes the failure line to stderr before it exits; so does this.
3077
- this.trace.writeStderr(faultReport(fault), null);
4330
+ this.reportFault(fault);
3078
4331
  // A failing run reports no exit code: native exits 1 for every fault and
3079
4332
  // names the kind in its message, so the kind is what travels, in
3080
4333
  // `runtime_failures`. The page turns it into an exit code of its own.
@@ -3114,6 +4367,12 @@ function combineFaults(primary, secondary) {
3114
4367
  }
3115
4368
  function faultInfo(fault) {
3116
4369
  if (fault.kind === "panic") {
4370
+ if (fault.handlerFailed)
4371
+ return [213, "ActorUnhandledFailure"];
4372
+ if (fault.timerOverdue)
4373
+ return [214, "ActorTimerOverdue"];
4374
+ if (fault.actorSendFailed)
4375
+ return [206, "ActorSendFailed"];
3117
4376
  if (fault.cancelled)
3118
4377
  return [-1, "Cancelled"];
3119
4378
  if (fault.deadline)
@@ -3128,11 +4387,18 @@ function faultInfo(fault) {
3128
4387
  actor_refused: [-4, "Refused"],
3129
4388
  }[fault.trap];
3130
4389
  }
3131
- function faultReport(fault, secondary = false) {
4390
+ function faultDiagnostic(fault, secondary = false) {
3132
4391
  const [code, reason] = faultInfo(fault);
3133
4392
  const label = secondary ? "secondary failure" : "failure";
3134
- const message = fault.message === undefined ? "" : `: ${fault.message}`;
3135
- return (`hew: ${label}: ${reason} (${code})${message}\n` +
4393
+ const message = fault.message === undefined ||
4394
+ (fault.kind === "panic" &&
4395
+ (fault.cancelled || fault.deadline || fault.timerOverdue))
4396
+ ? ""
4397
+ : `: ${fault.message}`;
4398
+ return `hew: ${label}: ${reason} (${code})${message}\n`;
4399
+ }
4400
+ function faultReport(fault, secondary = false) {
4401
+ return (faultDiagnostic(fault, secondary) +
3136
4402
  (fault.secondary ?? []).map((next) => faultReport(next, true)).join(""));
3137
4403
  }
3138
4404
  // ── refs ────────────────────────────────────────────────────────────────────
@@ -3207,6 +4473,8 @@ function actorErrorRole(reason) {
3207
4473
  return "ActorErrorRoutingFailed";
3208
4474
  case "EncodeFailed":
3209
4475
  return "ActorErrorEncodeFailed";
4476
+ case "MessageTooLarge":
4477
+ return "ActorErrorMessageTooLarge";
3210
4478
  case "ConnectionDropped":
3211
4479
  return "ActorErrorConnectionDropped";
3212
4480
  case "Partition":
@@ -3217,6 +4485,8 @@ function actorErrorRole(reason) {
3217
4485
  }
3218
4486
  function sendErrorRole(reason) {
3219
4487
  switch (reason) {
4488
+ case "MessageTooLarge":
4489
+ return "SendErrorMessageTooLarge";
3220
4490
  case "Full":
3221
4491
  return "SendErrorFull";
3222
4492
  case "Closed":
@@ -3286,7 +4556,9 @@ function toComparable(value) {
3286
4556
  case "vector":
3287
4557
  return value.items.map(toComparable);
3288
4558
  case "rc":
3289
- return toComparable(value.cell.value);
4559
+ return "Rc";
4560
+ case "weak":
4561
+ return "Weak";
3290
4562
  case "map":
3291
4563
  return [...value.entries.values()].map((entry) => [
3292
4564
  toComparable(entry.key),
@@ -3459,7 +4731,7 @@ function applyBinary(op, lhs, rhs, ty) {
3459
4731
  };
3460
4732
  }
3461
4733
  if (lhs.kind === "string") {
3462
- return { kind: "string", value: lhs.value + asString(rhs) };
4734
+ return stringValue(lhs.value + asString(rhs));
3463
4735
  }
3464
4736
  return {
3465
4737
  kind: "i64",
@@ -3525,7 +4797,7 @@ function applyCast(value, to) {
3525
4797
  return { kind: "bool", value: truth(value) };
3526
4798
  case "char":
3527
4799
  case "string":
3528
- return { kind: "string", value: asString(value) };
4800
+ return stringValue(asString(value));
3529
4801
  default:
3530
4802
  break;
3531
4803
  }