@hew-lang/sandbox-vm 0.6.1 → 0.6.2

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.
@@ -6,8 +6,10 @@
6
6
  /// follows the `unwind` edge the package names, and takes the failure edge a
7
7
  /// `checked.binary` names rather than trapping on its own.
8
8
  import { executeCodec } from "./codecs.js";
9
+ import { actorIdentical } from "./actor-identity.js";
10
+ import { ActorWaits } from "./actor-waits.js";
9
11
  import { TraceBuilder, runtimeFailure } from "../trace.js";
10
- import { UNIT, cloneValue, toJsonValue } from "../values.js";
12
+ import { UNIT, cloneValue, stringValue, toJsonValue, } from "../values.js";
11
13
  import { runtimeTag } from "./package.js";
12
14
  import { StructuralRenderer } from "./structural.js";
13
15
  import { selectedValue, collectionOperation, sharedOperation, } from "./value-operations.js";
@@ -15,9 +17,10 @@ import { Pipes } from "./pipes.js";
15
17
  import { FrameScheduler } from "./scheduler.js";
16
18
  import { DeterministicIds } from "../../scheduler/ids.js";
17
19
  import { Mt19937 } from "./mt19937.js";
18
- import { ShimFault, resolveExternShim, resolveRuntimeShim, } from "./shims.js";
20
+ import { ShimFault, decodeUtf8Checked, resolveExternShim, resolveRuntimeShim, } from "./shims.js";
19
21
  import { admitPackage } from "./validate.js";
20
22
  const DEFAULT_STEP_BUDGET = 1_000_000;
23
+ const DEFAULT_ACTOR_STOP_NS = 5_000_000_000;
21
24
  /// Ends the run. The status is already recorded on the trace.
22
25
  class Halt extends Error {
23
26
  status;
@@ -92,10 +95,13 @@ class ExecutorV1 {
92
95
  scheduler;
93
96
  ids;
94
97
  actors = new Map();
98
+ actorWaits = new ActorWaits();
95
99
  supervisors = new Map();
96
100
  roles = new Map();
97
101
  pipes = new Pipes();
98
102
  generators = new Map();
103
+ generatorStreams = new Map();
104
+ senderEndpoints = new Map();
99
105
  tasks = new Map();
100
106
  completedTasks = 0;
101
107
  actorIdentity = 0n;
@@ -114,7 +120,35 @@ class ExecutorV1 {
114
120
  this.scheduler = new FrameScheduler(trace, policy);
115
121
  this.ids = new DeterministicIds(trace.replay.seed);
116
122
  this.host = {
123
+ argv: [...(options.argv ?? ["main.hew"])],
117
124
  pipes: this.pipes,
125
+ generatorStream: (value) => {
126
+ const id = this.ids.channel();
127
+ this.generatorStreams.set(id, {
128
+ generator: this.generatorFor(value),
129
+ closed: false,
130
+ pending: null,
131
+ fault: null,
132
+ receiving: false,
133
+ });
134
+ return { kind: "stream", channelId: id };
135
+ },
136
+ encoderSink: (sink, encoder) => this.senderEndpoint({
137
+ sink,
138
+ encoder,
139
+ references: 0,
140
+ endpoints: 0,
141
+ busy: false,
142
+ waiting: new Set(),
143
+ }, !this.sinkClosed(sink)),
144
+ cloneSink: (value) => {
145
+ const endpoint = value.kind === "sink"
146
+ ? this.senderEndpoints.get(value.channelId)
147
+ : undefined;
148
+ return endpoint
149
+ ? this.senderEndpoint(endpoint.owner, endpoint.open)
150
+ : this.pipes.cloneSink(value);
151
+ },
118
152
  newPipe: (capacity) => this.newPipe(capacity),
119
153
  closePipe: (value) => this.pipes.close(value, this.pipeFault(this.current)),
120
154
  releaseValue: (value) => this.closeValue(value, this.pipeFault(this.current)),
@@ -135,7 +169,7 @@ class ExecutorV1 {
135
169
  id: "actor:root",
136
170
  actor: null,
137
171
  returned: (value) => {
138
- this.rootComplete = true;
172
+ this.completeRoot();
139
173
  this.trace.exitCode =
140
174
  pkg.entry?.exit === "status" && value.kind === "i64"
141
175
  ? Number(value.value)
@@ -143,7 +177,7 @@ class ExecutorV1 {
143
177
  },
144
178
  failed: (fault) => {
145
179
  this.rootFault = fault;
146
- this.rootComplete = true;
180
+ this.completeRoot();
147
181
  },
148
182
  });
149
183
  }
@@ -173,6 +207,10 @@ class ExecutorV1 {
173
207
  this.running = true;
174
208
  while (this.running) {
175
209
  const act = this.current;
210
+ act.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
211
+ if (this.scopeDeadline(act, fault) !== fault)
212
+ act.context.pendingCancellation ??= fault;
213
+ };
176
214
  while (act.opIndex < act.block.ops.length) {
177
215
  const op = act.block.ops[act.opIndex++];
178
216
  if (op.op !== "coverage.hit")
@@ -203,9 +241,19 @@ class ExecutorV1 {
203
241
  }
204
242
  // ── frames ───────────────────────────────────────────────────────────────
205
243
  activate(fn, args, caller, result, normal, unwind, context) {
206
- if ((this.pkg.resources ?? []).some((resource) => resource.close === fn.id) &&
207
- args[0])
244
+ const mandatoryClose = (this.pkg.resources ?? []).some((resource) => resource.close === fn.id);
245
+ const parentContext = caller?.context ?? context;
246
+ parentContext.pollBudget ??= { remaining: 0 };
247
+ if (mandatoryClose && args[0])
208
248
  this.closingResources.add(args[0]);
249
+ const executionContext = mandatoryClose
250
+ ? {
251
+ ...parentContext,
252
+ cancel: undefined,
253
+ pendingCancellation: undefined,
254
+ requestDeadline: undefined,
255
+ }
256
+ : parentContext;
209
257
  const env = new Map();
210
258
  for (const [at, param] of fn.params.entries()) {
211
259
  env.set(param.value, ownedRef(args[at] ?? UNIT));
@@ -231,7 +279,13 @@ class ExecutorV1 {
231
279
  arm.fields.forEach(record);
232
280
  }
233
281
  return {
234
- context: caller?.context ?? context,
282
+ rcOwner: fn.actor_construction
283
+ ? { blocks: new Set(), closed: false }
284
+ : (caller?.rcOwner ??
285
+ executionContext.rcOwner ??
286
+ executionContext.actor?.rcOwner ??
287
+ null),
288
+ context: executionContext,
235
289
  fn,
236
290
  block: this.blockAt(fn, fn.entry),
237
291
  opIndex: 0,
@@ -373,7 +427,7 @@ class ExecutorV1 {
373
427
  this.define(act, op.dst, { kind: "f64", value: floatConst(op) });
374
428
  return;
375
429
  case "const.char":
376
- this.define(act, op.dst, { kind: "string", value: op.value });
430
+ this.define(act, op.dst, stringValue(op.value));
377
431
  return;
378
432
  case "const.unit":
379
433
  this.define(act, op.dst, UNIT);
@@ -382,10 +436,7 @@ class ExecutorV1 {
382
436
  this.define(act, op.dst, { kind: "i64", value: BigInt(op.nanos) });
383
437
  return;
384
438
  case "const.str":
385
- this.define(act, op.dst, {
386
- kind: "string",
387
- value: this.pkg.strings[op.str] ?? "",
388
- });
439
+ this.define(act, op.dst, stringValue(this.pkg.strings[op.str] ?? ""));
389
440
  return;
390
441
  case "const.bytes":
391
442
  this.define(act, op.dst, bytesValue(this.pkg.bytes[op.bytes] ?? []));
@@ -652,8 +703,9 @@ class ExecutorV1 {
652
703
  const context = {
653
704
  id,
654
705
  actor: act.context.actor,
706
+ rcOwner: act.rcOwner,
655
707
  requestDeadline: this.requestDeadline(act),
656
- returned: (value) => this.closeValueAsync(value, null, (fault) => this.finishGenerator(generator, fault)),
708
+ returned: (value) => this.closeValueAsync(value, null, (fault) => this.finishGenerator(generator, fault), act.context.actor, act.rcOwner),
657
709
  failed: (fault) => this.finishGenerator(generator, fault),
658
710
  };
659
711
  const generator = {
@@ -748,10 +800,10 @@ class ExecutorV1 {
748
800
  changed();
749
801
  };
750
802
  if (failure && winner)
751
- this.closeValueAsync(value, this.faultText(failure), finish, act.context.actor);
803
+ this.closeValueAsync(value, this.faultText(failure), finish, act.context.actor, act.rcOwner);
752
804
  else
753
805
  finish();
754
- }, act.context.actor);
806
+ }, act.context.actor, act.rcOwner);
755
807
  };
756
808
  task.cancel = () => {
757
809
  if (task.done || task.finishing)
@@ -770,12 +822,44 @@ class ExecutorV1 {
770
822
  ready();
771
823
  return;
772
824
  }
773
- case "task.spawn": {
825
+ case "task.spawn":
826
+ case "task.invoke": {
774
827
  const group = act.scopes.get(op.scope);
775
828
  if (!group)
776
829
  throw new Error(`task scope ${op.scope} is missing`);
777
- const callable = this.read(act, op.callable);
778
- this.invalidate(act, op.callable);
830
+ const consuming = op.op === "task.spawn" ||
831
+ ("receiver" in op.callee && op.callee.receiver.decision === "move");
832
+ const dynamic = op.op === "task.invoke" && "dyn" in op.callee ? op.callee : null;
833
+ const dynamicReceiver = dynamic
834
+ ? this.boundary(act, dynamic.receiver)
835
+ : null;
836
+ if (dynamicReceiver && dynamicReceiver.kind !== "dyn")
837
+ throw new TypeError("prepared dynamic receiver is not a trait object");
838
+ const dynamicEntry = dynamicReceiver?.kind === "dyn" && dynamic
839
+ ? this.dynamicEntry(dynamicReceiver, dynamic.dyn)
840
+ : null;
841
+ if (dynamic && dynamicEntry?.method !== dynamic.method)
842
+ throw new Error("prepared dynamic slot differs from its checked method");
843
+ const callable = op.op === "task.spawn"
844
+ ? this.read(act, op.callable)
845
+ : "direct" in op.callee
846
+ ? { kind: "function", id: String(op.callee.direct) }
847
+ : "mutable" in op.callee
848
+ ? { kind: "function", id: String(op.callee.mutable) }
849
+ : "dyn" in op.callee
850
+ ? {
851
+ kind: "function",
852
+ id: String(dynamicEntry.callee),
853
+ }
854
+ : this.boundary(act, op.callee.receiver);
855
+ if (op.op === "task.spawn")
856
+ this.invalidate(act, op.callable);
857
+ const args = op.op === "task.invoke"
858
+ ? op.args.map((arg) => this.boundary(act, arg))
859
+ : [];
860
+ const specialization = op.op === "task.invoke" && "specialization" in op.callee
861
+ ? op.callee.specialization
862
+ : null;
779
863
  const task = this.newTask();
780
864
  group.tasks.push(task);
781
865
  const settle = (value, fault) => {
@@ -786,9 +870,7 @@ class ExecutorV1 {
786
870
  !(fault.kind === "panic" && (fault.cancelled || fault.deadline)) &&
787
871
  act.context.actor)
788
872
  act.context.actor.crashing ??= fault;
789
- this.closeValueAsync(callable, act.context.actor
790
- ? this.faultText(act.context.actor.crashing)
791
- : null, (closeFault) => {
873
+ const complete = (closeFault) => {
792
874
  if (!fault || (fault.kind === "panic" && fault.cancelled))
793
875
  fault = closeFault ?? fault;
794
876
  this.settleTask(task, value, fault);
@@ -803,33 +885,92 @@ class ExecutorV1 {
803
885
  }
804
886
  for (const changed of [...group.changed])
805
887
  changed();
806
- });
888
+ };
889
+ if (!consuming) {
890
+ complete(null);
891
+ return;
892
+ }
893
+ this.closeValueAsync(callable, act.context.actor
894
+ ? this.faultText(act.context.actor.crashing)
895
+ : null, complete, act.context.actor, act.rcOwner);
896
+ };
897
+ const mutable = op.op === "task.invoke" && "mutable" in op.callee ? op.callee : null;
898
+ const receiverRef = mutable
899
+ ? this.refOf(act, mutable.receiver.value)
900
+ : null;
901
+ if (dynamicReceiver?.kind === "dyn" && dynamicEntry && dynamic)
902
+ args.unshift(this.erasedReceiver(act, dynamic.receiver, dynamicReceiver.value, dynamicEntry.receiver));
903
+ if (receiverRef)
904
+ args.unshift(mutable.staged
905
+ ? cloneValue(readRef(receiverRef))
906
+ : readRef(receiverRef));
907
+ const handback = (value, fault, result) => {
908
+ if (!receiverRef || !mutable || value === null)
909
+ throw new Error("prepared mutable method omitted its receiver handback");
910
+ if (mutable.staged && fault) {
911
+ this.closeValueAsync(value, this.faultText(fault), (closeFault) => settle(UNIT, combineFaults(fault, closeFault)), act.context.actor, act.rcOwner);
912
+ }
913
+ else {
914
+ const previous = readRef(receiverRef);
915
+ writeRef(receiverRef, value);
916
+ if (mutable.staged)
917
+ this.closeValueAsync(previous, null, (closeFault) => {
918
+ if (closeFault)
919
+ this.closeValueAsync(result, this.faultText(closeFault), (resultFault) => settle(UNIT, combineFaults(closeFault, resultFault)), act.context.actor, act.rcOwner);
920
+ else
921
+ settle(result, null);
922
+ }, act.context.actor, act.rcOwner);
923
+ else
924
+ settle(result, fault);
925
+ }
807
926
  };
808
927
  const context = {
809
928
  id: task.id,
810
929
  actor: act.context.actor,
930
+ rcOwner: act.rcOwner,
811
931
  requestDeadline: this.requestDeadline(act),
812
- returned: (value) => settle(value, null),
813
- failed: (fault) => settle(UNIT, fault),
932
+ returned: (value) => {
933
+ if (dynamicEntry?.receiver_handback &&
934
+ dynamicReceiver?.kind === "dyn") {
935
+ if (value.kind !== "record" || value.fields.length !== 2)
936
+ throw new Error("prepared dynamic method omitted its dual result");
937
+ dynamicReceiver.value = value.fields[1];
938
+ settle(value.fields[0], null);
939
+ }
940
+ else if (!mutable)
941
+ settle(value, null);
942
+ else if (value.kind === "record" && value.fields.length === 2)
943
+ handback(value.fields[1], null, value.fields[0]);
944
+ else
945
+ throw new Error("prepared mutable method returned no dual result");
946
+ },
947
+ failed: (fault, value) => {
948
+ if (dynamicEntry?.receiver_handback &&
949
+ dynamicReceiver?.kind === "dyn") {
950
+ if (!value)
951
+ throw new Error("prepared dynamic method omitted its fault handback");
952
+ dynamicReceiver.value = value;
953
+ }
954
+ if (mutable)
955
+ handback(value ?? null, fault, UNIT);
956
+ else
957
+ settle(UNIT, fault);
958
+ },
814
959
  };
815
- const frame = this.activate(this.functionAt(calleeFunction(callable)), callable.kind === "closure" ? [callable.environment] : [], null, null, undefined, null, context);
816
- let started = false;
960
+ const frame = this.activate(this.functionAt(calleeFunction(callable, specialization)), callable.kind === "closure" ? [callable.environment, ...args] : args, null, null, undefined, null, context);
817
961
  task.cancel = () => {
818
962
  if (task.done || task.finishing)
819
963
  return;
820
- if (started)
821
- context.cancel?.();
822
- else
823
- settle(UNIT, {
824
- kind: "panic",
825
- message: "task cancelled",
826
- cancelled: true,
827
- });
964
+ context.pendingCancellation ??= {
965
+ kind: "panic",
966
+ message: "task cancelled",
967
+ cancelled: true,
968
+ };
969
+ context.cancel?.(context.pendingCancellation);
828
970
  };
829
971
  this.scheduler.enqueue(task.id, () => {
830
972
  if (task.done || task.finishing)
831
973
  return;
832
- started = true;
833
974
  this.runFrame(frame);
834
975
  });
835
976
  this.trace.allocateId("task", task.id, act.context.id);
@@ -906,14 +1047,7 @@ class ExecutorV1 {
906
1047
  if (receiver.kind !== "dyn") {
907
1048
  throw new TypeError(`dyn.call expected a trait object, got ${receiver.kind}`);
908
1049
  }
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
- }
1050
+ const entry = this.dynamicEntry(receiver, term.slot);
917
1051
  // The wrapped value is the method's receiver, ahead of the call's own
918
1052
  // arguments, passed under the decision the slot records: the erasure
919
1053
  // the concrete type was made under, not something read off the method.
@@ -993,6 +1127,9 @@ class ExecutorV1 {
993
1127
  [
994
1128
  "RcNew",
995
1129
  "RcClone",
1130
+ "RcDowngrade",
1131
+ "WeakCloneRc",
1132
+ "WeakUpgradeRc",
996
1133
  "RcGet",
997
1134
  "RcIsUnique",
998
1135
  "RcSet",
@@ -1000,7 +1137,7 @@ class ExecutorV1 {
1000
1137
  "RcWeakCount",
1001
1138
  ].includes(entry.family)) {
1002
1139
  const args = term.args.map((operand) => this.boundary(act, operand));
1003
- this.runValueProgram(act, term, args, sharedOperation(entry.family, args));
1140
+ this.runValueProgram(act, term, args, sharedOperation(entry.family, args, act.rcOwner, term.shared_payload_type ?? "", (value) => this.variant(term.result_shape, value ? "OptionSome" : "OptionNone", value ? [value] : [])));
1004
1141
  return;
1005
1142
  }
1006
1143
  if (entry?.family === "SupervisorPool") {
@@ -1016,13 +1153,40 @@ class ExecutorV1 {
1016
1153
  else
1017
1154
  this.completeShim(act, term, UNIT);
1018
1155
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1019
- });
1156
+ }, act.context.actor, act.rcOwner);
1020
1157
  return;
1021
1158
  }
1022
1159
  if (entry?.family === "StructuralFormat") {
1023
1160
  this.structuralFormat(act, term);
1024
1161
  return;
1025
1162
  }
1163
+ if (entry?.family === "BytesDecodeUtf8") {
1164
+ const value = decodeUtf8Checked(this.boundary(act, term.args[0]));
1165
+ let result;
1166
+ if ("value" in value)
1167
+ result = this.variant(term.result_shape, "ResultOk", [
1168
+ stringValue(value.value),
1169
+ ]);
1170
+ else {
1171
+ const shapes = term.variant_result_shapes;
1172
+ const error = this.pkg.aggregates[shapes.error];
1173
+ result = this.variant(term.result_shape, "ResultErr", [
1174
+ {
1175
+ kind: "record",
1176
+ typeId: error.name,
1177
+ shape: error.id,
1178
+ fields: [
1179
+ { kind: "i64", value: BigInt(value.validUpTo) },
1180
+ this.variant(shapes.error_len, value.errorLen === null ? "OptionNone" : "OptionSome", value.errorLen === null
1181
+ ? []
1182
+ : [{ kind: "i64", value: BigInt(value.errorLen) }]),
1183
+ ],
1184
+ },
1185
+ ]);
1186
+ }
1187
+ this.completeShim(act, term, result);
1188
+ return;
1189
+ }
1026
1190
  const shim = entry ? resolveRuntimeShim(entry) : undefined;
1027
1191
  if (!entry || !shim) {
1028
1192
  throw new Error(`runtime family ${term.family} resolved no shim after admission`);
@@ -1081,6 +1245,7 @@ class ExecutorV1 {
1081
1245
  this.takeEdge(act, act.fault ? term.fault : term.normal);
1082
1246
  return;
1083
1247
  case "enter_defer":
1248
+ act.context.pollSuppression = (act.context.pollSuppression ?? 0) + 1;
1084
1249
  // The deferred body runs on its own fault state: a body reached while
1085
1250
  // unwinding must not read the fault it is cleaning up after.
1086
1251
  act.parked.push(act.fault);
@@ -1088,6 +1253,7 @@ class ExecutorV1 {
1088
1253
  this.takeEdge(act, term.body);
1089
1254
  return;
1090
1255
  case "finish_defer": {
1256
+ act.context.pollSuppression = (act.context.pollSuppression ?? 0) - 1;
1091
1257
  // Combine the parked fault with whatever the body left, keeping the
1092
1258
  // first: a join must never forget the fault the boundary carried in.
1093
1259
  const parked = act.parked.pop();
@@ -1117,12 +1283,7 @@ class ExecutorV1 {
1117
1283
  typeId: shape.name,
1118
1284
  shape: shape.id,
1119
1285
  tag: deadline ? term.deadline_variant : term.fault_variant,
1120
- payload: [
1121
- {
1122
- kind: "string",
1123
- value: faultReport(fault),
1124
- },
1125
- ],
1286
+ payload: [stringValue(faultReport(fault))],
1126
1287
  };
1127
1288
  act.fault = null;
1128
1289
  if (act.context.actor)
@@ -1217,7 +1378,7 @@ class ExecutorV1 {
1217
1378
  this.keepInputs(act, term, args);
1218
1379
  this.raiseFault(act, fault, term.unwind);
1219
1380
  resume();
1220
- });
1381
+ }, act.context.actor, act.rcOwner);
1221
1382
  };
1222
1383
  const next = (value = UNIT) => {
1223
1384
  let step;
@@ -1235,6 +1396,9 @@ class ExecutorV1 {
1235
1396
  this.completeShim(act, term, step.value);
1236
1397
  resume();
1237
1398
  }
1399
+ else if (step.value.kind === "fault") {
1400
+ fail({ kind: "panic", message: step.value.message });
1401
+ }
1238
1402
  else if (step.value.kind === "release") {
1239
1403
  const request = step.value;
1240
1404
  this.closeValueAsync(request.value, this.pipeFault(act), (fault) => {
@@ -1248,11 +1412,11 @@ class ExecutorV1 {
1248
1412
  act.fault ??= fault;
1249
1413
  }
1250
1414
  next();
1251
- });
1415
+ }, act.context.actor, act.rcOwner);
1252
1416
  }
1253
1417
  else {
1254
1418
  const request = step.value;
1255
- const child = this.invokeFrame(act.context.actor, request.callee, request.args, next, fail);
1419
+ const child = this.invokeFrame(act.context.actor, request.callee, request.args, next, fail, act.rcOwner);
1256
1420
  act.context.cancel = (fault) => child.cancel?.(fault);
1257
1421
  }
1258
1422
  };
@@ -1261,8 +1425,17 @@ class ExecutorV1 {
1261
1425
  }
1262
1426
  supervisorPool(act, term, operation) {
1263
1427
  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");
1428
+ if (view?.kind !== "vector")
1429
+ throw new Error("pool operation has no view");
1430
+ if (operation === "Len") {
1431
+ this.completeShim(act, term, {
1432
+ kind: "i64",
1433
+ value: BigInt(view.items.length),
1434
+ });
1435
+ return;
1436
+ }
1437
+ if (index?.kind !== "i64")
1438
+ throw new Error("pool operation has no index");
1266
1439
  const value = view.items[Number(index.value)];
1267
1440
  if (operation === "Get") {
1268
1441
  this.completeShim(act, term, this.variant(term.result_shape, value ? "OptionSome" : "OptionNone", value ? [value] : []));
@@ -1282,7 +1455,7 @@ class ExecutorV1 {
1282
1455
  const continueRendering = () => {
1283
1456
  const next = renderer.next();
1284
1457
  if (typeof next === "string") {
1285
- this.completeShim(act, term, { kind: "string", value: next });
1458
+ this.completeShim(act, term, stringValue(next));
1286
1459
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1287
1460
  return;
1288
1461
  }
@@ -1307,7 +1480,92 @@ class ExecutorV1 {
1307
1480
  }
1308
1481
  this.takeEdge(act, term.normal);
1309
1482
  }
1483
+ sweepRcOwner(owner, actor, done) {
1484
+ let firstFault = null;
1485
+ let count = 0;
1486
+ const byType = new Map();
1487
+ const schedule = (work) => this.scheduler.enqueue(actor?.id ?? "actor:root", work);
1488
+ const next = () => {
1489
+ const blocks = owner.blocks;
1490
+ owner.blocks = new Set();
1491
+ if (!blocks.size) {
1492
+ owner.closed = true;
1493
+ if (actor && count)
1494
+ this.trace.snapshot("actor.rc_sweep", {
1495
+ actor_id: actor.id,
1496
+ rc_sweep: {
1497
+ count: String(count),
1498
+ by_type: [...byType]
1499
+ .sort(([a], [b]) => a.localeCompare(b))
1500
+ .map(([type_name, amount]) => ({
1501
+ type_name,
1502
+ count: String(amount),
1503
+ })),
1504
+ },
1505
+ });
1506
+ done(firstFault);
1507
+ return;
1508
+ }
1509
+ const marking = blocks.values();
1510
+ const payloads = blocks.values();
1511
+ let remaining = 64;
1512
+ const release = () => {
1513
+ if (!remaining--) {
1514
+ remaining = 64;
1515
+ schedule(release);
1516
+ return;
1517
+ }
1518
+ const item = payloads.next();
1519
+ if (item.done) {
1520
+ schedule(next);
1521
+ return;
1522
+ }
1523
+ const cell = item.value;
1524
+ const payload = cell.value;
1525
+ cell.value = UNIT;
1526
+ blocks.delete(cell);
1527
+ this.closeValueAsync(payload, this.faultText(firstFault), (fault) => {
1528
+ firstFault = combineFaults(firstFault, fault);
1529
+ release();
1530
+ }, actor, owner);
1531
+ };
1532
+ const mark = () => {
1533
+ for (let i = 0; i < 64; i++) {
1534
+ const item = marking.next();
1535
+ if (item.done) {
1536
+ release();
1537
+ return;
1538
+ }
1539
+ const cell = item.value;
1540
+ cell.dying = true;
1541
+ cell.refs = 0;
1542
+ cell.owner = null;
1543
+ count += 1;
1544
+ byType.set(cell.typeName, (byType.get(cell.typeName) ?? 0) + 1);
1545
+ }
1546
+ schedule(mark);
1547
+ };
1548
+ mark();
1549
+ };
1550
+ next();
1551
+ }
1552
+ finishConstruction(act, done) {
1553
+ if (!act.fn.actor_construction || !act.rcOwner)
1554
+ return false;
1555
+ const owner = act.rcOwner;
1556
+ act.rcOwner = null;
1557
+ this.running = false;
1558
+ this.sweepRcOwner(owner, act.context.actor, (fault) => {
1559
+ act.fault = combineFaults(act.fault, fault);
1560
+ done();
1561
+ const current = this.current;
1562
+ this.scheduler.enqueue(current.context.id, () => this.runFrame(current));
1563
+ });
1564
+ return true;
1565
+ }
1310
1566
  returnFrom(act, value) {
1567
+ if (this.finishConstruction(act, () => this.returnFrom(act, value)))
1568
+ return;
1311
1569
  const caller = act.caller;
1312
1570
  if (!caller) {
1313
1571
  this.running = false;
@@ -1332,6 +1590,14 @@ class ExecutorV1 {
1332
1590
  this.current = caller;
1333
1591
  this.takeEdge(caller, act.normal);
1334
1592
  }
1593
+ dynamicEntry(receiver, slot) {
1594
+ const table = this.pkg.vtables[receiver.vtable] ??
1595
+ this.pkg.vtables.find((table) => table.id === receiver.vtable);
1596
+ const entry = table?.slots.find((entry) => entry.slot === slot);
1597
+ if (!entry)
1598
+ throw new Error(`vtable ${receiver.vtable} has no slot ${slot}`);
1599
+ return entry;
1600
+ }
1335
1601
  /// The wrapped value on its way to a trait method's `self`. A `copy` receiver
1336
1602
  /// deep-clones, a `move` invalidates the trait object it came out of, and a
1337
1603
  /// borrow passes the live value.
@@ -1359,6 +1625,8 @@ class ExecutorV1 {
1359
1625
  this.takeEdge(act, unwind);
1360
1626
  }
1361
1627
  resumeUnwind(act, handback) {
1628
+ if (this.finishConstruction(act, () => this.resumeUnwind(act, handback)))
1629
+ return;
1362
1630
  const fault = act.fault;
1363
1631
  if (!fault) {
1364
1632
  throw new Error(`${act.fn.name}: resume_unwind with no pending fault`);
@@ -1366,7 +1634,7 @@ class ExecutorV1 {
1366
1634
  const caller = act.caller;
1367
1635
  if (!caller) {
1368
1636
  this.running = false;
1369
- act.context.failed(fault);
1637
+ act.context.failed(fault, handback);
1370
1638
  return;
1371
1639
  }
1372
1640
  if (!act.unwind) {
@@ -1436,37 +1704,27 @@ class ExecutorV1 {
1436
1704
  return;
1437
1705
  }
1438
1706
  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());
1707
+ const sink = this.boundary(act, term.inputs[0]);
1708
+ const value = this.boundary(act, term.inputs[1]);
1709
+ const wake = this.park(act, term);
1445
1710
  const cancel = act.context.cancel;
1711
+ const stop = this.sendStream(sink, value, term.detail.park, act.context.actor, (status, fault) => {
1712
+ wake(UNIT, fault, status);
1713
+ }, act.rcOwner);
1446
1714
  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);
1715
+ stop(fault, (closeFault) => cancel?.(closeFault ?? fault));
1456
1716
  };
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
1717
  return;
1467
1718
  }
1468
1719
  case "StreamNext": {
1469
1720
  const stream = this.boundary(act, term.inputs[0]);
1721
+ const adapter = stream.kind === "stream"
1722
+ ? this.generatorStreams.get(stream.channelId)
1723
+ : undefined;
1724
+ if (adapter) {
1725
+ this.receiveGeneratorStream(act, term, adapter);
1726
+ return;
1727
+ }
1470
1728
  let cancelWait = () => { };
1471
1729
  const wake = this.park(act, term, () => cancelWait());
1472
1730
  cancelWait = this.pipes.receive(stream, term.detail.park, (value, fault) => {
@@ -1479,10 +1737,13 @@ class ExecutorV1 {
1479
1737
  }
1480
1738
  case "Await": {
1481
1739
  const task = this.taskFor(this.boundary(act, term.inputs[0]));
1482
- const wake = this.park(act, term);
1740
+ let unwatch = () => { };
1741
+ const wake = this.park(act, term, () => unwatch());
1483
1742
  const ready = () => {
1484
- if (!task.done)
1743
+ if (!task.done) {
1744
+ this.pendingActorWait(act, [task.actorTarget], false, "ask", (fault) => wake(UNIT, fault));
1485
1745
  return;
1746
+ }
1486
1747
  if (task.fault)
1487
1748
  wake(UNIT, task.fault);
1488
1749
  else {
@@ -1492,6 +1753,11 @@ class ExecutorV1 {
1492
1753
  }
1493
1754
  };
1494
1755
  task.changed.push(ready);
1756
+ unwatch = () => {
1757
+ const index = task.changed.indexOf(ready);
1758
+ if (index >= 0)
1759
+ task.changed.splice(index, 1);
1760
+ };
1495
1761
  ready();
1496
1762
  return;
1497
1763
  }
@@ -1554,6 +1820,11 @@ class ExecutorV1 {
1554
1820
  cancelTimer?.();
1555
1821
  wake({ kind: "i64", value: BigInt(ready[0].index) });
1556
1822
  }
1823
+ else {
1824
+ this.pendingActorWait(act, sources.map((value) => value.kind === "stream"
1825
+ ? undefined
1826
+ : this.taskFor(value).actorTarget), hasTimeout, "select", (fault) => wake(UNIT, fault));
1827
+ }
1557
1828
  };
1558
1829
  if (hasTimeout) {
1559
1830
  const duration = values[values.length - 1];
@@ -1581,18 +1852,39 @@ class ExecutorV1 {
1581
1852
  const protocol = term.detail;
1582
1853
  const inputs = term.inputs.map((input) => this.boundary(act, input));
1583
1854
  const wake = this.park(act, term);
1855
+ let target;
1856
+ let checkWait;
1584
1857
  const cancelRequest = this.ask(protocol, inputs, (value, error, closeFault) => {
1585
1858
  act.fault ??= closeFault ?? null;
1586
1859
  return wake(this.completion(term.result_shape, term.error_shape, value, error));
1587
- }, term.request_shapes);
1860
+ }, term.request_shapes, (actor) => {
1861
+ target = actor;
1862
+ checkWait?.();
1863
+ });
1588
1864
  const cancel = act.context.cancel;
1589
1865
  act.context.cancel = (fault) => cancelRequest((closeFault) => cancel(closeFault ?? fault));
1866
+ checkWait = () => this.pendingActorWait(act, [target], false, "ask", (fault) => cancelRequest((closeFault) => wake(UNIT, closeFault ?? fault)));
1867
+ checkWait();
1590
1868
  return;
1591
1869
  }
1592
1870
  case "NativeIo": {
1593
1871
  if (term.detail?.operation === "SinkFinish") {
1594
- this.host.pipes.close(this.boundary(act, term.inputs[0]));
1595
- this.park(act, term)(UNIT);
1872
+ const wake = this.park(act, term);
1873
+ const cancel = act.context.cancel;
1874
+ let stopped = null;
1875
+ act.context.cancel = (fault = {
1876
+ kind: "panic",
1877
+ message: "task cancelled",
1878
+ cancelled: true,
1879
+ }) => {
1880
+ stopped ??= fault;
1881
+ };
1882
+ this.finishSink(this.boundary(act, term.inputs[0]), null, () => {
1883
+ if (stopped)
1884
+ cancel(stopped);
1885
+ else
1886
+ wake(UNIT);
1887
+ });
1596
1888
  return;
1597
1889
  }
1598
1890
  // Admission passed `StdinReadLine`. The record is the raw line,
@@ -1602,6 +1894,40 @@ class ExecutorV1 {
1602
1894
  this.park(act, term)(bytesValue([...line]));
1603
1895
  return;
1604
1896
  }
1897
+ case "Safepoint": {
1898
+ if (act.context.pollSuppression) {
1899
+ this.takeEdge(act, term.resumes[0]);
1900
+ return;
1901
+ }
1902
+ if (act.context.pendingCancellation && term.cancel) {
1903
+ act.fault = act.context.pendingCancellation;
1904
+ this.takeEdge(act, term.cancel);
1905
+ return;
1906
+ }
1907
+ const budget = term.detail?.budget;
1908
+ if (!Number.isInteger(budget) || budget <= 0)
1909
+ throw new Error("safepoint requires a positive compiler-authored budget");
1910
+ const countdown = act.context.pollBudget;
1911
+ countdown.remaining = (countdown.remaining || budget) - 1;
1912
+ if (countdown.remaining > 0) {
1913
+ this.takeEdge(act, term.resumes[0]);
1914
+ return;
1915
+ }
1916
+ countdown.remaining = budget;
1917
+ this.scheduler.advanceSlice();
1918
+ const fault = act.context.pendingCancellation ?? this.scopeDeadline(act);
1919
+ if (fault && term.cancel) {
1920
+ act.fault = fault;
1921
+ this.takeEdge(act, term.cancel);
1922
+ }
1923
+ else if (this.scheduler.hasRunnableWork()) {
1924
+ this.park(act, term)(UNIT);
1925
+ }
1926
+ else {
1927
+ this.takeEdge(act, term.resumes[0]);
1928
+ }
1929
+ return;
1930
+ }
1605
1931
  case "Sleep": {
1606
1932
  const duration = term.inputs[0]
1607
1933
  ? this.boundary(act, term.inputs[0])
@@ -1616,13 +1942,34 @@ class ExecutorV1 {
1616
1942
  throw new Error(`suspension kind ${term.kind} resolved no handler after admission`);
1617
1943
  }
1618
1944
  }
1945
+ pendingActorWait(act, targets, externalProgress, operation, fail) {
1946
+ const owner = act.context.actor;
1947
+ // Independent tasks carry actor ownership for cleanup, but do not hold its turn.
1948
+ if (!owner || owner.active !== act.context || !act.context.parked)
1949
+ return;
1950
+ this.actorWaits.ready(owner);
1951
+ if (externalProgress ||
1952
+ this.requestDeadline(act) !== undefined ||
1953
+ targets.some((target) => !target))
1954
+ return;
1955
+ const path = this.actorWaits.pending(owner, targets);
1956
+ if (path)
1957
+ fail({
1958
+ kind: "panic",
1959
+ message: `local actor wait cycle at ${operation}: ${path.map((actor) => actor.identity).join(" -> ")}`,
1960
+ });
1961
+ }
1619
1962
  park(act, term, cleanup = () => { }) {
1620
1963
  this.running = false;
1964
+ act.context.parked = true;
1621
1965
  let pending = null;
1622
1966
  const resume = (value, fault = null, edge = 0) => {
1623
1967
  if (pending)
1624
1968
  return false;
1625
1969
  pending = { value, fault, edge };
1970
+ act.context.parked = false;
1971
+ if (act.context.actor?.active === act.context)
1972
+ this.actorWaits.ready(act.context.actor);
1626
1973
  cleanup();
1627
1974
  this.scheduler.enqueue(act.context.id, () => {
1628
1975
  const outcome = pending;
@@ -1631,7 +1978,8 @@ class ExecutorV1 {
1631
1978
  const fault = closeFault ?? (outcome.fault ? (act.fault ?? outcome.fault) : null);
1632
1979
  if (fault) {
1633
1980
  act.fault = fault;
1634
- this.takeEdge(act, fault.kind === "panic" && fault.cancelled
1981
+ this.takeEdge(act, term.kind === "Safepoint" ||
1982
+ (fault.kind === "panic" && fault.cancelled)
1635
1983
  ? term.cancel
1636
1984
  : term.unwind);
1637
1985
  }
@@ -1649,13 +1997,15 @@ class ExecutorV1 {
1649
1997
  // the cancellation latch live through that window and retire a result
1650
1998
  // whose normal edge the consumer never entered.
1651
1999
  if (outcome.fault)
1652
- this.closeValueAsync(outcome.value, this.faultText(act.fault ?? outcome.fault), finish, act.context.actor);
2000
+ this.closeValueAsync(outcome.value, this.faultText(act.fault ?? outcome.fault), finish, act.context.actor, act.rcOwner);
1653
2001
  else
1654
2002
  finish();
1655
2003
  });
1656
2004
  return true;
1657
2005
  };
1658
2006
  act.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
2007
+ if (term.cancel == null)
2008
+ return;
1659
2009
  if (pending)
1660
2010
  pending.fault ??= fault;
1661
2011
  else
@@ -1663,6 +2013,222 @@ class ExecutorV1 {
1663
2013
  };
1664
2014
  return resume;
1665
2015
  }
2016
+ sinkClosed(value) {
2017
+ const endpoint = value.kind === "sink"
2018
+ ? this.senderEndpoints.get(value.channelId)
2019
+ : undefined;
2020
+ return endpoint ? !endpoint.open : this.pipes.isClosed(value);
2021
+ }
2022
+ senderEndpoint(owner, open = true) {
2023
+ const id = this.ids.channel();
2024
+ owner.references++;
2025
+ if (open)
2026
+ owner.endpoints++;
2027
+ this.senderEndpoints.set(id, { owner, open });
2028
+ return { kind: "sink", channelId: id };
2029
+ }
2030
+ finishSink(value, fault, done) {
2031
+ const endpoint = value.kind === "sink"
2032
+ ? this.senderEndpoints.get(value.channelId)
2033
+ : undefined;
2034
+ if (!endpoint) {
2035
+ this.pipes.close(value, fault);
2036
+ done();
2037
+ return;
2038
+ }
2039
+ if (!endpoint.open) {
2040
+ done();
2041
+ return;
2042
+ }
2043
+ endpoint.open = false;
2044
+ endpoint.owner.endpoints--;
2045
+ if (fault || endpoint.owner.endpoints === 0) {
2046
+ const finish = () => {
2047
+ if (endpoint.owner.busy) {
2048
+ endpoint.owner.waiting.add(finish);
2049
+ return;
2050
+ }
2051
+ this.finishSink(endpoint.owner.sink, fault, done);
2052
+ };
2053
+ finish();
2054
+ }
2055
+ else
2056
+ done();
2057
+ }
2058
+ sendStream(sink, value, park, actor, complete, rcOwner) {
2059
+ const endpoint = sink.kind === "sink"
2060
+ ? this.senderEndpoints.get(sink.channelId)
2061
+ : undefined;
2062
+ let owned = true;
2063
+ let finished = false;
2064
+ let closing = false;
2065
+ let claimed = false;
2066
+ let stopped = null;
2067
+ let encoder = null;
2068
+ let child = null;
2069
+ let withdraw = () => { };
2070
+ const drained = [];
2071
+ const release = () => {
2072
+ if (!claimed || !endpoint)
2073
+ return;
2074
+ claimed = false;
2075
+ endpoint.owner.busy = false;
2076
+ for (const wake of endpoint.owner.waiting)
2077
+ this.scheduler.enqueue(actor?.id ?? "actor:root", wake);
2078
+ endpoint.owner.waiting.clear();
2079
+ };
2080
+ const finish = (status, fault = null) => {
2081
+ if (finished || closing)
2082
+ return;
2083
+ closing = true;
2084
+ release();
2085
+ const done = (closeFault = null) => {
2086
+ finished = true;
2087
+ const failure = combineFaults(fault, closeFault);
2088
+ if (stopped)
2089
+ for (const wake of drained.splice(0))
2090
+ wake(failure);
2091
+ else
2092
+ complete(status, failure);
2093
+ };
2094
+ if (owned) {
2095
+ owned = false;
2096
+ this.closeValueAsync(value, this.faultText(fault ?? stopped), done, actor, rcOwner);
2097
+ }
2098
+ else
2099
+ done();
2100
+ };
2101
+ const start = () => {
2102
+ if (stopped || finished || closing)
2103
+ return;
2104
+ if (!endpoint) {
2105
+ withdraw = this.pipes.send(sink, value, park, (status) => {
2106
+ if (status === 0)
2107
+ owned = false;
2108
+ finish(status);
2109
+ });
2110
+ return;
2111
+ }
2112
+ if (!endpoint.open) {
2113
+ finish(1);
2114
+ return;
2115
+ }
2116
+ const owner = endpoint.owner;
2117
+ if (owner.busy) {
2118
+ if (!park) {
2119
+ finish(2);
2120
+ return;
2121
+ }
2122
+ owner.waiting.add(start);
2123
+ withdraw = () => {
2124
+ owner.waiting.delete(start);
2125
+ };
2126
+ return;
2127
+ }
2128
+ owner.busy = true;
2129
+ claimed = true;
2130
+ const callable = owner.encoder;
2131
+ encoder = this.invokeFrame(actor, calleeFunction(callable, null), callable.kind === "closure" ? [callable.environment, value] : [value], (encoded) => {
2132
+ encoder = null;
2133
+ if (stopped) {
2134
+ this.closeValueAsync(encoded, this.faultText(stopped), (fault) => finish(0, fault), actor, rcOwner);
2135
+ }
2136
+ else {
2137
+ child = this.sendStream(owner.sink, encoded, park, actor, (status, fault) => {
2138
+ child = null;
2139
+ finish(status, fault);
2140
+ }, rcOwner);
2141
+ }
2142
+ }, (fault) => {
2143
+ encoder = null;
2144
+ finish(3, fault);
2145
+ }, rcOwner);
2146
+ };
2147
+ start();
2148
+ return (fault, done) => {
2149
+ if (finished) {
2150
+ done(null);
2151
+ return;
2152
+ }
2153
+ stopped ??= fault;
2154
+ drained.push(done);
2155
+ withdraw();
2156
+ if (closing)
2157
+ return;
2158
+ if (encoder)
2159
+ encoder.cancel?.(fault);
2160
+ else if (child)
2161
+ child(fault, (failure) => {
2162
+ child = null;
2163
+ finish(0, failure);
2164
+ });
2165
+ else
2166
+ finish(0);
2167
+ };
2168
+ }
2169
+ receiveGeneratorStream(act, term, adapter) {
2170
+ const wake = this.park(act, term);
2171
+ const receive = (value, fault) => {
2172
+ if (fault)
2173
+ wake(UNIT, fault);
2174
+ else
2175
+ wake(this.variant(term.result_shape, value === null ? "OptionNone" : "OptionSome", value === null ? [] : [value]));
2176
+ };
2177
+ if (adapter.pending !== null) {
2178
+ const value = adapter.pending;
2179
+ adapter.pending = null;
2180
+ receive(value, null);
2181
+ return;
2182
+ }
2183
+ const generator = adapter.generator;
2184
+ if (generator.done) {
2185
+ receive(null, adapter.fault ?? generator.fault);
2186
+ return;
2187
+ }
2188
+ if (term.detail.park)
2189
+ adapter.waiting = receive;
2190
+ else
2191
+ receive(null, null);
2192
+ const cancel = act.context.cancel;
2193
+ if (term.detail.park)
2194
+ act.context.cancel = (fault) => {
2195
+ adapter.waiting = undefined;
2196
+ this.closeGenerator(generator, (closeFault) => cancel?.(closeFault ?? fault));
2197
+ };
2198
+ if (adapter.receiving)
2199
+ return;
2200
+ adapter.receiving = true;
2201
+ generator.receive = (value, fault) => {
2202
+ adapter.receiving = false;
2203
+ if (adapter.closed) {
2204
+ if (value !== null)
2205
+ this.closeValueAsync(value, null, () => { });
2206
+ return;
2207
+ }
2208
+ adapter.fault = fault;
2209
+ const waiting = adapter.waiting;
2210
+ adapter.waiting = undefined;
2211
+ if (waiting)
2212
+ waiting(value, fault);
2213
+ else
2214
+ adapter.pending = value;
2215
+ };
2216
+ if (generator.started) {
2217
+ const resume = generator.resume;
2218
+ generator.resume = undefined;
2219
+ if (!resume)
2220
+ throw new Error("generator stream lost its suspended yield");
2221
+ resume();
2222
+ }
2223
+ else {
2224
+ this.scheduler.enqueue(generator.id, () => {
2225
+ if (generator.done)
2226
+ return;
2227
+ generator.started = true;
2228
+ this.runFrame(generator.frame);
2229
+ });
2230
+ }
2231
+ }
1666
2232
  newPipe(capacity) {
1667
2233
  const id = this.ids.channel();
1668
2234
  this.trace.allocateId("channel", id, this.current?.context.id ?? "actor:root");
@@ -1688,7 +2254,7 @@ class ExecutorV1 {
1688
2254
  act.fault = combineFaults(act.fault, fault);
1689
2255
  if (!synchronous)
1690
2256
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1691
- });
2257
+ }, act.context.actor, act.rcOwner);
1692
2258
  synchronous = false;
1693
2259
  if (!completed)
1694
2260
  this.running = false;
@@ -1713,7 +2279,7 @@ class ExecutorV1 {
1713
2279
  receive?.(null, generator.fault);
1714
2280
  for (const closed of generator.closed.splice(0))
1715
2281
  closed(generator.fault);
1716
- });
2282
+ }, generator.frame.context.actor, generator.frame.rcOwner);
1717
2283
  }
1718
2284
  closeGenerator(generator, done) {
1719
2285
  if (generator.done) {
@@ -1726,7 +2292,7 @@ class ExecutorV1 {
1726
2292
  else
1727
2293
  generator.frame.context.cancel?.();
1728
2294
  }
1729
- closeValueAsync(value, fault, done, actor = this.current.context.actor) {
2295
+ closeValueAsync(value, fault, done, actor = this.current.context.actor, rcOwner = actor?.rcOwner ?? this.current.rcOwner) {
1730
2296
  const pending = [value];
1731
2297
  let firstFault = null;
1732
2298
  const next = (failure = null) => {
@@ -1737,15 +2303,35 @@ class ExecutorV1 {
1737
2303
  while (pending.length) {
1738
2304
  const value = pending.pop();
1739
2305
  switch (value.kind) {
2306
+ case "string":
2307
+ value.release();
2308
+ break;
1740
2309
  case "rc":
1741
2310
  if (!value.closed) {
1742
2311
  value.closed = true;
1743
- if (--value.cell.refs === 0) {
2312
+ if (value.cell && !value.cell.dying && --value.cell.refs === 0) {
2313
+ value.cell.owner?.blocks.delete(value.cell);
2314
+ value.cell.owner = null;
1744
2315
  pending.push(value.cell.value);
1745
2316
  value.cell.value = UNIT;
1746
2317
  }
1747
2318
  }
1748
2319
  break;
2320
+ case "weak":
2321
+ if (!value.closed) {
2322
+ value.closed = true;
2323
+ value.cell.weak -= 1;
2324
+ }
2325
+ break;
2326
+ case "timer": {
2327
+ if (value.closed)
2328
+ break;
2329
+ const resource = this.pkg.resources?.[value.resource];
2330
+ if (resource?.close === undefined)
2331
+ throw new Error("timer lost its selected close");
2332
+ this.invokeFrame(actor, resource.close, [value], () => next(), next, rcOwner);
2333
+ return;
2334
+ }
1749
2335
  case "task": {
1750
2336
  // Dropping a handle does not cancel scoped work. Join drains the
1751
2337
  // task, and scope close releases any result that was never taken.
@@ -1753,7 +2339,7 @@ class ExecutorV1 {
1753
2339
  if (task?.done) {
1754
2340
  const result = task.value;
1755
2341
  task.value = UNIT;
1756
- this.closeValueAsync(result, currentFault, (fault) => next(fault ?? (task.close ? task.fault : null)), actor);
2342
+ this.closeValueAsync(result, currentFault, (fault) => next(fault ?? (task.close ? task.fault : null)), actor, rcOwner);
1757
2343
  return;
1758
2344
  }
1759
2345
  if (task?.close) {
@@ -1781,7 +2367,7 @@ class ExecutorV1 {
1781
2367
  if (this.consumedResources.has(value))
1782
2368
  break;
1783
2369
  if (!this.closingResources.has(value)) {
1784
- this.invokeFrame(actor, resource.close, [value], () => next(), next);
2370
+ this.invokeFrame(actor, resource.close, [value], () => next(), next, rcOwner);
1785
2371
  return;
1786
2372
  }
1787
2373
  this.consumedResources.add(value);
@@ -1795,10 +2381,36 @@ class ExecutorV1 {
1795
2381
  case "vector":
1796
2382
  pending.push(...[...value.items].reverse());
1797
2383
  break;
1798
- case "sink":
1799
- case "stream":
2384
+ case "stream": {
2385
+ const adapter = this.generatorStreams.get(value.channelId);
2386
+ if (adapter) {
2387
+ this.generatorStreams.delete(value.channelId);
2388
+ adapter.closed = true;
2389
+ if (adapter.pending !== null)
2390
+ pending.push(adapter.pending);
2391
+ adapter.pending = null;
2392
+ adapter.waiting = undefined;
2393
+ this.closeGenerator(adapter.generator, next);
2394
+ return;
2395
+ }
1800
2396
  pending.push(...this.pipes.close(value, currentFault).reverse());
1801
2397
  break;
2398
+ }
2399
+ case "sink": {
2400
+ const endpoint = this.senderEndpoints.get(value.channelId);
2401
+ if (endpoint) {
2402
+ this.finishSink(value, currentFault, () => {
2403
+ this.senderEndpoints.delete(value.channelId);
2404
+ if (--endpoint.owner.references === 0)
2405
+ pending.push(endpoint.owner.sink, endpoint.owner.encoder);
2406
+ next();
2407
+ });
2408
+ return;
2409
+ }
2410
+ else
2411
+ pending.push(...this.pipes.close(value, currentFault).reverse());
2412
+ break;
2413
+ }
1802
2414
  }
1803
2415
  }
1804
2416
  done(firstFault);
@@ -1806,6 +2418,8 @@ class ExecutorV1 {
1806
2418
  next();
1807
2419
  }
1808
2420
  closeValue(value, fault) {
2421
+ if (value.kind === "string")
2422
+ value.release();
1809
2423
  if (value.kind === "closure")
1810
2424
  this.closeValue(value.environment, fault);
1811
2425
  else if (value.kind === "sink" || value.kind === "stream")
@@ -1854,25 +2468,33 @@ class ExecutorV1 {
1854
2468
  for (const changed of [...task.changed])
1855
2469
  changed();
1856
2470
  }
1857
- invokeFrame(actor, callable, args, returned, failed) {
1858
- const frame = this.activate(this.functionAt(callable), args, null, null, undefined, null, {
2471
+ invokeFrame(actor, callable, args, returned, failed, rcOwner = actor?.rcOwner ?? null, inline = false) {
2472
+ const context = {
1859
2473
  id: actor?.id ?? "actor:root",
1860
2474
  actor,
2475
+ rcOwner,
1861
2476
  returned,
1862
- failed,
1863
- });
2477
+ failed: (fault) => failed(fault),
2478
+ };
2479
+ const frame = this.activate(this.functionAt(callable), args, null, null, undefined, null, context);
1864
2480
  let cancelled = null;
1865
- frame.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
2481
+ context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
1866
2482
  cancelled = fault;
1867
2483
  };
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;
2484
+ if (inline) {
2485
+ this.current = frame;
2486
+ this.running = true;
2487
+ }
2488
+ else {
2489
+ this.scheduler.enqueue(frame.context.id, () => {
2490
+ context.cancel = undefined;
2491
+ if (cancelled && frame.context === context)
2492
+ context.failed(cancelled);
2493
+ else
2494
+ this.runFrame(frame);
2495
+ });
2496
+ }
2497
+ return context;
1876
2498
  }
1877
2499
  variant(shapeId, role, payload = []) {
1878
2500
  const shape = shapeId == null ? undefined : this.pkg.variants[shapeId];
@@ -1920,12 +2542,21 @@ class ExecutorV1 {
1920
2542
  ? { kind: "Actor", identity: act.context.actor.identity }
1921
2543
  : { kind: "Task" };
1922
2544
  switch (operation.op) {
2545
+ case "identity": {
2546
+ const [left, right] = args;
2547
+ this.completeShim(act, term, {
2548
+ kind: "bool",
2549
+ value: actorIdentical(left, right),
2550
+ });
2551
+ return;
2552
+ }
1923
2553
  case "spawn": {
1924
2554
  const layout = this.pkg.actors?.find((actor) => actor.id === operation.actor);
1925
2555
  if (!layout)
1926
2556
  throw new Error(`actor layout ${operation.actor} is missing`);
1927
2557
  let supplied = 0;
1928
2558
  const actor = {
2559
+ rcOwner: act.rcOwner ?? { blocks: new Set(), closed: false },
1929
2560
  identity: ++this.actorIdentity,
1930
2561
  id: this.ids.actor(),
1931
2562
  layout,
@@ -1935,9 +2566,11 @@ class ExecutorV1 {
1935
2566
  fields: layout.state_fields.map((field) => field.deferred ? UNIT : (args[supplied++] ?? UNIT)),
1936
2567
  },
1937
2568
  mailbox: [],
2569
+ timers: new Set(),
1938
2570
  disposals: [],
1939
2571
  admission: [],
1940
2572
  busy: true,
2573
+ constructing: true,
1941
2574
  alive: true,
1942
2575
  completed: false,
1943
2576
  closing: false,
@@ -1947,44 +2580,181 @@ class ExecutorV1 {
1947
2580
  closingFault: null,
1948
2581
  crashing: null,
1949
2582
  active: null,
1950
- stopHook: null,
2583
+ terminalHook: null,
1951
2584
  closed: [],
1952
2585
  };
1953
2586
  this.actors.set(actor.id, actor);
1954
2587
  this.trace.allocateId("actor", actor.id, act.context.id);
1955
2588
  this.trace.snapshot("actor.spawn", { actor_id: actor.id });
1956
2589
  const handle = { kind: "actor", id: actor.id };
1957
- this.running = false;
1958
2590
  const failed = (fault) => {
2591
+ actor.active = null;
1959
2592
  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));
2593
+ actor.constructing = false;
2594
+ act.rcOwner = null;
2595
+ this.cancelActorTimers(actor);
2596
+ this.drainMailbox(actor, this.faultText(fault), (payloadFault) => {
2597
+ const primary = combineFaults(fault, payloadFault);
2598
+ this.closeValueAsync(actor.state, this.faultText(primary), (stateFault) => {
2599
+ const retained = combineFaults(primary, stateFault);
2600
+ this.sweepRcOwner(actor.rcOwner, actor, (sweepFault) => {
2601
+ actor.completed = true;
2602
+ for (const closed of actor.closed.splice(0))
2603
+ closed();
2604
+ this.raiseFault(act, combineFaults(retained, sweepFault), term.unwind);
2605
+ this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2606
+ });
2607
+ }, actor, actor.rcOwner);
1968
2608
  });
1969
2609
  };
1970
2610
  const started = () => {
2611
+ if (actor.closing) {
2612
+ failed(actor.active?.pendingCancellation ?? {
2613
+ kind: "panic",
2614
+ message: "actor construction cancelled",
2615
+ cancelled: true,
2616
+ });
2617
+ return;
2618
+ }
2619
+ actor.constructing = false;
2620
+ actor.active = null;
2621
+ act.rcOwner = null;
1971
2622
  actor.busy = false;
2623
+ this.startPeriodicTimers(actor);
1972
2624
  this.dispatchActor(actor);
1973
2625
  this.completeShim(act, term, handle);
1974
- this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2626
+ this.current = act;
2627
+ this.running = true;
1975
2628
  };
2629
+ // Construction enters the same interpreter loop as its caller. Only
2630
+ // a real suspension queues work and advances deterministic scheduling.
1976
2631
  const start = () => {
1977
2632
  if (layout.start !== undefined)
1978
- this.invokeFrame(actor, layout.start, [actor.state], started, failed);
2633
+ actor.active = this.invokeFrame(actor, layout.start, [actor.state], started, failed, actor.rcOwner, true);
1979
2634
  else
1980
2635
  started();
1981
2636
  };
1982
2637
  if (layout.init !== undefined)
1983
- this.invokeFrame(actor, layout.init, [actor.state, ...args.slice(supplied)], start, failed);
2638
+ actor.active = this.invokeFrame(actor, layout.init, [actor.state, ...args.slice(supplied)], start, failed, actor.rcOwner, true);
1984
2639
  else
1985
2640
  start();
1986
2641
  return;
1987
2642
  }
2643
+ case "timer_schedule": {
2644
+ const [target, after, overdue, hasOverdue, payload] = args;
2645
+ const actor = this.actorFor(target);
2646
+ if (actor !== act.context.actor ||
2647
+ after?.kind !== "i64" ||
2648
+ overdue?.kind !== "i64" ||
2649
+ hasOverdue?.kind !== "bool" ||
2650
+ payload?.kind !== "record")
2651
+ throw new Error("timer schedule lost its checked owner or timing");
2652
+ const handler = actor.layout.handlers.find((handler) => handler.message_id === operation.message);
2653
+ if (!handler)
2654
+ throw new Error("timer schedule lost its handler");
2655
+ if (this.rootComplete || actor.closing) {
2656
+ this.running = false;
2657
+ const refused = {
2658
+ kind: "panic",
2659
+ actorSendFailed: true,
2660
+ message: "actor timer admission is closed",
2661
+ };
2662
+ this.closeValueAsync(payload, faultReport(refused), (fault) => {
2663
+ this.raiseFault(act, combineFaults(refused, fault), term.unwind);
2664
+ this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2665
+ }, actor, act.rcOwner);
2666
+ return;
2667
+ }
2668
+ let phase = "scheduled";
2669
+ let pending = payload;
2670
+ let overdueAt = null;
2671
+ let disarm = null;
2672
+ const forget = () => {
2673
+ disarm?.();
2674
+ disarm = null;
2675
+ actor.timers.delete(timer);
2676
+ };
2677
+ const miss = (discarded) => {
2678
+ if (phase !== "fired")
2679
+ return;
2680
+ phase = hasOverdue.value ? "overdue" : "cancelled";
2681
+ forget();
2682
+ if (discarded && !hasOverdue.value)
2683
+ this.trace.writeStderr("actor.timer_discarded: the one-shot handler did not start\n", null);
2684
+ if (!hasOverdue.value || !actor.alive)
2685
+ return;
2686
+ const fault = {
2687
+ kind: "panic",
2688
+ timerOverdue: true,
2689
+ message: "actor timer did not start before its overdue deadline",
2690
+ };
2691
+ if (actor.active) {
2692
+ actor.active.pendingCancellation ??= fault;
2693
+ actor.active.cancel?.(actor.active.pendingCancellation);
2694
+ }
2695
+ else
2696
+ this.crashActor(actor, fault, handler);
2697
+ };
2698
+ const timer = {
2699
+ cancel: () => {
2700
+ if (phase !== "overdue")
2701
+ phase = "cancelled";
2702
+ forget();
2703
+ if (pending) {
2704
+ actor.disposals.push(pending);
2705
+ pending = null;
2706
+ this.dispatchActor(actor);
2707
+ }
2708
+ },
2709
+ started: () => {
2710
+ if (phase === "fired" &&
2711
+ overdueAt !== null &&
2712
+ this.trace.virtualTimeMs >= overdueAt)
2713
+ miss(false);
2714
+ const allowed = phase !== "overdue";
2715
+ if (phase === "fired")
2716
+ phase = "started";
2717
+ forget();
2718
+ return allowed;
2719
+ },
2720
+ discarded: () => miss(true),
2721
+ };
2722
+ actor.timers.add(timer);
2723
+ disarm = this.scheduler.after(after.value, () => {
2724
+ disarm = null;
2725
+ if (phase !== "scheduled")
2726
+ return;
2727
+ phase = "fired";
2728
+ pending = null;
2729
+ if (hasOverdue.value) {
2730
+ overdueAt =
2731
+ this.trace.virtualTimeMs + Number(overdue.value) / 1_000_000;
2732
+ disarm = this.scheduler.after(overdue.value, () => miss(false));
2733
+ }
2734
+ const message = {
2735
+ timer,
2736
+ caller: { kind: "Timer" },
2737
+ handler,
2738
+ payload: payload.fields,
2739
+ reply: false,
2740
+ obligated: false,
2741
+ complete: (_value, _error, drained) => drained?.(null),
2742
+ };
2743
+ const status = this.admitActorMessage(actor, message);
2744
+ if (status === "full" || status === "closed") {
2745
+ actor.disposals.push(payload);
2746
+ timer.discarded();
2747
+ this.dispatchActor(actor);
2748
+ }
2749
+ });
2750
+ this.completeShim(act, term, {
2751
+ kind: "timer",
2752
+ resource: operation.resource,
2753
+ cancel: () => timer.cancel(),
2754
+ closed: false,
2755
+ });
2756
+ return;
2757
+ }
1988
2758
  case "stream_start": {
1989
2759
  const [target, payload] = args;
1990
2760
  const fields = fieldsOf(payload);
@@ -2035,6 +2805,10 @@ class ExecutorV1 {
2035
2805
  task.actorError = error;
2036
2806
  this.settleTask(task, value ?? UNIT, closeFault ?? null);
2037
2807
  return true;
2808
+ }, undefined, (actor) => {
2809
+ task.actorTarget = actor;
2810
+ for (const changed of [...task.changed])
2811
+ changed();
2038
2812
  });
2039
2813
  task.close = (done) => cancelRequest((fault) => {
2040
2814
  this.settleTask(task, UNIT, fault);
@@ -2071,15 +2845,17 @@ class ExecutorV1 {
2071
2845
  this.variant(shapes.success, discarded ? "DeliveryDiscarded" : "DeliveryAccepted"),
2072
2846
  ]);
2073
2847
  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
- },
2848
+ this.variant(term.error_shape, "ActorErrorRejected", [
2849
+ {
2850
+ kind: "record",
2851
+ typeId: this.aggregateName(shapes.failure),
2852
+ shape: shapes.failure,
2853
+ fields: [
2854
+ this.variant(shapes.reason, sendErrorRole(reason)),
2855
+ request,
2856
+ ],
2857
+ },
2858
+ ]),
2083
2859
  ]);
2084
2860
  let parked = false;
2085
2861
  const finish = (value) => {
@@ -2092,87 +2868,38 @@ class ExecutorV1 {
2092
2868
  finish(rejected("Dead"));
2093
2869
  return;
2094
2870
  }
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
- }
2871
+ const message = {
2872
+ caller,
2873
+ handler,
2874
+ payload: payload.fields,
2875
+ reply: false,
2876
+ obligated: false,
2877
+ complete: (_value, _error, drained) => drained?.(null),
2120
2878
  };
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) {
2879
+ const status = this.admitActorMessage(actor, message);
2880
+ if (status === "full") {
2166
2881
  if (operation.policy === "drop_newest") {
2167
- discard(payload, accepted(true));
2882
+ let settled = false;
2883
+ let waiting = false;
2884
+ this.closeValueAsync(payload, this.pipeFault(act), (fault) => {
2885
+ act.fault ??= fault;
2886
+ settled = true;
2887
+ if (waiting)
2888
+ finish(accepted(true));
2889
+ else
2890
+ this.completeShim(act, term, accepted(true));
2891
+ }, act.context.actor, act.rcOwner);
2892
+ if (!settled) {
2893
+ waiting = true;
2894
+ parked = true;
2895
+ this.running = false;
2896
+ }
2168
2897
  return;
2169
2898
  }
2170
2899
  if (operation.policy === "reject") {
2171
2900
  finish(rejected("Full"));
2172
2901
  return;
2173
2902
  }
2174
- // The checker already resolved declaration defaults and explicit
2175
- // overrides; the declaration's own resolution happened above.
2176
2903
  if (operation.policy !== "wait")
2177
2904
  throw new Error("submission has no admitted policy");
2178
2905
  parked = true;
@@ -2180,20 +2907,10 @@ class ExecutorV1 {
2180
2907
  actor.admission.push(submit);
2181
2908
  return;
2182
2909
  }
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));
2910
+ if (status === "closed")
2911
+ finish(rejected("Dead"));
2912
+ else
2913
+ finish(accepted(status === "discarded"));
2197
2914
  };
2198
2915
  submit();
2199
2916
  return;
@@ -2202,15 +2919,32 @@ class ExecutorV1 {
2202
2919
  const layout = this.pkg.supervisors?.find((layout) => layout.id === operation.supervisor);
2203
2920
  if (!layout)
2204
2921
  throw new Error("supervisor descriptor is missing");
2922
+ const isPool = layout.children.some((child) => child.is_pool);
2923
+ const count = isPool ? args[args.length - 1] : undefined;
2924
+ if (isPool && count?.kind !== "i64")
2925
+ throw new Error("pool construction has no count");
2926
+ const config = isPool ? args.slice(0, -1) : args;
2927
+ if (count?.kind === "i64" &&
2928
+ (count.value <= 0n || count.value > 2147483647n)) {
2929
+ const fault = {
2930
+ kind: "panic",
2931
+ message: "supervisor pool count must be between 1 and 2147483647",
2932
+ };
2933
+ this.running = false;
2934
+ this.closeValueAsync({ kind: "record", typeId: "", fields: config }, fault.message, (closeFault) => {
2935
+ this.raiseFault(act, combineFaults(fault, closeFault), term.unwind);
2936
+ this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2937
+ }, act.context.actor, act.rcOwner);
2938
+ return;
2939
+ }
2940
+ const members = count?.kind === "i64" ? Number(count.value) : 1;
2205
2941
  const owner = {
2206
2942
  id: this.ids.supervisor(),
2207
2943
  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)),
2944
+ config,
2945
+ children: layout.children.flatMap(() => Array.from({ length: members }, () => null)),
2946
+ specs: layout.children.flatMap((child) => Array.from({ length: members }, () => child)),
2947
+ offsets: layout.children.map((_child, index) => index),
2214
2948
  recovering: new Set(),
2215
2949
  restartBusy: false,
2216
2950
  pendingRestarts: [],
@@ -2269,7 +3003,7 @@ class ExecutorV1 {
2269
3003
  this.completeShim(act, term, {
2270
3004
  kind: "vector",
2271
3005
  elementType: "role",
2272
- items: Array.from({ length: spec.pool_count ?? 1 }, (_, index) => makeRole(offset + index)),
3006
+ items: Array.from({ length: owner.children.length }, (_, index) => makeRole(offset + index)),
2273
3007
  });
2274
3008
  return;
2275
3009
  }
@@ -2279,12 +3013,16 @@ class ExecutorV1 {
2279
3013
  case "await_restarted": {
2280
3014
  const target = args[0];
2281
3015
  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)) {
3016
+ if (!role) {
3017
+ if (target.kind !== "actor" && target.kind !== "supervisor")
3018
+ throw new Error("restarted requires an actor or supervisor handle");
3019
+ this.completeShim(act, term, UNIT);
3020
+ return;
3021
+ }
3022
+ if (this.roleRestartPending(role)) {
2285
3023
  this.running = false;
2286
3024
  const settled = () => {
2287
- if (this.rolePending(role)) {
3025
+ if (this.roleRestartPending(role)) {
2288
3026
  role.waiting.push(settled);
2289
3027
  return;
2290
3028
  }
@@ -2324,7 +3062,10 @@ class ExecutorV1 {
2324
3062
  if (target.kind === "actor") {
2325
3063
  const actor = this.actorFor(target);
2326
3064
  const complete = () => {
2327
- const fault = actor.crashing ?? actor.closingFault;
3065
+ // A crash was reported when it happened and already counts toward
3066
+ // the exit status; only a fault raised while the actor was being
3067
+ // stopped passes to the caller that waits for it.
3068
+ const fault = actor.closingFault;
2328
3069
  if (actor.closeFaultDebt !== undefined)
2329
3070
  this.faultDebts.delete(actor.closeFaultDebt);
2330
3071
  if (fault)
@@ -2370,6 +3111,14 @@ class ExecutorV1 {
2370
3111
  throw new Error(`actor operation ${operation.op} has no executor`);
2371
3112
  }
2372
3113
  }
3114
+ scopeDeadline(act, expected) {
3115
+ for (let frame = act; frame?.context === act.context; frame = frame.caller) {
3116
+ for (const group of frame.scopes.values())
3117
+ if (group.deadline && (!expected || group.deadline === expected))
3118
+ return group.deadline;
3119
+ }
3120
+ return undefined;
3121
+ }
2373
3122
  requestDeadline(act) {
2374
3123
  let deadline = act.context.requestDeadline;
2375
3124
  for (let frame = act; frame; frame = frame.caller) {
@@ -2380,11 +3129,12 @@ class ExecutorV1 {
2380
3129
  }
2381
3130
  return deadline;
2382
3131
  }
2383
- ask(protocol, inputs, complete, shapes) {
3132
+ ask(protocol, inputs, complete, shapes, selectedTarget) {
2384
3133
  let settled = false;
2385
3134
  let admitted = false;
2386
3135
  let closing = false;
2387
3136
  const owner = this.current.context.actor;
3137
+ const rcOwner = this.current.rcOwner;
2388
3138
  const deadline = this.requestDeadline(this.current);
2389
3139
  let cancelTimer;
2390
3140
  let message;
@@ -2406,7 +3156,7 @@ class ExecutorV1 {
2406
3156
  this.crashActor(owner, fault);
2407
3157
  else if (fault)
2408
3158
  this.rootFault ??= fault;
2409
- }, admitted ? receiver : owner);
3159
+ }, admitted ? receiver : owner, admitted ? (receiver?.rcOwner ?? null) : rcOwner);
2410
3160
  }
2411
3161
  else
2412
3162
  drained?.(null);
@@ -2430,7 +3180,7 @@ class ExecutorV1 {
2430
3180
  closeFault = fault;
2431
3181
  for (const waiter of closeWaiters.splice(0))
2432
3182
  waiter(fault);
2433
- }, owner);
3183
+ }, owner, rcOwner);
2434
3184
  };
2435
3185
  const reject = (reason) => {
2436
3186
  if (settled || closing)
@@ -2463,6 +3213,9 @@ class ExecutorV1 {
2463
3213
  const handler = actor.layout.handlers.find((handler) => handler.message_id === protocol.message);
2464
3214
  if (!handler)
2465
3215
  throw new Error(`actor ${actor.id} has no message ${protocol.message}`);
3216
+ selectedTarget?.(actor);
3217
+ if (settled || closing)
3218
+ return;
2466
3219
  const full = actor.layout.mailbox_capacity !== undefined &&
2467
3220
  actor.mailbox.length >= actor.layout.mailbox_capacity;
2468
3221
  if (full) {
@@ -2552,6 +3305,114 @@ class ExecutorV1 {
2552
3305
  closeRequest(done);
2553
3306
  };
2554
3307
  }
3308
+ admitActorMessage(actor, message) {
3309
+ if (!actor.alive || actor.closing)
3310
+ return "closed";
3311
+ const full = actor.layout.mailbox_capacity !== undefined &&
3312
+ actor.mailbox.length >= actor.layout.mailbox_capacity;
3313
+ let declared = full ? actor.layout.overflow : null;
3314
+ const discard = (lost) => {
3315
+ actor.disposals.push({
3316
+ kind: "record",
3317
+ typeId: "",
3318
+ fields: lost.payload,
3319
+ });
3320
+ lost.timer?.discarded();
3321
+ this.dispatchActor(actor);
3322
+ };
3323
+ const coalesce = actor.layout.coalesce;
3324
+ if (declared === "coalesce" && coalesce) {
3325
+ const key = coalesce.keys.find((key) => key.message === message.handler.message_id);
3326
+ const queued = key
3327
+ ? actor.mailbox.findIndex((queued) => !queued.obligated &&
3328
+ queued.handler.message_id === message.handler.message_id &&
3329
+ equals(queued.payload[key.param], message.payload[key.param]))
3330
+ : -1;
3331
+ if (queued >= 0) {
3332
+ const superseded = actor.mailbox[queued];
3333
+ actor.mailbox[queued] = message;
3334
+ discard(superseded);
3335
+ return "accepted";
3336
+ }
3337
+ declared = coalesce.fallback;
3338
+ }
3339
+ if (declared === "drop_new") {
3340
+ discard(message);
3341
+ return "discarded";
3342
+ }
3343
+ const oldest = declared === "drop_old"
3344
+ ? actor.mailbox.findIndex((queued) => !queued.obligated)
3345
+ : -1;
3346
+ if (oldest >= 0) {
3347
+ const [evicted] = actor.mailbox.splice(oldest, 1);
3348
+ actor.mailbox.push(message);
3349
+ discard(evicted);
3350
+ return "accepted";
3351
+ }
3352
+ if (full)
3353
+ return "full";
3354
+ actor.mailbox.push(message);
3355
+ this.trace.snapshot("actor.send", {
3356
+ actor_id: actor.id,
3357
+ handler: message.handler.name,
3358
+ });
3359
+ this.dispatchActor(actor);
3360
+ return "accepted";
3361
+ }
3362
+ completeRoot() {
3363
+ this.rootComplete = true;
3364
+ // Quiesce actor timers before draining accepted work; sleeps still wake.
3365
+ for (const actor of this.actors.values())
3366
+ this.cancelActorTimers(actor);
3367
+ }
3368
+ startPeriodicTimers(actor) {
3369
+ if (this.rootComplete || actor.closing)
3370
+ return;
3371
+ for (const handler of actor.layout.handlers) {
3372
+ if (handler.every_ns == null)
3373
+ continue;
3374
+ const interval = Math.floor(handler.every_ns / 1_000_000);
3375
+ let next = this.trace.virtualTimeMs + interval;
3376
+ let disarm = null;
3377
+ const timer = {
3378
+ cancel: () => {
3379
+ disarm?.();
3380
+ disarm = null;
3381
+ actor.timers.delete(timer);
3382
+ },
3383
+ };
3384
+ const fire = () => {
3385
+ disarm = null;
3386
+ if (!actor.timers.has(timer))
3387
+ return;
3388
+ this.admitActorMessage(actor, {
3389
+ caller: { kind: "Timer" },
3390
+ handler,
3391
+ payload: [],
3392
+ reply: false,
3393
+ obligated: false,
3394
+ complete: (_value, _error, drained) => drained?.(null),
3395
+ });
3396
+ if (!actor.timers.has(timer))
3397
+ return;
3398
+ next += interval;
3399
+ if (next <= this.trace.virtualTimeMs)
3400
+ next +=
3401
+ (Math.floor((this.trace.virtualTimeMs - next) / interval) + 1) *
3402
+ interval;
3403
+ arm();
3404
+ };
3405
+ const arm = () => {
3406
+ disarm = this.scheduler.after(BigInt(Math.round((next - this.trace.virtualTimeMs) * 1_000_000)), fire);
3407
+ };
3408
+ actor.timers.add(timer);
3409
+ arm();
3410
+ }
3411
+ }
3412
+ cancelActorTimers(actor) {
3413
+ for (const timer of [...actor.timers])
3414
+ timer.cancel();
3415
+ }
2555
3416
  dispatchActor(actor) {
2556
3417
  if (actor.busy || !actor.alive || actor.drainingMailbox)
2557
3418
  return;
@@ -2587,13 +3448,21 @@ class ExecutorV1 {
2587
3448
  this.dispatchActor(actor);
2588
3449
  return;
2589
3450
  }
2590
- const message = actor.mailbox.shift();
3451
+ const message = actor.mailbox[0];
2591
3452
  if (!message || !actor.alive) {
2592
3453
  actor.busy = false;
2593
3454
  if (actor.closing && actor.alive)
2594
3455
  this.dispatchActor(actor);
2595
3456
  return;
2596
3457
  }
3458
+ // An overdue claim can synchronously begin terminal cleanup. Keep the
3459
+ // payload in its mailbox until that claim succeeds.
3460
+ if (message.timer && !message.timer.started()) {
3461
+ actor.busy = false;
3462
+ this.dispatchActor(actor);
3463
+ return;
3464
+ }
3465
+ actor.mailbox.shift();
2597
3466
  for (const admit of actor.admission.splice(0))
2598
3467
  admit();
2599
3468
  if (message.requestDeadline !== undefined &&
@@ -2623,7 +3492,7 @@ class ExecutorV1 {
2623
3492
  const drained = (fault) => {
2624
3493
  actor.busy = false;
2625
3494
  if (fault)
2626
- this.crashActor(actor, fault);
3495
+ this.crashActor(actor, fault, message.handler);
2627
3496
  else
2628
3497
  this.dispatchActor(actor);
2629
3498
  };
@@ -2639,8 +3508,9 @@ class ExecutorV1 {
2639
3508
  else {
2640
3509
  this.crashActor(actor, {
2641
3510
  kind: "panic",
3511
+ handlerFailed: true,
2642
3512
  message: "unhandled declared handler failure",
2643
- });
3513
+ }, message.handler);
2644
3514
  return;
2645
3515
  }
2646
3516
  }
@@ -2667,7 +3537,7 @@ class ExecutorV1 {
2667
3537
  this.dispatchActor(actor);
2668
3538
  }
2669
3539
  else {
2670
- this.crashActor(actor, fault);
3540
+ this.crashActor(actor, fault, message.handler);
2671
3541
  message.complete(null, "Trapped");
2672
3542
  }
2673
3543
  },
@@ -2676,7 +3546,12 @@ class ExecutorV1 {
2676
3546
  this.runFrame(frame);
2677
3547
  });
2678
3548
  }
2679
- crashActor(actor, fault) {
3549
+ crashActor(actor, fault, handler) {
3550
+ this.reportFault(fault);
3551
+ if (handler) {
3552
+ const context = `${actor.layout.name}.${handler.name}`;
3553
+ this.trace.writeStderr(`hew: actor crash in ${context} (actor ${actor.identity}): ${faultInfo(fault)[1]}\n`, null);
3554
+ }
2680
3555
  const debt = ++this.nextFaultDebt;
2681
3556
  this.faultDebts.add(debt);
2682
3557
  if (actor.supervisor)
@@ -2684,6 +3559,7 @@ class ExecutorV1 {
2684
3559
  actor.crashing ??= fault;
2685
3560
  actor.alive = false;
2686
3561
  actor.busy = false;
3562
+ this.cancelActorTimers(actor);
2687
3563
  this.trace.snapshot("actor.crash", {
2688
3564
  actor_id: actor.id,
2689
3565
  message: this.faultText(fault),
@@ -2693,47 +3569,114 @@ class ExecutorV1 {
2693
3569
  actor.mailboxDrained.push(() => finished(action));
2694
3570
  return;
2695
3571
  }
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);
3572
+ this.closeValueAsync(actor.state, this.faultText(fault), (stateFault) => {
3573
+ actor.closingFault = combineFaults(actor.closingFault, stateFault);
3574
+ fault = combineFaults(fault, stateFault);
3575
+ actor.crashing = fault;
3576
+ this.reportFault(fault);
3577
+ this.drainMailbox(actor, this.faultText(fault), (payloadFault) => {
3578
+ actor.closingFault = combineFaults(actor.closingFault, payloadFault);
3579
+ fault = combineFaults(fault, payloadFault);
3580
+ actor.crashing = fault;
3581
+ this.reportFault(fault);
3582
+ this.sweepRcOwner(actor.rcOwner, actor, (sweepFault) => {
3583
+ actor.closingFault = combineFaults(actor.closingFault, sweepFault);
3584
+ actor.crashing = combineFaults(actor.crashing, sweepFault);
3585
+ this.reportFault(actor.crashing);
3586
+ if (actor.closingFault) {
3587
+ this.reportFault(actor.closingFault);
3588
+ }
3589
+ actor.completed = true;
3590
+ for (const wake of actor.closed.splice(0))
3591
+ wake();
3592
+ for (const admit of actor.admission.splice(0))
3593
+ admit();
3594
+ if (actor.supervisor) {
3595
+ const { owner, child } = actor.supervisor;
3596
+ if (action === "Restart")
3597
+ this.restartChild(owner, child, [debt]);
3598
+ else if (action === "Escalate")
3599
+ this.escalateSupervisor(owner, [debt]);
3600
+ else {
3601
+ owner.spent.add(child);
3602
+ owner.recovering.delete(child);
3603
+ this.wakeRoles();
3604
+ }
3605
+ }
3606
+ });
3607
+ });
3608
+ }, actor);
2715
3609
  };
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");
3610
+ const runHook = () => {
3611
+ if (actor.layout.crash !== undefined) {
3612
+ const [code, message] = faultInfo(fault);
3613
+ const info = {
3614
+ kind: "record",
3615
+ typeId: this.aggregateName(actor.layout.crash_info),
3616
+ shape: actor.layout.crash_info,
3617
+ fields: [
3618
+ {
3619
+ kind: "enum",
3620
+ typeId: this.variantName(actor.layout.crash_kind),
3621
+ shape: actor.layout.crash_kind,
3622
+ tag: runtimeTag(this.pkg.variants[actor.layout.crash_kind], code === 214
3623
+ ? "CrashKindTimerOverdue"
3624
+ : code === 213
3625
+ ? "CrashKindHandlerFailed"
3626
+ : "CrashKindFault"),
3627
+ payload: [],
3628
+ },
3629
+ stringValue(String(message)),
3630
+ ],
3631
+ };
3632
+ let cancelTimer;
3633
+ const complete = (action) => {
3634
+ cancelTimer?.();
3635
+ actor.terminalHook = null;
3636
+ finished(action);
3637
+ };
3638
+ const hook = this.invokeFrame(actor, actor.layout.crash, [actor.state, info], (value) => {
3639
+ const shape = this.pkg.variants[actor.layout.crash_action];
3640
+ const action = value.kind === "enum" ? shape?.cases[value.tag]?.name : undefined;
3641
+ if (!action)
3642
+ throw new Error("crash hook returned no declared action");
3643
+ complete(action);
3644
+ }, (hookFault) => {
3645
+ actor.closingFault = combineFaults(actor.closingFault, hookFault);
3646
+ fault = combineFaults(fault, hookFault);
3647
+ actor.crashing = fault;
3648
+ this.reportFault(fault);
3649
+ complete("Restart");
3650
+ });
3651
+ actor.terminalHook = hook;
3652
+ const parent = actor.supervisor;
3653
+ const stopNs = parent
3654
+ ? this.childStopBudget(parent.owner, parent.child)
3655
+ : DEFAULT_ACTOR_STOP_NS;
3656
+ hook.requestDeadline = this.trace.virtualTimeMs + stopNs / 1_000_000;
3657
+ const expire = () => {
3658
+ hook.pendingCancellation ??= {
3659
+ kind: "panic",
3660
+ message: "crash hook deadline elapsed",
3661
+ deadline: true,
3662
+ };
3663
+ hook.cancel?.(hook.pendingCancellation);
3664
+ };
3665
+ if (stopNs <= 0)
3666
+ expire();
3667
+ else
3668
+ cancelTimer = this.scheduler.after(BigInt(stopNs), expire);
3669
+ }
3670
+ else
3671
+ finished("Restart");
3672
+ };
3673
+ this.drainMailbox(actor, this.faultText(fault), (cleanupFault) => {
3674
+ actor.closingFault = combineFaults(actor.closingFault, cleanupFault);
3675
+ fault = combineFaults(fault, cleanupFault);
3676
+ actor.crashing = fault;
3677
+ this.reportFault(fault);
3678
+ runHook();
3679
+ });
2737
3680
  }
2738
3681
  supervisorFor(value) {
2739
3682
  if (value.kind !== "supervisor")
@@ -2786,6 +3729,23 @@ class ExecutorV1 {
2786
3729
  return (!!owner?.alive &&
2787
3730
  (owner.recovering.has(role.child) || owner.children[role.child] === null));
2788
3731
  }
3732
+ roleRestartPending(role) {
3733
+ const parent = this.roles.get(role.ownerId);
3734
+ if (parent && this.roleRestartPending(parent))
3735
+ return true;
3736
+ if (this.rolePending(role))
3737
+ return true;
3738
+ const value = this.roleValue(role) ?? role.latest;
3739
+ if (value?.kind === "actor") {
3740
+ const actor = this.actorFor(value);
3741
+ return !actor.completed && (actor.closing || !actor.alive);
3742
+ }
3743
+ if (value?.kind === "supervisor") {
3744
+ const owner = this.supervisorFor(value);
3745
+ return !owner.completed && !owner.alive;
3746
+ }
3747
+ return false;
3748
+ }
2789
3749
  wakeRoles() {
2790
3750
  for (const role of this.roles.values()) {
2791
3751
  this.roleValue(role);
@@ -2918,6 +3878,16 @@ class ExecutorV1 {
2918
3878
  else if (value.kind === "supervisor")
2919
3879
  this.stopSupervisor(this.supervisorFor(value), true);
2920
3880
  }
3881
+ childStopBudget(owner, child) {
3882
+ const spec = owner.specs[child];
3883
+ if (spec.stop_config !== undefined) {
3884
+ const duration = owner.config[spec.stop_config];
3885
+ if (duration?.kind !== "i64" || duration.value < 0n)
3886
+ throw new Error("child stop budget is not a non-negative duration");
3887
+ return Number(duration.value);
3888
+ }
3889
+ return (spec.stop_ns ?? ("actor" in spec.role ? DEFAULT_ACTOR_STOP_NS : null));
3890
+ }
2921
3891
  stopSupervisor(owner, force = false) {
2922
3892
  if (force) {
2923
3893
  owner.terminating = true;
@@ -2937,12 +3907,7 @@ class ExecutorV1 {
2937
3907
  const index = childIndex--;
2938
3908
  const child = owner.children[index];
2939
3909
  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;
3910
+ const stopNs = this.childStopBudget(owner, index);
2946
3911
  if (!owner.terminating && stopNs !== null && stopNs > 0) {
2947
3912
  owner.teardownCancelTimer = this.scheduler.after(BigInt(stopNs), () => this.terminateChild(child));
2948
3913
  }
@@ -2950,9 +3915,11 @@ class ExecutorV1 {
2950
3915
  return;
2951
3916
  }
2952
3917
  this.closeValueAsync({ kind: "record", typeId: "", fields: owner.config }, null, (fault) => {
2953
- if (fault)
2954
- this.faultDebts.add(++this.nextFaultDebt);
3918
+ const debt = fault ? ++this.nextFaultDebt : undefined;
3919
+ if (debt !== undefined)
3920
+ this.faultDebts.add(debt);
2955
3921
  owner.completed = true;
3922
+ this.settleSupervisedEnd(owner, fault ? "Crashed" : owner.terminating ? "Terminated" : "Stopped", debt === undefined ? [] : [debt]);
2956
3923
  for (const wake of owner.closed.splice(0))
2957
3924
  wake();
2958
3925
  this.wakeRoles();
@@ -2987,6 +3954,15 @@ class ExecutorV1 {
2987
3954
  if (actor.closing || actor.completed)
2988
3955
  return;
2989
3956
  actor.closing = true;
3957
+ this.cancelActorTimers(actor);
3958
+ if (actor.constructing && actor.active) {
3959
+ actor.active.pendingCancellation ??= {
3960
+ kind: "panic",
3961
+ message: "actor construction stopped",
3962
+ cancelled: true,
3963
+ };
3964
+ actor.active.cancel?.(actor.active.pendingCancellation);
3965
+ }
2990
3966
  // Admission closes immediately. The active turn and every accepted queued
2991
3967
  // turn finish before stop hooks and state release.
2992
3968
  this.dispatchActor(actor);
@@ -2998,8 +3974,17 @@ class ExecutorV1 {
2998
3974
  return;
2999
3975
  actor.closing = true;
3000
3976
  actor.terminating = true;
3001
- actor.active?.cancel?.();
3002
- actor.stopHook?.cancel?.();
3977
+ this.cancelActorTimers(actor);
3978
+ for (const context of [actor.active, actor.terminalHook]) {
3979
+ if (!context)
3980
+ continue;
3981
+ context.pendingCancellation ??= {
3982
+ kind: "panic",
3983
+ message: "actor terminated",
3984
+ cancelled: true,
3985
+ };
3986
+ context.cancel?.(context.pendingCancellation);
3987
+ }
3003
3988
  this.dispatchActor(actor);
3004
3989
  for (const admit of actor.admission.splice(0))
3005
3990
  admit();
@@ -3010,11 +3995,11 @@ class ExecutorV1 {
3010
3995
  const next = () => {
3011
3996
  const hook = actor.terminating ? undefined : hooks.shift();
3012
3997
  if (hook !== undefined) {
3013
- actor.stopHook = this.invokeFrame(actor, hook, [actor.state], () => {
3014
- actor.stopHook = null;
3998
+ actor.terminalHook = this.invokeFrame(actor, hook, [actor.state], () => {
3999
+ actor.terminalHook = null;
3015
4000
  next();
3016
4001
  }, (fault) => {
3017
- actor.stopHook = null;
4002
+ actor.terminalHook = null;
3018
4003
  if (!(actor.terminating && fault.kind === "panic" && fault.cancelled))
3019
4004
  actor.closingFault = combineFaults(actor.closingFault, fault);
3020
4005
  hooks.length = 0;
@@ -3024,31 +4009,37 @@ class ExecutorV1 {
3024
4009
  else {
3025
4010
  this.closeValueAsync(actor.state, this.faultText(actor.closingFault), (fault) => {
3026
4011
  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();
4012
+ this.sweepRcOwner(actor.rcOwner, actor, (sweepFault) => {
4013
+ actor.closingFault = combineFaults(actor.closingFault, sweepFault);
4014
+ // Stopped cleanup belongs to the completion barrier, independently
4015
+ // of handler-crash debt and supervisor restart decisions.
4016
+ actor.crashing = null;
4017
+ if (actor.closingFault) {
4018
+ this.reportFault(actor.closingFault);
4019
+ actor.closeFaultDebt = ++this.nextFaultDebt;
4020
+ this.faultDebts.add(actor.closeFaultDebt);
4021
+ }
4022
+ actor.completed = true;
4023
+ actor.alive = false;
4024
+ actor.busy = false;
4025
+ this.trace.snapshot(actor.terminating ? "actor.terminate" : "actor.stop", { actor_id: actor.id });
4026
+ this.settleSupervisedEnd(actor, actor.closingFault
4027
+ ? "Crashed"
4028
+ : actor.terminating
4029
+ ? "Terminated"
4030
+ : "Stopped", actor.closeFaultDebt === undefined
4031
+ ? []
4032
+ : [actor.closeFaultDebt]);
4033
+ for (const wake of actor.closed.splice(0))
4034
+ wake();
4035
+ });
3045
4036
  }, actor);
3046
4037
  }
3047
4038
  };
3048
4039
  next();
3049
4040
  }
3050
- settleSupervisedEnd(actor, reason) {
3051
- const parent = actor.supervisor;
4041
+ settleSupervisedEnd(subject, reason, debts) {
4042
+ const parent = subject.supervisor;
3052
4043
  if (!parent ||
3053
4044
  !parent.owner.alive ||
3054
4045
  parent.owner.recovering.has(parent.child))
@@ -3057,7 +4048,7 @@ class ExecutorV1 {
3057
4048
  const policy = owner.specs[child].restart;
3058
4049
  if (policy === "permanent" ||
3059
4050
  (policy === "transient" && reason !== "Stopped")) {
3060
- this.restartChild(owner, child, actor.closeFaultDebt === undefined ? [] : [actor.closeFaultDebt]);
4051
+ this.restartChild(owner, child, debts);
3061
4052
  }
3062
4053
  else {
3063
4054
  owner.spent.add(child);
@@ -3065,6 +4056,16 @@ class ExecutorV1 {
3065
4056
  }
3066
4057
  }
3067
4058
  // ── exit and faults ──────────────────────────────────────────────────────
4059
+ reportFault(fault) {
4060
+ const diagnostics = [fault.primary ?? fault, ...(fault.secondary ?? [])];
4061
+ diagnostics.forEach((diagnostic, index) => {
4062
+ const origin = diagnostic.primary ?? diagnostic;
4063
+ if (origin.reported)
4064
+ return;
4065
+ this.trace.writeStderr(faultDiagnostic(origin, index !== 0), null);
4066
+ origin.reported = true;
4067
+ });
4068
+ }
3068
4069
  haltWithFault(fault) {
3069
4070
  const isPanic = fault.kind === "panic";
3070
4071
  const status = isPanic ? "panic" : "trap";
@@ -3074,7 +4075,7 @@ class ExecutorV1 {
3074
4075
  : (fault.message ?? trapMessage(fault.trap));
3075
4076
  const [code, kind] = faultInfo(fault);
3076
4077
  // Native writes the failure line to stderr before it exits; so does this.
3077
- this.trace.writeStderr(faultReport(fault), null);
4078
+ this.reportFault(fault);
3078
4079
  // A failing run reports no exit code: native exits 1 for every fault and
3079
4080
  // names the kind in its message, so the kind is what travels, in
3080
4081
  // `runtime_failures`. The page turns it into an exit code of its own.
@@ -3114,6 +4115,12 @@ function combineFaults(primary, secondary) {
3114
4115
  }
3115
4116
  function faultInfo(fault) {
3116
4117
  if (fault.kind === "panic") {
4118
+ if (fault.handlerFailed)
4119
+ return [213, "ActorUnhandledFailure"];
4120
+ if (fault.timerOverdue)
4121
+ return [214, "ActorTimerOverdue"];
4122
+ if (fault.actorSendFailed)
4123
+ return [206, "ActorSendFailed"];
3117
4124
  if (fault.cancelled)
3118
4125
  return [-1, "Cancelled"];
3119
4126
  if (fault.deadline)
@@ -3128,11 +4135,18 @@ function faultInfo(fault) {
3128
4135
  actor_refused: [-4, "Refused"],
3129
4136
  }[fault.trap];
3130
4137
  }
3131
- function faultReport(fault, secondary = false) {
4138
+ function faultDiagnostic(fault, secondary = false) {
3132
4139
  const [code, reason] = faultInfo(fault);
3133
4140
  const label = secondary ? "secondary failure" : "failure";
3134
- const message = fault.message === undefined ? "" : `: ${fault.message}`;
3135
- return (`hew: ${label}: ${reason} (${code})${message}\n` +
4141
+ const message = fault.message === undefined ||
4142
+ (fault.kind === "panic" &&
4143
+ (fault.cancelled || fault.deadline || fault.timerOverdue))
4144
+ ? ""
4145
+ : `: ${fault.message}`;
4146
+ return `hew: ${label}: ${reason} (${code})${message}\n`;
4147
+ }
4148
+ function faultReport(fault, secondary = false) {
4149
+ return (faultDiagnostic(fault, secondary) +
3136
4150
  (fault.secondary ?? []).map((next) => faultReport(next, true)).join(""));
3137
4151
  }
3138
4152
  // ── refs ────────────────────────────────────────────────────────────────────
@@ -3286,7 +4300,9 @@ function toComparable(value) {
3286
4300
  case "vector":
3287
4301
  return value.items.map(toComparable);
3288
4302
  case "rc":
3289
- return toComparable(value.cell.value);
4303
+ return "Rc";
4304
+ case "weak":
4305
+ return "Weak";
3290
4306
  case "map":
3291
4307
  return [...value.entries.values()].map((entry) => [
3292
4308
  toComparable(entry.key),
@@ -3459,7 +4475,7 @@ function applyBinary(op, lhs, rhs, ty) {
3459
4475
  };
3460
4476
  }
3461
4477
  if (lhs.kind === "string") {
3462
- return { kind: "string", value: lhs.value + asString(rhs) };
4478
+ return stringValue(lhs.value + asString(rhs));
3463
4479
  }
3464
4480
  return {
3465
4481
  kind: "i64",
@@ -3525,7 +4541,7 @@ function applyCast(value, to) {
3525
4541
  return { kind: "bool", value: truth(value) };
3526
4542
  case "char":
3527
4543
  case "string":
3528
- return { kind: "string", value: asString(value) };
4544
+ return stringValue(asString(value));
3529
4545
  default:
3530
4546
  break;
3531
4547
  }