@hew-lang/sandbox-vm 0.6.0 → 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,12 +95,16 @@ 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;
107
+ actorIdentity = 0n;
101
108
  faultDebts = new Set();
102
109
  nextFaultDebt = 0;
103
110
  closingResources = new WeakSet();
@@ -113,7 +120,35 @@ class ExecutorV1 {
113
120
  this.scheduler = new FrameScheduler(trace, policy);
114
121
  this.ids = new DeterministicIds(trace.replay.seed);
115
122
  this.host = {
123
+ argv: [...(options.argv ?? ["main.hew"])],
116
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
+ },
117
152
  newPipe: (capacity) => this.newPipe(capacity),
118
153
  closePipe: (value) => this.pipes.close(value, this.pipeFault(this.current)),
119
154
  releaseValue: (value) => this.closeValue(value, this.pipeFault(this.current)),
@@ -134,7 +169,7 @@ class ExecutorV1 {
134
169
  id: "actor:root",
135
170
  actor: null,
136
171
  returned: (value) => {
137
- this.rootComplete = true;
172
+ this.completeRoot();
138
173
  this.trace.exitCode =
139
174
  pkg.entry?.exit === "status" && value.kind === "i64"
140
175
  ? Number(value.value)
@@ -142,7 +177,7 @@ class ExecutorV1 {
142
177
  },
143
178
  failed: (fault) => {
144
179
  this.rootFault = fault;
145
- this.rootComplete = true;
180
+ this.completeRoot();
146
181
  },
147
182
  });
148
183
  }
@@ -172,6 +207,10 @@ class ExecutorV1 {
172
207
  this.running = true;
173
208
  while (this.running) {
174
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
+ };
175
214
  while (act.opIndex < act.block.ops.length) {
176
215
  const op = act.block.ops[act.opIndex++];
177
216
  if (op.op !== "coverage.hit")
@@ -202,9 +241,19 @@ class ExecutorV1 {
202
241
  }
203
242
  // ── frames ───────────────────────────────────────────────────────────────
204
243
  activate(fn, args, caller, result, normal, unwind, context) {
205
- if ((this.pkg.resources ?? []).some((resource) => resource.close === fn.id) &&
206
- 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])
207
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;
208
257
  const env = new Map();
209
258
  for (const [at, param] of fn.params.entries()) {
210
259
  env.set(param.value, ownedRef(args[at] ?? UNIT));
@@ -230,7 +279,13 @@ class ExecutorV1 {
230
279
  arm.fields.forEach(record);
231
280
  }
232
281
  return {
233
- 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,
234
289
  fn,
235
290
  block: this.blockAt(fn, fn.entry),
236
291
  opIndex: 0,
@@ -372,7 +427,7 @@ class ExecutorV1 {
372
427
  this.define(act, op.dst, { kind: "f64", value: floatConst(op) });
373
428
  return;
374
429
  case "const.char":
375
- this.define(act, op.dst, { kind: "string", value: op.value });
430
+ this.define(act, op.dst, stringValue(op.value));
376
431
  return;
377
432
  case "const.unit":
378
433
  this.define(act, op.dst, UNIT);
@@ -381,10 +436,7 @@ class ExecutorV1 {
381
436
  this.define(act, op.dst, { kind: "i64", value: BigInt(op.nanos) });
382
437
  return;
383
438
  case "const.str":
384
- this.define(act, op.dst, {
385
- kind: "string",
386
- value: this.pkg.strings[op.str] ?? "",
387
- });
439
+ this.define(act, op.dst, stringValue(this.pkg.strings[op.str] ?? ""));
388
440
  return;
389
441
  case "const.bytes":
390
442
  this.define(act, op.dst, bytesValue(this.pkg.bytes[op.bytes] ?? []));
@@ -550,7 +602,8 @@ class ExecutorV1 {
550
602
  const shape = this.closureAt(op.closure);
551
603
  this.define(act, op.dst, {
552
604
  kind: "closure",
553
- body: shape.body,
605
+ entries: shape.entries,
606
+ specialization: null,
554
607
  environment: {
555
608
  kind: "record",
556
609
  typeId: "",
@@ -559,12 +612,23 @@ class ExecutorV1 {
559
612
  });
560
613
  return;
561
614
  }
562
- case "callable.coerce":
563
- // A coercion only weakens the callable's proved capabilities; SIR
564
- // refuses one that changes an argument, a result or a capture shape,
565
- // so the value itself is unchanged.
566
- this.define(act, op.dst, this.read(act, op.source));
615
+ case "callable.coerce": {
616
+ const callable = this.read(act, op.source);
617
+ if (op.specialization === null) {
618
+ this.define(act, op.dst, callable);
619
+ }
620
+ else {
621
+ if (callable.kind !== "closure" ||
622
+ !callable.entries.some((entry) => entry.specialization === op.specialization)) {
623
+ throw new Error("callable specialization has no checked invocation entry");
624
+ }
625
+ this.define(act, op.dst, {
626
+ ...callable,
627
+ specialization: op.specialization,
628
+ });
629
+ }
567
630
  return;
631
+ }
568
632
  case "dyn.make":
569
633
  this.define(act, op.dst, {
570
634
  kind: "dyn",
@@ -595,6 +659,9 @@ class ExecutorV1 {
595
659
  const duration = this.read(act, op.duration);
596
660
  if (duration.kind !== "i64")
597
661
  throw new Error("scope duration is not a duration");
662
+ group.expiresAt =
663
+ this.trace.virtualTimeMs +
664
+ Math.max(0, Number(duration.value) / 1_000_000);
598
665
  group.cancelTimer = this.scheduler.after(duration.value, () => {
599
666
  group.deadline = {
600
667
  kind: "panic",
@@ -636,7 +703,9 @@ class ExecutorV1 {
636
703
  const context = {
637
704
  id,
638
705
  actor: act.context.actor,
639
- returned: (value) => this.closeValueAsync(value, null, (fault) => this.finishGenerator(generator, fault)),
706
+ rcOwner: act.rcOwner,
707
+ requestDeadline: this.requestDeadline(act),
708
+ returned: (value) => this.closeValueAsync(value, null, (fault) => this.finishGenerator(generator, fault), act.context.actor, act.rcOwner),
640
709
  failed: (fault) => this.finishGenerator(generator, fault),
641
710
  };
642
711
  const generator = {
@@ -731,10 +800,10 @@ class ExecutorV1 {
731
800
  changed();
732
801
  };
733
802
  if (failure && winner)
734
- this.closeValueAsync(value, this.faultText(failure), finish, act.context.actor);
803
+ this.closeValueAsync(value, this.faultText(failure), finish, act.context.actor, act.rcOwner);
735
804
  else
736
805
  finish();
737
- }, act.context.actor);
806
+ }, act.context.actor, act.rcOwner);
738
807
  };
739
808
  task.cancel = () => {
740
809
  if (task.done || task.finishing)
@@ -753,12 +822,44 @@ class ExecutorV1 {
753
822
  ready();
754
823
  return;
755
824
  }
756
- case "task.spawn": {
825
+ case "task.spawn":
826
+ case "task.invoke": {
757
827
  const group = act.scopes.get(op.scope);
758
828
  if (!group)
759
829
  throw new Error(`task scope ${op.scope} is missing`);
760
- const callable = this.read(act, op.callable);
761
- 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;
762
863
  const task = this.newTask();
763
864
  group.tasks.push(task);
764
865
  const settle = (value, fault) => {
@@ -769,9 +870,7 @@ class ExecutorV1 {
769
870
  !(fault.kind === "panic" && (fault.cancelled || fault.deadline)) &&
770
871
  act.context.actor)
771
872
  act.context.actor.crashing ??= fault;
772
- this.closeValueAsync(callable, act.context.actor
773
- ? this.faultText(act.context.actor.crashing)
774
- : null, (closeFault) => {
873
+ const complete = (closeFault) => {
775
874
  if (!fault || (fault.kind === "panic" && fault.cancelled))
776
875
  fault = closeFault ?? fault;
777
876
  this.settleTask(task, value, fault);
@@ -786,32 +885,92 @@ class ExecutorV1 {
786
885
  }
787
886
  for (const changed of [...group.changed])
788
887
  changed();
789
- });
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
+ }
790
926
  };
791
927
  const context = {
792
928
  id: task.id,
793
929
  actor: act.context.actor,
794
- returned: (value) => settle(value, null),
795
- failed: (fault) => settle(UNIT, fault),
930
+ rcOwner: act.rcOwner,
931
+ requestDeadline: this.requestDeadline(act),
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
+ },
796
959
  };
797
- const frame = this.activate(this.functionAt(calleeFunction(callable)), callable.kind === "closure" ? [callable.environment] : [], null, null, undefined, null, context);
798
- 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);
799
961
  task.cancel = () => {
800
962
  if (task.done || task.finishing)
801
963
  return;
802
- if (started)
803
- context.cancel?.();
804
- else
805
- settle(UNIT, {
806
- kind: "panic",
807
- message: "task cancelled",
808
- cancelled: true,
809
- });
964
+ context.pendingCancellation ??= {
965
+ kind: "panic",
966
+ message: "task cancelled",
967
+ cancelled: true,
968
+ };
969
+ context.cancel?.(context.pendingCancellation);
810
970
  };
811
971
  this.scheduler.enqueue(task.id, () => {
812
972
  if (task.done || task.finishing)
813
973
  return;
814
- started = true;
815
974
  this.runFrame(frame);
816
975
  });
817
976
  this.trace.allocateId("task", task.id, act.context.id);
@@ -888,14 +1047,7 @@ class ExecutorV1 {
888
1047
  if (receiver.kind !== "dyn") {
889
1048
  throw new TypeError(`dyn.call expected a trait object, got ${receiver.kind}`);
890
1049
  }
891
- const vtable = this.pkg.vtables[receiver.vtable] ??
892
- this.pkg.vtables.find((candidate) => candidate.id === receiver.vtable);
893
- // The slot number is the checker's own index, so it is matched rather
894
- // than used as a position in the slot array.
895
- const entry = vtable?.slots.find((candidate) => candidate.slot === term.slot);
896
- if (!entry) {
897
- throw new Error(`vtable ${receiver.vtable} has no slot ${term.slot}`);
898
- }
1050
+ const entry = this.dynamicEntry(receiver, term.slot);
899
1051
  // The wrapped value is the method's receiver, ahead of the call's own
900
1052
  // arguments, passed under the decision the slot records: the erasure
901
1053
  // the concrete type was made under, not something read off the method.
@@ -920,7 +1072,7 @@ class ExecutorV1 {
920
1072
  // `self`. A plain function value carries none.
921
1073
  const callee = this.boundary(act, term.callee);
922
1074
  const args = term.args.map((operand) => this.boundary(act, operand));
923
- this.current = this.activate(this.functionAt(calleeFunction(callee)), callee.kind === "closure" ? [callee.environment, ...args] : args, act, term.result, term.normal, term.unwind);
1075
+ this.current = this.activate(this.functionAt(calleeFunction(callee, term.specialization)), callee.kind === "closure" ? [callee.environment, ...args] : args, act, term.result, term.normal, term.unwind);
924
1076
  return;
925
1077
  }
926
1078
  case "actor.call":
@@ -928,6 +1080,38 @@ class ExecutorV1 {
928
1080
  return;
929
1081
  case "runtime.call": {
930
1082
  const entry = this.pkg.runtime_families[term.family];
1083
+ if (entry?.family === "ActorCaller") {
1084
+ const caller = act.context.caller ?? { kind: "Task" };
1085
+ const payload = [];
1086
+ if (caller.kind === "Actor") {
1087
+ const shape = this.pkg.variants[term.result_shape];
1088
+ const tag = runtimeTag(shape, "CallerActor");
1089
+ payload.push({
1090
+ kind: "record",
1091
+ typeId: shape.cases[tag].fields[0],
1092
+ fields: [{ kind: "i64", value: caller.identity }],
1093
+ });
1094
+ }
1095
+ this.completeShim(act, term, this.variant(term.result_shape, caller.kind === "Actor"
1096
+ ? "CallerActor"
1097
+ : caller.kind === "Timer"
1098
+ ? "CallerTimer"
1099
+ : "CallerTask", payload));
1100
+ return;
1101
+ }
1102
+ if (entry?.family === "ActorRefusalLog") {
1103
+ const [message, handler, reason] = term.args.map((argument) => this.boundary(act, argument));
1104
+ this.trace.snapshot("actor.delivery_refused", {
1105
+ actor_id: act.context.actor?.id ?? null,
1106
+ handler: toJsonValue(handler),
1107
+ message: toJsonValue(message),
1108
+ reason: toJsonValue(reason),
1109
+ });
1110
+ if (!act.context.reply)
1111
+ this.trace.writeStderr(`WARN actor.delivery_refused actor=${act.context.actor?.identity ?? 0n} handler=${asString(handler)} reason=${JSON.stringify(asString(reason))}\n`, null);
1112
+ this.completeShim(act, term, UNIT);
1113
+ return;
1114
+ }
931
1115
  if (entry &&
932
1116
  (entry.family === "Map" ||
933
1117
  entry.family === "Set" ||
@@ -943,6 +1127,9 @@ class ExecutorV1 {
943
1127
  [
944
1128
  "RcNew",
945
1129
  "RcClone",
1130
+ "RcDowngrade",
1131
+ "WeakCloneRc",
1132
+ "WeakUpgradeRc",
946
1133
  "RcGet",
947
1134
  "RcIsUnique",
948
1135
  "RcSet",
@@ -950,7 +1137,7 @@ class ExecutorV1 {
950
1137
  "RcWeakCount",
951
1138
  ].includes(entry.family)) {
952
1139
  const args = term.args.map((operand) => this.boundary(act, operand));
953
- 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] : [])));
954
1141
  return;
955
1142
  }
956
1143
  if (entry?.family === "SupervisorPool") {
@@ -966,13 +1153,40 @@ class ExecutorV1 {
966
1153
  else
967
1154
  this.completeShim(act, term, UNIT);
968
1155
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
969
- });
1156
+ }, act.context.actor, act.rcOwner);
970
1157
  return;
971
1158
  }
972
1159
  if (entry?.family === "StructuralFormat") {
973
1160
  this.structuralFormat(act, term);
974
1161
  return;
975
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
+ }
976
1190
  const shim = entry ? resolveRuntimeShim(entry) : undefined;
977
1191
  if (!entry || !shim) {
978
1192
  throw new Error(`runtime family ${term.family} resolved no shim after admission`);
@@ -1031,6 +1245,7 @@ class ExecutorV1 {
1031
1245
  this.takeEdge(act, act.fault ? term.fault : term.normal);
1032
1246
  return;
1033
1247
  case "enter_defer":
1248
+ act.context.pollSuppression = (act.context.pollSuppression ?? 0) + 1;
1034
1249
  // The deferred body runs on its own fault state: a body reached while
1035
1250
  // unwinding must not read the fault it is cleaning up after.
1036
1251
  act.parked.push(act.fault);
@@ -1038,6 +1253,7 @@ class ExecutorV1 {
1038
1253
  this.takeEdge(act, term.body);
1039
1254
  return;
1040
1255
  case "finish_defer": {
1256
+ act.context.pollSuppression = (act.context.pollSuppression ?? 0) - 1;
1041
1257
  // Combine the parked fault with whatever the body left, keeping the
1042
1258
  // first: a join must never forget the fault the boundary carried in.
1043
1259
  const parked = act.parked.pop();
@@ -1067,12 +1283,7 @@ class ExecutorV1 {
1067
1283
  typeId: shape.name,
1068
1284
  shape: shape.id,
1069
1285
  tag: deadline ? term.deadline_variant : term.fault_variant,
1070
- payload: [
1071
- {
1072
- kind: "string",
1073
- value: faultReport(fault),
1074
- },
1075
- ],
1286
+ payload: [stringValue(faultReport(fault))],
1076
1287
  };
1077
1288
  act.fault = null;
1078
1289
  if (act.context.actor)
@@ -1167,7 +1378,7 @@ class ExecutorV1 {
1167
1378
  this.keepInputs(act, term, args);
1168
1379
  this.raiseFault(act, fault, term.unwind);
1169
1380
  resume();
1170
- });
1381
+ }, act.context.actor, act.rcOwner);
1171
1382
  };
1172
1383
  const next = (value = UNIT) => {
1173
1384
  let step;
@@ -1185,6 +1396,9 @@ class ExecutorV1 {
1185
1396
  this.completeShim(act, term, step.value);
1186
1397
  resume();
1187
1398
  }
1399
+ else if (step.value.kind === "fault") {
1400
+ fail({ kind: "panic", message: step.value.message });
1401
+ }
1188
1402
  else if (step.value.kind === "release") {
1189
1403
  const request = step.value;
1190
1404
  this.closeValueAsync(request.value, this.pipeFault(act), (fault) => {
@@ -1198,11 +1412,11 @@ class ExecutorV1 {
1198
1412
  act.fault ??= fault;
1199
1413
  }
1200
1414
  next();
1201
- });
1415
+ }, act.context.actor, act.rcOwner);
1202
1416
  }
1203
1417
  else {
1204
1418
  const request = step.value;
1205
- 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);
1206
1420
  act.context.cancel = (fault) => child.cancel?.(fault);
1207
1421
  }
1208
1422
  };
@@ -1211,8 +1425,17 @@ class ExecutorV1 {
1211
1425
  }
1212
1426
  supervisorPool(act, term, operation) {
1213
1427
  const [view, index] = term.args.map((arg) => this.boundary(act, arg));
1214
- if (view?.kind !== "vector" || index?.kind !== "i64")
1215
- 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");
1216
1439
  const value = view.items[Number(index.value)];
1217
1440
  if (operation === "Get") {
1218
1441
  this.completeShim(act, term, this.variant(term.result_shape, value ? "OptionSome" : "OptionNone", value ? [value] : []));
@@ -1232,7 +1455,7 @@ class ExecutorV1 {
1232
1455
  const continueRendering = () => {
1233
1456
  const next = renderer.next();
1234
1457
  if (typeof next === "string") {
1235
- this.completeShim(act, term, { kind: "string", value: next });
1458
+ this.completeShim(act, term, stringValue(next));
1236
1459
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1237
1460
  return;
1238
1461
  }
@@ -1257,7 +1480,92 @@ class ExecutorV1 {
1257
1480
  }
1258
1481
  this.takeEdge(act, term.normal);
1259
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
+ }
1260
1566
  returnFrom(act, value) {
1567
+ if (this.finishConstruction(act, () => this.returnFrom(act, value)))
1568
+ return;
1261
1569
  const caller = act.caller;
1262
1570
  if (!caller) {
1263
1571
  this.running = false;
@@ -1282,6 +1590,14 @@ class ExecutorV1 {
1282
1590
  this.current = caller;
1283
1591
  this.takeEdge(caller, act.normal);
1284
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
+ }
1285
1601
  /// The wrapped value on its way to a trait method's `self`. A `copy` receiver
1286
1602
  /// deep-clones, a `move` invalidates the trait object it came out of, and a
1287
1603
  /// borrow passes the live value.
@@ -1309,6 +1625,8 @@ class ExecutorV1 {
1309
1625
  this.takeEdge(act, unwind);
1310
1626
  }
1311
1627
  resumeUnwind(act, handback) {
1628
+ if (this.finishConstruction(act, () => this.resumeUnwind(act, handback)))
1629
+ return;
1312
1630
  const fault = act.fault;
1313
1631
  if (!fault) {
1314
1632
  throw new Error(`${act.fn.name}: resume_unwind with no pending fault`);
@@ -1316,7 +1634,7 @@ class ExecutorV1 {
1316
1634
  const caller = act.caller;
1317
1635
  if (!caller) {
1318
1636
  this.running = false;
1319
- act.context.failed(fault);
1637
+ act.context.failed(fault, handback);
1320
1638
  return;
1321
1639
  }
1322
1640
  if (!act.unwind) {
@@ -1386,37 +1704,27 @@ class ExecutorV1 {
1386
1704
  return;
1387
1705
  }
1388
1706
  case "StreamSend": {
1389
- const [sink, value] = term.inputs.map((input) => this.boundary(act, input));
1390
- let cancelWait = () => { };
1391
- let owned = true;
1392
- let closing = false;
1393
- let cancelled = null;
1394
- 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);
1395
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);
1396
1714
  act.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
1397
- cancelled ??= fault;
1398
- cancelWait();
1399
- if (owned) {
1400
- owned = false;
1401
- closing = true;
1402
- this.closeValueAsync(value, this.faultText(fault), (closeFault) => cancel(closeFault ?? fault));
1403
- }
1404
- else if (!closing)
1405
- cancel(fault);
1715
+ stop(fault, (closeFault) => cancel?.(closeFault ?? fault));
1406
1716
  };
1407
- cancelWait = this.pipes.send(sink, value, term.detail.park, (status) => {
1408
- owned = false;
1409
- if (status === 0)
1410
- wake(UNIT);
1411
- else {
1412
- closing = true;
1413
- this.closeValueAsync(value, this.pipeFault(act), (fault) => wake(UNIT, fault ?? cancelled, status));
1414
- }
1415
- });
1416
1717
  return;
1417
1718
  }
1418
1719
  case "StreamNext": {
1419
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
+ }
1420
1728
  let cancelWait = () => { };
1421
1729
  const wake = this.park(act, term, () => cancelWait());
1422
1730
  cancelWait = this.pipes.receive(stream, term.detail.park, (value, fault) => {
@@ -1429,10 +1737,13 @@ class ExecutorV1 {
1429
1737
  }
1430
1738
  case "Await": {
1431
1739
  const task = this.taskFor(this.boundary(act, term.inputs[0]));
1432
- const wake = this.park(act, term);
1740
+ let unwatch = () => { };
1741
+ const wake = this.park(act, term, () => unwatch());
1433
1742
  const ready = () => {
1434
- if (!task.done)
1743
+ if (!task.done) {
1744
+ this.pendingActorWait(act, [task.actorTarget], false, "ask", (fault) => wake(UNIT, fault));
1435
1745
  return;
1746
+ }
1436
1747
  if (task.fault)
1437
1748
  wake(UNIT, task.fault);
1438
1749
  else {
@@ -1442,6 +1753,11 @@ class ExecutorV1 {
1442
1753
  }
1443
1754
  };
1444
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
+ };
1445
1761
  ready();
1446
1762
  return;
1447
1763
  }
@@ -1504,6 +1820,11 @@ class ExecutorV1 {
1504
1820
  cancelTimer?.();
1505
1821
  wake({ kind: "i64", value: BigInt(ready[0].index) });
1506
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
+ }
1507
1828
  };
1508
1829
  if (hasTimeout) {
1509
1830
  const duration = values[values.length - 1];
@@ -1531,18 +1852,39 @@ class ExecutorV1 {
1531
1852
  const protocol = term.detail;
1532
1853
  const inputs = term.inputs.map((input) => this.boundary(act, input));
1533
1854
  const wake = this.park(act, term);
1855
+ let target;
1856
+ let checkWait;
1534
1857
  const cancelRequest = this.ask(protocol, inputs, (value, error, closeFault) => {
1535
1858
  act.fault ??= closeFault ?? null;
1536
1859
  return wake(this.completion(term.result_shape, term.error_shape, value, error));
1537
- }, term.request_shapes);
1860
+ }, term.request_shapes, (actor) => {
1861
+ target = actor;
1862
+ checkWait?.();
1863
+ });
1538
1864
  const cancel = act.context.cancel;
1539
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();
1540
1868
  return;
1541
1869
  }
1542
1870
  case "NativeIo": {
1543
1871
  if (term.detail?.operation === "SinkFinish") {
1544
- this.host.pipes.close(this.boundary(act, term.inputs[0]));
1545
- 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
+ });
1546
1888
  return;
1547
1889
  }
1548
1890
  // Admission passed `StdinReadLine`. The record is the raw line,
@@ -1552,6 +1894,40 @@ class ExecutorV1 {
1552
1894
  this.park(act, term)(bytesValue([...line]));
1553
1895
  return;
1554
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
+ }
1555
1931
  case "Sleep": {
1556
1932
  const duration = term.inputs[0]
1557
1933
  ? this.boundary(act, term.inputs[0])
@@ -1566,13 +1942,34 @@ class ExecutorV1 {
1566
1942
  throw new Error(`suspension kind ${term.kind} resolved no handler after admission`);
1567
1943
  }
1568
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
+ }
1569
1962
  park(act, term, cleanup = () => { }) {
1570
1963
  this.running = false;
1964
+ act.context.parked = true;
1571
1965
  let pending = null;
1572
1966
  const resume = (value, fault = null, edge = 0) => {
1573
1967
  if (pending)
1574
1968
  return false;
1575
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);
1576
1973
  cleanup();
1577
1974
  this.scheduler.enqueue(act.context.id, () => {
1578
1975
  const outcome = pending;
@@ -1581,7 +1978,8 @@ class ExecutorV1 {
1581
1978
  const fault = closeFault ?? (outcome.fault ? (act.fault ?? outcome.fault) : null);
1582
1979
  if (fault) {
1583
1980
  act.fault = fault;
1584
- this.takeEdge(act, fault.kind === "panic" && fault.cancelled
1981
+ this.takeEdge(act, term.kind === "Safepoint" ||
1982
+ (fault.kind === "panic" && fault.cancelled)
1585
1983
  ? term.cancel
1586
1984
  : term.unwind);
1587
1985
  }
@@ -1599,13 +1997,15 @@ class ExecutorV1 {
1599
1997
  // the cancellation latch live through that window and retire a result
1600
1998
  // whose normal edge the consumer never entered.
1601
1999
  if (outcome.fault)
1602
- 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);
1603
2001
  else
1604
2002
  finish();
1605
2003
  });
1606
2004
  return true;
1607
2005
  };
1608
2006
  act.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
2007
+ if (term.cancel == null)
2008
+ return;
1609
2009
  if (pending)
1610
2010
  pending.fault ??= fault;
1611
2011
  else
@@ -1613,6 +2013,222 @@ class ExecutorV1 {
1613
2013
  };
1614
2014
  return resume;
1615
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
+ }
1616
2232
  newPipe(capacity) {
1617
2233
  const id = this.ids.channel();
1618
2234
  this.trace.allocateId("channel", id, this.current?.context.id ?? "actor:root");
@@ -1638,7 +2254,7 @@ class ExecutorV1 {
1638
2254
  act.fault = combineFaults(act.fault, fault);
1639
2255
  if (!synchronous)
1640
2256
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1641
- });
2257
+ }, act.context.actor, act.rcOwner);
1642
2258
  synchronous = false;
1643
2259
  if (!completed)
1644
2260
  this.running = false;
@@ -1663,7 +2279,7 @@ class ExecutorV1 {
1663
2279
  receive?.(null, generator.fault);
1664
2280
  for (const closed of generator.closed.splice(0))
1665
2281
  closed(generator.fault);
1666
- });
2282
+ }, generator.frame.context.actor, generator.frame.rcOwner);
1667
2283
  }
1668
2284
  closeGenerator(generator, done) {
1669
2285
  if (generator.done) {
@@ -1676,7 +2292,7 @@ class ExecutorV1 {
1676
2292
  else
1677
2293
  generator.frame.context.cancel?.();
1678
2294
  }
1679
- 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) {
1680
2296
  const pending = [value];
1681
2297
  let firstFault = null;
1682
2298
  const next = (failure = null) => {
@@ -1687,15 +2303,35 @@ class ExecutorV1 {
1687
2303
  while (pending.length) {
1688
2304
  const value = pending.pop();
1689
2305
  switch (value.kind) {
2306
+ case "string":
2307
+ value.release();
2308
+ break;
1690
2309
  case "rc":
1691
2310
  if (!value.closed) {
1692
2311
  value.closed = true;
1693
- 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;
1694
2315
  pending.push(value.cell.value);
1695
2316
  value.cell.value = UNIT;
1696
2317
  }
1697
2318
  }
1698
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
+ }
1699
2335
  case "task": {
1700
2336
  // Dropping a handle does not cancel scoped work. Join drains the
1701
2337
  // task, and scope close releases any result that was never taken.
@@ -1703,7 +2339,7 @@ class ExecutorV1 {
1703
2339
  if (task?.done) {
1704
2340
  const result = task.value;
1705
2341
  task.value = UNIT;
1706
- 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);
1707
2343
  return;
1708
2344
  }
1709
2345
  if (task?.close) {
@@ -1731,7 +2367,7 @@ class ExecutorV1 {
1731
2367
  if (this.consumedResources.has(value))
1732
2368
  break;
1733
2369
  if (!this.closingResources.has(value)) {
1734
- this.invokeFrame(actor, resource.close, [value], () => next(), next);
2370
+ this.invokeFrame(actor, resource.close, [value], () => next(), next, rcOwner);
1735
2371
  return;
1736
2372
  }
1737
2373
  this.consumedResources.add(value);
@@ -1745,10 +2381,36 @@ class ExecutorV1 {
1745
2381
  case "vector":
1746
2382
  pending.push(...[...value.items].reverse());
1747
2383
  break;
1748
- case "sink":
1749
- 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
+ }
1750
2396
  pending.push(...this.pipes.close(value, currentFault).reverse());
1751
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
+ }
1752
2414
  }
1753
2415
  }
1754
2416
  done(firstFault);
@@ -1756,6 +2418,8 @@ class ExecutorV1 {
1756
2418
  next();
1757
2419
  }
1758
2420
  closeValue(value, fault) {
2421
+ if (value.kind === "string")
2422
+ value.release();
1759
2423
  if (value.kind === "closure")
1760
2424
  this.closeValue(value.environment, fault);
1761
2425
  else if (value.kind === "sink" || value.kind === "stream")
@@ -1804,25 +2468,33 @@ class ExecutorV1 {
1804
2468
  for (const changed of [...task.changed])
1805
2469
  changed();
1806
2470
  }
1807
- invokeFrame(actor, callable, args, returned, failed) {
1808
- 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 = {
1809
2473
  id: actor?.id ?? "actor:root",
1810
2474
  actor,
2475
+ rcOwner,
1811
2476
  returned,
1812
- failed,
1813
- });
2477
+ failed: (fault) => failed(fault),
2478
+ };
2479
+ const frame = this.activate(this.functionAt(callable), args, null, null, undefined, null, context);
1814
2480
  let cancelled = null;
1815
- frame.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
2481
+ context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
1816
2482
  cancelled = fault;
1817
2483
  };
1818
- this.scheduler.enqueue(frame.context.id, () => {
1819
- frame.context.cancel = undefined;
1820
- if (cancelled)
1821
- frame.context.failed(cancelled);
1822
- else
1823
- this.runFrame(frame);
1824
- });
1825
- 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;
1826
2498
  }
1827
2499
  variant(shapeId, role, payload = []) {
1828
2500
  const shape = shapeId == null ? undefined : this.pkg.variants[shapeId];
@@ -1866,13 +2538,26 @@ class ExecutorV1 {
1866
2538
  actorCall(act, term) {
1867
2539
  const args = term.args.map((arg) => this.boundary(act, arg));
1868
2540
  const operation = term.operation;
2541
+ const caller = act.context.actor
2542
+ ? { kind: "Actor", identity: act.context.actor.identity }
2543
+ : { kind: "Task" };
1869
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
+ }
1870
2553
  case "spawn": {
1871
2554
  const layout = this.pkg.actors?.find((actor) => actor.id === operation.actor);
1872
2555
  if (!layout)
1873
2556
  throw new Error(`actor layout ${operation.actor} is missing`);
1874
2557
  let supplied = 0;
1875
2558
  const actor = {
2559
+ rcOwner: act.rcOwner ?? { blocks: new Set(), closed: false },
2560
+ identity: ++this.actorIdentity,
1876
2561
  id: this.ids.actor(),
1877
2562
  layout,
1878
2563
  state: {
@@ -1881,9 +2566,11 @@ class ExecutorV1 {
1881
2566
  fields: layout.state_fields.map((field) => field.deferred ? UNIT : (args[supplied++] ?? UNIT)),
1882
2567
  },
1883
2568
  mailbox: [],
2569
+ timers: new Set(),
1884
2570
  disposals: [],
1885
2571
  admission: [],
1886
2572
  busy: true,
2573
+ constructing: true,
1887
2574
  alive: true,
1888
2575
  completed: false,
1889
2576
  closing: false,
@@ -1893,44 +2580,181 @@ class ExecutorV1 {
1893
2580
  closingFault: null,
1894
2581
  crashing: null,
1895
2582
  active: null,
1896
- stopHook: null,
2583
+ terminalHook: null,
1897
2584
  closed: [],
1898
2585
  };
1899
2586
  this.actors.set(actor.id, actor);
1900
2587
  this.trace.allocateId("actor", actor.id, act.context.id);
1901
2588
  this.trace.snapshot("actor.spawn", { actor_id: actor.id });
1902
2589
  const handle = { kind: "actor", id: actor.id };
1903
- this.running = false;
1904
2590
  const failed = (fault) => {
2591
+ actor.active = null;
1905
2592
  actor.alive = false;
1906
- this.closeValueAsync(actor.state, this.faultText(fault), () => {
1907
- actor.completed = true;
1908
- for (const message of actor.mailbox.splice(0))
1909
- message.complete(null, "Dead");
1910
- for (const closed of actor.closed.splice(0))
1911
- closed();
1912
- this.raiseFault(act, fault, term.unwind);
1913
- 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);
1914
2608
  });
1915
2609
  };
1916
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;
1917
2622
  actor.busy = false;
2623
+ this.startPeriodicTimers(actor);
1918
2624
  this.dispatchActor(actor);
1919
2625
  this.completeShim(act, term, handle);
1920
- this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2626
+ this.current = act;
2627
+ this.running = true;
1921
2628
  };
2629
+ // Construction enters the same interpreter loop as its caller. Only
2630
+ // a real suspension queues work and advances deterministic scheduling.
1922
2631
  const start = () => {
1923
2632
  if (layout.start !== undefined)
1924
- this.invokeFrame(actor, layout.start, [actor.state], started, failed);
2633
+ actor.active = this.invokeFrame(actor, layout.start, [actor.state], started, failed, actor.rcOwner, true);
1925
2634
  else
1926
2635
  started();
1927
2636
  };
1928
2637
  if (layout.init !== undefined)
1929
- 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);
1930
2639
  else
1931
2640
  start();
1932
2641
  return;
1933
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
+ }
1934
2758
  case "stream_start": {
1935
2759
  const [target, payload] = args;
1936
2760
  const fields = fieldsOf(payload);
@@ -1956,6 +2780,7 @@ class ExecutorV1 {
1956
2780
  if (!handler)
1957
2781
  throw new Error("stream request has no producer handler");
1958
2782
  actor.mailbox.push({
2783
+ caller,
1959
2784
  handler,
1960
2785
  payload: fields,
1961
2786
  reply: false,
@@ -1980,6 +2805,10 @@ class ExecutorV1 {
1980
2805
  task.actorError = error;
1981
2806
  this.settleTask(task, value ?? UNIT, closeFault ?? null);
1982
2807
  return true;
2808
+ }, undefined, (actor) => {
2809
+ task.actorTarget = actor;
2810
+ for (const changed of [...task.changed])
2811
+ changed();
1983
2812
  });
1984
2813
  task.close = (done) => cancelRequest((fault) => {
1985
2814
  this.settleTask(task, UNIT, fault);
@@ -2016,15 +2845,17 @@ class ExecutorV1 {
2016
2845
  this.variant(shapes.success, discarded ? "DeliveryDiscarded" : "DeliveryAccepted"),
2017
2846
  ]);
2018
2847
  const rejected = (reason) => this.variant(term.result_shape, "ResultErr", [
2019
- {
2020
- kind: "record",
2021
- typeId: this.aggregateName(shapes.failure),
2022
- shape: shapes.failure,
2023
- fields: [
2024
- this.variant(shapes.reason, sendErrorRole(reason)),
2025
- request,
2026
- ],
2027
- },
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
+ ]),
2028
2859
  ]);
2029
2860
  let parked = false;
2030
2861
  const finish = (value) => {
@@ -2037,85 +2868,38 @@ class ExecutorV1 {
2037
2868
  finish(rejected("Dead"));
2038
2869
  return;
2039
2870
  }
2040
- const full = actor.layout.mailbox_capacity !== undefined &&
2041
- actor.mailbox.length >= actor.layout.mailbox_capacity;
2042
- // Declared loss transfers cleanup to the receiver. Sender-selected
2043
- // loss still closes the sender's unadmitted request in its own frame.
2044
- const discardAtReceiver = (value, report) => {
2045
- actor.disposals.push(value);
2046
- this.dispatchActor(actor);
2047
- finish(report);
2048
- };
2049
- const discard = (value, report) => {
2050
- let settled = false;
2051
- let waiting = false;
2052
- this.closeValueAsync(value, this.pipeFault(act), (fault) => {
2053
- act.fault ??= fault;
2054
- settled = true;
2055
- if (waiting)
2056
- finish(report);
2057
- else
2058
- this.completeShim(act, term, report);
2059
- }, act.context.actor);
2060
- if (!settled) {
2061
- waiting = true;
2062
- parked = true;
2063
- this.running = false;
2064
- }
2871
+ const message = {
2872
+ caller,
2873
+ handler,
2874
+ payload: payload.fields,
2875
+ reply: false,
2876
+ obligated: false,
2877
+ complete: (_value, _error, drained) => drained?.(null),
2065
2878
  };
2066
- let declared = full ? actor.layout.overflow : null;
2067
- const coalesce = actor.layout.coalesce;
2068
- if (declared === "coalesce" && coalesce) {
2069
- const key = coalesce.keys.find((key) => key.message === handler.message_id);
2070
- const queued = key
2071
- ? actor.mailbox.findIndex((message) => !message.obligated &&
2072
- message.handler.message_id === handler.message_id &&
2073
- equals(message.payload[key.param], payload.fields[key.param]))
2074
- : -1;
2075
- if (queued >= 0) {
2076
- const superseded = actor.mailbox[queued];
2077
- actor.mailbox[queued] = {
2078
- ...superseded,
2079
- payload: payload.fields,
2080
- };
2081
- discardAtReceiver({ kind: "record", typeId: "", fields: superseded.payload }, accepted(false));
2082
- return;
2083
- }
2084
- declared = coalesce.fallback;
2085
- }
2086
- if (declared === "drop_new") {
2087
- discardAtReceiver(payload, accepted(true));
2088
- return;
2089
- }
2090
- // A queued completion call owes its caller a reply, so `drop_old`
2091
- // evicts only a one-way message and refuses when there is none.
2092
- const oldest = declared === "drop_old"
2093
- ? actor.mailbox.findIndex((message) => !message.obligated)
2094
- : -1;
2095
- if (oldest >= 0) {
2096
- const [evicted] = actor.mailbox.splice(oldest, 1);
2097
- actor.mailbox.push({
2098
- handler,
2099
- payload: payload.fields,
2100
- reply: false,
2101
- obligated: false,
2102
- complete: (_value, _error, drained) => drained?.(null),
2103
- });
2104
- this.dispatchActor(actor);
2105
- discardAtReceiver({ kind: "record", typeId: "", fields: evicted.payload }, accepted(false));
2106
- return;
2107
- }
2108
- if (full) {
2879
+ const status = this.admitActorMessage(actor, message);
2880
+ if (status === "full") {
2109
2881
  if (operation.policy === "drop_newest") {
2110
- 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
+ }
2111
2897
  return;
2112
2898
  }
2113
2899
  if (operation.policy === "reject") {
2114
2900
  finish(rejected("Full"));
2115
2901
  return;
2116
2902
  }
2117
- // The checker already resolved declaration defaults and explicit
2118
- // overrides; the declaration's own resolution happened above.
2119
2903
  if (operation.policy !== "wait")
2120
2904
  throw new Error("submission has no admitted policy");
2121
2905
  parked = true;
@@ -2123,19 +2907,10 @@ class ExecutorV1 {
2123
2907
  actor.admission.push(submit);
2124
2908
  return;
2125
2909
  }
2126
- actor.mailbox.push({
2127
- handler,
2128
- payload: payload.fields,
2129
- reply: false,
2130
- obligated: false,
2131
- complete: (_value, _error, drained) => drained?.(null),
2132
- });
2133
- this.trace.snapshot("actor.send", {
2134
- actor_id: actor.id,
2135
- handler: handler.name,
2136
- });
2137
- this.dispatchActor(actor);
2138
- finish(accepted(false));
2910
+ if (status === "closed")
2911
+ finish(rejected("Dead"));
2912
+ else
2913
+ finish(accepted(status === "discarded"));
2139
2914
  };
2140
2915
  submit();
2141
2916
  return;
@@ -2144,15 +2919,32 @@ class ExecutorV1 {
2144
2919
  const layout = this.pkg.supervisors?.find((layout) => layout.id === operation.supervisor);
2145
2920
  if (!layout)
2146
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;
2147
2941
  const owner = {
2148
2942
  id: this.ids.supervisor(),
2149
2943
  layout,
2150
- config: args,
2151
- children: layout.children.flatMap((child) => Array.from({ length: child.pool_count ?? 1 }, () => null)),
2152
- specs: layout.children.flatMap((child) => Array.from({ length: child.pool_count ?? 1 }, () => child)),
2153
- offsets: layout.children.map((_child, index) => layout.children
2154
- .slice(0, index)
2155
- .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),
2156
2948
  recovering: new Set(),
2157
2949
  restartBusy: false,
2158
2950
  pendingRestarts: [],
@@ -2211,7 +3003,7 @@ class ExecutorV1 {
2211
3003
  this.completeShim(act, term, {
2212
3004
  kind: "vector",
2213
3005
  elementType: "role",
2214
- items: Array.from({ length: spec.pool_count ?? 1 }, (_, index) => makeRole(offset + index)),
3006
+ items: Array.from({ length: owner.children.length }, (_, index) => makeRole(offset + index)),
2215
3007
  });
2216
3008
  return;
2217
3009
  }
@@ -2221,12 +3013,16 @@ class ExecutorV1 {
2221
3013
  case "await_restarted": {
2222
3014
  const target = args[0];
2223
3015
  const role = "id" in target ? this.roles.get(target.id) : undefined;
2224
- if (!role)
2225
- throw new Error("restarted requires a supervised role");
2226
- 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)) {
2227
3023
  this.running = false;
2228
3024
  const settled = () => {
2229
- if (this.rolePending(role)) {
3025
+ if (this.roleRestartPending(role)) {
2230
3026
  role.waiting.push(settled);
2231
3027
  return;
2232
3028
  }
@@ -2266,7 +3062,10 @@ class ExecutorV1 {
2266
3062
  if (target.kind === "actor") {
2267
3063
  const actor = this.actorFor(target);
2268
3064
  const complete = () => {
2269
- 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;
2270
3069
  if (actor.closeFaultDebt !== undefined)
2271
3070
  this.faultDebts.delete(actor.closeFaultDebt);
2272
3071
  if (fault)
@@ -2312,15 +3111,41 @@ class ExecutorV1 {
2312
3111
  throw new Error(`actor operation ${operation.op} has no executor`);
2313
3112
  }
2314
3113
  }
2315
- ask(protocol, inputs, complete, shapes) {
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
+ }
3122
+ requestDeadline(act) {
3123
+ let deadline = act.context.requestDeadline;
3124
+ for (let frame = act; frame; frame = frame.caller) {
3125
+ for (const group of frame.scopes.values()) {
3126
+ if (group.expiresAt !== undefined)
3127
+ deadline = Math.min(deadline ?? Infinity, group.expiresAt);
3128
+ }
3129
+ }
3130
+ return deadline;
3131
+ }
3132
+ ask(protocol, inputs, complete, shapes, selectedTarget) {
2316
3133
  let settled = false;
2317
3134
  let admitted = false;
2318
3135
  let closing = false;
2319
3136
  const owner = this.current.context.actor;
3137
+ const rcOwner = this.current.rcOwner;
3138
+ const deadline = this.requestDeadline(this.current);
3139
+ let cancelTimer;
3140
+ let message;
3141
+ const caller = owner
3142
+ ? { kind: "Actor", identity: owner.identity }
3143
+ : { kind: "Task" };
2320
3144
  let receiver = null;
2321
3145
  const finish = (value, error, drained) => {
2322
3146
  const alreadySettled = settled;
2323
3147
  settled = true;
3148
+ cancelTimer?.();
2324
3149
  if (alreadySettled || complete(value, error) === false) {
2325
3150
  // The finishing receiver still owns a reply that its caller no
2326
3151
  // longer accepts. Its next turn must wait for this release.
@@ -2331,7 +3156,7 @@ class ExecutorV1 {
2331
3156
  this.crashActor(owner, fault);
2332
3157
  else if (fault)
2333
3158
  this.rootFault ??= fault;
2334
- }, admitted ? receiver : owner);
3159
+ }, admitted ? receiver : owner, admitted ? (receiver?.rcOwner ?? null) : rcOwner);
2335
3160
  }
2336
3161
  else
2337
3162
  drained?.(null);
@@ -2355,7 +3180,7 @@ class ExecutorV1 {
2355
3180
  closeFault = fault;
2356
3181
  for (const waiter of closeWaiters.splice(0))
2357
3182
  waiter(fault);
2358
- }, owner);
3183
+ }, owner, rcOwner);
2359
3184
  };
2360
3185
  const reject = (reason) => {
2361
3186
  if (settled || closing)
@@ -2364,12 +3189,17 @@ class ExecutorV1 {
2364
3189
  if (settled)
2365
3190
  return;
2366
3191
  settled = true;
3192
+ cancelTimer?.();
2367
3193
  complete(null, reason, fault);
2368
3194
  });
2369
3195
  };
2370
3196
  const attempt = () => {
2371
3197
  if (settled || closing)
2372
3198
  return;
3199
+ if (deadline !== undefined && deadline <= this.trace.virtualTimeMs) {
3200
+ reject("TimedOut");
3201
+ return;
3202
+ }
2373
3203
  const role = "id" in target ? this.roles.get(target.id) : undefined;
2374
3204
  if (role && this.rolePending(role)) {
2375
3205
  role.waiting.push(attempt);
@@ -2383,6 +3213,9 @@ class ExecutorV1 {
2383
3213
  const handler = actor.layout.handlers.find((handler) => handler.message_id === protocol.message);
2384
3214
  if (!handler)
2385
3215
  throw new Error(`actor ${actor.id} has no message ${protocol.message}`);
3216
+ selectedTarget?.(actor);
3217
+ if (settled || closing)
3218
+ return;
2386
3219
  const full = actor.layout.mailbox_capacity !== undefined &&
2387
3220
  actor.mailbox.length >= actor.layout.mailbox_capacity;
2388
3221
  if (full) {
@@ -2422,29 +3255,164 @@ class ExecutorV1 {
2422
3255
  }
2423
3256
  admitted = true;
2424
3257
  receiver = actor;
2425
- actor.mailbox.push({
3258
+ message = {
3259
+ caller,
2426
3260
  handler,
2427
3261
  payload,
2428
3262
  complete: finish,
2429
3263
  reply: true,
2430
3264
  obligated: true,
2431
- });
3265
+ requestDeadline: deadline,
3266
+ };
3267
+ actor.mailbox.push(message);
2432
3268
  this.trace.snapshot("actor.ask", {
2433
3269
  actor_id: actor.id,
2434
3270
  handler: handler.name,
2435
3271
  });
2436
3272
  this.dispatchActor(actor);
2437
3273
  };
3274
+ const withdrawQueued = () => {
3275
+ if (!message || !receiver)
3276
+ return;
3277
+ const index = receiver.mailbox.indexOf(message);
3278
+ if (index < 0)
3279
+ return;
3280
+ receiver.mailbox.splice(index, 1);
3281
+ admitted = false;
3282
+ for (const admit of receiver.admission.splice(0))
3283
+ admit();
3284
+ };
3285
+ if (deadline !== undefined) {
3286
+ cancelTimer = this.scheduler.after(BigInt(Math.max(0, Math.round((deadline - this.trace.virtualTimeMs) * 1_000_000))), () => {
3287
+ if (settled || closing)
3288
+ return;
3289
+ withdrawQueued();
3290
+ if (admitted)
3291
+ finish(null, "TimedOut");
3292
+ else
3293
+ reject("TimedOut");
3294
+ });
3295
+ }
2438
3296
  attempt();
2439
3297
  return (done) => {
2440
3298
  const wasSettled = settled;
2441
3299
  settled = true;
3300
+ cancelTimer?.();
3301
+ withdrawQueued();
2442
3302
  if (admitted || (wasSettled && !closing))
2443
3303
  done(null);
2444
3304
  else
2445
3305
  closeRequest(done);
2446
3306
  };
2447
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
+ }
2448
3416
  dispatchActor(actor) {
2449
3417
  if (actor.busy || !actor.alive || actor.drainingMailbox)
2450
3418
  return;
@@ -2480,15 +3448,35 @@ class ExecutorV1 {
2480
3448
  this.dispatchActor(actor);
2481
3449
  return;
2482
3450
  }
2483
- const message = actor.mailbox.shift();
3451
+ const message = actor.mailbox[0];
2484
3452
  if (!message || !actor.alive) {
2485
3453
  actor.busy = false;
2486
3454
  if (actor.closing && actor.alive)
2487
3455
  this.dispatchActor(actor);
2488
3456
  return;
2489
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();
2490
3466
  for (const admit of actor.admission.splice(0))
2491
3467
  admit();
3468
+ if (message.requestDeadline !== undefined &&
3469
+ message.requestDeadline <= this.trace.virtualTimeMs) {
3470
+ this.closeValueAsync({ kind: "record", typeId: "", fields: message.payload }, null, (fault) => {
3471
+ message.complete(null, "TimedOut");
3472
+ actor.busy = false;
3473
+ if (fault)
3474
+ this.crashActor(actor, fault);
3475
+ else
3476
+ this.dispatchActor(actor);
3477
+ }, actor);
3478
+ return;
3479
+ }
2492
3480
  this.trace.snapshot("actor.receive", {
2493
3481
  actor_id: actor.id,
2494
3482
  handler: message.handler.name,
@@ -2496,12 +3484,15 @@ class ExecutorV1 {
2496
3484
  const frame = this.activate(this.functionAt(message.handler.callable), [actor.state, ...message.payload], null, null, undefined, null, {
2497
3485
  id: actor.id,
2498
3486
  actor,
3487
+ caller: message.caller,
3488
+ requestDeadline: message.requestDeadline,
3489
+ reply: message.reply,
2499
3490
  returned: (value) => {
2500
3491
  actor.active = null;
2501
3492
  const drained = (fault) => {
2502
3493
  actor.busy = false;
2503
3494
  if (fault)
2504
- this.crashActor(actor, fault);
3495
+ this.crashActor(actor, fault, message.handler);
2505
3496
  else
2506
3497
  this.dispatchActor(actor);
2507
3498
  };
@@ -2517,8 +3508,9 @@ class ExecutorV1 {
2517
3508
  else {
2518
3509
  this.crashActor(actor, {
2519
3510
  kind: "panic",
3511
+ handlerFailed: true,
2520
3512
  message: "unhandled declared handler failure",
2521
- });
3513
+ }, message.handler);
2522
3514
  return;
2523
3515
  }
2524
3516
  }
@@ -2533,12 +3525,19 @@ class ExecutorV1 {
2533
3525
  failed: (fault) => {
2534
3526
  actor.active = null;
2535
3527
  actor.busy = false;
2536
- if (fault.kind === "panic" && fault.cancelled) {
3528
+ if (fault.kind === "trap" &&
3529
+ fault.trap === "actor_refused" &&
3530
+ !fault.secondary?.length) {
3531
+ actor.crashing = null;
3532
+ message.complete(null, "Refused");
3533
+ this.dispatchActor(actor);
3534
+ }
3535
+ else if (fault.kind === "panic" && fault.cancelled) {
2537
3536
  message.complete(null, "Dead");
2538
3537
  this.dispatchActor(actor);
2539
3538
  }
2540
3539
  else {
2541
- this.crashActor(actor, fault);
3540
+ this.crashActor(actor, fault, message.handler);
2542
3541
  message.complete(null, "Trapped");
2543
3542
  }
2544
3543
  },
@@ -2547,7 +3546,12 @@ class ExecutorV1 {
2547
3546
  this.runFrame(frame);
2548
3547
  });
2549
3548
  }
2550
- 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
+ }
2551
3555
  const debt = ++this.nextFaultDebt;
2552
3556
  this.faultDebts.add(debt);
2553
3557
  if (actor.supervisor)
@@ -2555,6 +3559,7 @@ class ExecutorV1 {
2555
3559
  actor.crashing ??= fault;
2556
3560
  actor.alive = false;
2557
3561
  actor.busy = false;
3562
+ this.cancelActorTimers(actor);
2558
3563
  this.trace.snapshot("actor.crash", {
2559
3564
  actor_id: actor.id,
2560
3565
  message: this.faultText(fault),
@@ -2564,47 +3569,114 @@ class ExecutorV1 {
2564
3569
  actor.mailboxDrained.push(() => finished(action));
2565
3570
  return;
2566
3571
  }
2567
- this.closeValueAsync(actor.state, this.faultText(fault), () => this.drainMailbox(actor, this.faultText(fault), () => {
2568
- actor.completed = true;
2569
- for (const wake of actor.closed.splice(0))
2570
- wake();
2571
- for (const admit of actor.admission.splice(0))
2572
- admit();
2573
- if (actor.supervisor) {
2574
- const { owner, child } = actor.supervisor;
2575
- if (action === "Restart")
2576
- this.restartChild(owner, child, [debt]);
2577
- else if (action === "Escalate")
2578
- this.escalateSupervisor(owner, [debt]);
2579
- else {
2580
- owner.spent.add(child);
2581
- owner.recovering.delete(child);
2582
- this.wakeRoles();
2583
- }
2584
- }
2585
- }), 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);
2586
3609
  };
2587
- if (actor.layout.crash !== undefined) {
2588
- const [code, message] = faultInfo(fault);
2589
- const info = {
2590
- kind: "record",
2591
- typeId: this.aggregateName(actor.layout.crash_info),
2592
- shape: actor.layout.crash_info,
2593
- fields: [
2594
- { kind: "i64", value: BigInt(code) },
2595
- { kind: "string", value: String(message) },
2596
- ],
2597
- };
2598
- this.invokeFrame(actor, actor.layout.crash, [actor.state, info], (value) => {
2599
- const shape = this.pkg.variants[actor.layout.crash_action];
2600
- const action = value.kind === "enum" ? shape?.cases[value.tag]?.name : undefined;
2601
- if (!action)
2602
- throw new Error("crash hook returned no declared action");
2603
- finished(action);
2604
- }, () => finished("Kill"));
2605
- }
2606
- else
2607
- 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
+ });
2608
3680
  }
2609
3681
  supervisorFor(value) {
2610
3682
  if (value.kind !== "supervisor")
@@ -2657,6 +3729,23 @@ class ExecutorV1 {
2657
3729
  return (!!owner?.alive &&
2658
3730
  (owner.recovering.has(role.child) || owner.children[role.child] === null));
2659
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
+ }
2660
3749
  wakeRoles() {
2661
3750
  for (const role of this.roles.values()) {
2662
3751
  this.roleValue(role);
@@ -2789,6 +3878,16 @@ class ExecutorV1 {
2789
3878
  else if (value.kind === "supervisor")
2790
3879
  this.stopSupervisor(this.supervisorFor(value), true);
2791
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
+ }
2792
3891
  stopSupervisor(owner, force = false) {
2793
3892
  if (force) {
2794
3893
  owner.terminating = true;
@@ -2808,12 +3907,7 @@ class ExecutorV1 {
2808
3907
  const index = childIndex--;
2809
3908
  const child = owner.children[index];
2810
3909
  owner.teardownChild = child;
2811
- const spec = owner.specs[index];
2812
- const stopNs = spec.stop_ns === undefined
2813
- ? "actor" in spec.role
2814
- ? 5_000_000_000
2815
- : null
2816
- : spec.stop_ns;
3910
+ const stopNs = this.childStopBudget(owner, index);
2817
3911
  if (!owner.terminating && stopNs !== null && stopNs > 0) {
2818
3912
  owner.teardownCancelTimer = this.scheduler.after(BigInt(stopNs), () => this.terminateChild(child));
2819
3913
  }
@@ -2821,9 +3915,11 @@ class ExecutorV1 {
2821
3915
  return;
2822
3916
  }
2823
3917
  this.closeValueAsync({ kind: "record", typeId: "", fields: owner.config }, null, (fault) => {
2824
- if (fault)
2825
- this.faultDebts.add(++this.nextFaultDebt);
3918
+ const debt = fault ? ++this.nextFaultDebt : undefined;
3919
+ if (debt !== undefined)
3920
+ this.faultDebts.add(debt);
2826
3921
  owner.completed = true;
3922
+ this.settleSupervisedEnd(owner, fault ? "Crashed" : owner.terminating ? "Terminated" : "Stopped", debt === undefined ? [] : [debt]);
2827
3923
  for (const wake of owner.closed.splice(0))
2828
3924
  wake();
2829
3925
  this.wakeRoles();
@@ -2858,6 +3954,15 @@ class ExecutorV1 {
2858
3954
  if (actor.closing || actor.completed)
2859
3955
  return;
2860
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
+ }
2861
3966
  // Admission closes immediately. The active turn and every accepted queued
2862
3967
  // turn finish before stop hooks and state release.
2863
3968
  this.dispatchActor(actor);
@@ -2869,8 +3974,17 @@ class ExecutorV1 {
2869
3974
  return;
2870
3975
  actor.closing = true;
2871
3976
  actor.terminating = true;
2872
- actor.active?.cancel?.();
2873
- 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
+ }
2874
3988
  this.dispatchActor(actor);
2875
3989
  for (const admit of actor.admission.splice(0))
2876
3990
  admit();
@@ -2881,11 +3995,11 @@ class ExecutorV1 {
2881
3995
  const next = () => {
2882
3996
  const hook = actor.terminating ? undefined : hooks.shift();
2883
3997
  if (hook !== undefined) {
2884
- actor.stopHook = this.invokeFrame(actor, hook, [actor.state], () => {
2885
- actor.stopHook = null;
3998
+ actor.terminalHook = this.invokeFrame(actor, hook, [actor.state], () => {
3999
+ actor.terminalHook = null;
2886
4000
  next();
2887
4001
  }, (fault) => {
2888
- actor.stopHook = null;
4002
+ actor.terminalHook = null;
2889
4003
  if (!(actor.terminating && fault.kind === "panic" && fault.cancelled))
2890
4004
  actor.closingFault = combineFaults(actor.closingFault, fault);
2891
4005
  hooks.length = 0;
@@ -2895,31 +4009,37 @@ class ExecutorV1 {
2895
4009
  else {
2896
4010
  this.closeValueAsync(actor.state, this.faultText(actor.closingFault), (fault) => {
2897
4011
  actor.closingFault = combineFaults(actor.closingFault, fault);
2898
- // Stopped cleanup belongs to the completion barrier, independently
2899
- // of handler-crash debt and supervisor restart decisions.
2900
- actor.crashing = null;
2901
- if (actor.closingFault) {
2902
- actor.closeFaultDebt = ++this.nextFaultDebt;
2903
- this.faultDebts.add(actor.closeFaultDebt);
2904
- }
2905
- actor.completed = true;
2906
- actor.alive = false;
2907
- actor.busy = false;
2908
- this.trace.snapshot(actor.terminating ? "actor.terminate" : "actor.stop", { actor_id: actor.id });
2909
- this.settleSupervisedEnd(actor, actor.closingFault
2910
- ? "Crashed"
2911
- : actor.terminating
2912
- ? "Terminated"
2913
- : "Stopped");
2914
- for (const wake of actor.closed.splice(0))
2915
- 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
+ });
2916
4036
  }, actor);
2917
4037
  }
2918
4038
  };
2919
4039
  next();
2920
4040
  }
2921
- settleSupervisedEnd(actor, reason) {
2922
- const parent = actor.supervisor;
4041
+ settleSupervisedEnd(subject, reason, debts) {
4042
+ const parent = subject.supervisor;
2923
4043
  if (!parent ||
2924
4044
  !parent.owner.alive ||
2925
4045
  parent.owner.recovering.has(parent.child))
@@ -2928,7 +4048,7 @@ class ExecutorV1 {
2928
4048
  const policy = owner.specs[child].restart;
2929
4049
  if (policy === "permanent" ||
2930
4050
  (policy === "transient" && reason !== "Stopped")) {
2931
- this.restartChild(owner, child, actor.closeFaultDebt === undefined ? [] : [actor.closeFaultDebt]);
4051
+ this.restartChild(owner, child, debts);
2932
4052
  }
2933
4053
  else {
2934
4054
  owner.spent.add(child);
@@ -2936,6 +4056,16 @@ class ExecutorV1 {
2936
4056
  }
2937
4057
  }
2938
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
+ }
2939
4069
  haltWithFault(fault) {
2940
4070
  const isPanic = fault.kind === "panic";
2941
4071
  const status = isPanic ? "panic" : "trap";
@@ -2945,7 +4075,7 @@ class ExecutorV1 {
2945
4075
  : (fault.message ?? trapMessage(fault.trap));
2946
4076
  const [code, kind] = faultInfo(fault);
2947
4077
  // Native writes the failure line to stderr before it exits; so does this.
2948
- this.trace.writeStderr(faultReport(fault), null);
4078
+ this.reportFault(fault);
2949
4079
  // A failing run reports no exit code: native exits 1 for every fault and
2950
4080
  // names the kind in its message, so the kind is what travels, in
2951
4081
  // `runtime_failures`. The page turns it into an exit code of its own.
@@ -2975,14 +4105,22 @@ function combineFaults(primary, secondary) {
2975
4105
  // Reports own an immutable ordered view of original diagnostics. Faults can
2976
4106
  // be shared by a parked unwind, actor completion and multiple observers;
2977
4107
  // combining those views must neither duplicate nor mutate their graph.
2978
- const retained = primary.kind === "panic" && primary.cancelled
2979
- ? diagnostics.filter((fault) => !(fault.kind === "panic" && fault.cancelled))
4108
+ const control = (fault) => (fault.kind === "panic" && fault.cancelled) ||
4109
+ (fault.kind === "trap" && fault.trap === "actor_refused");
4110
+ const retained = control(primary)
4111
+ ? diagnostics.filter((fault) => !control(fault))
2980
4112
  : diagnostics;
2981
4113
  const [first, ...rest] = retained.length ? retained : diagnostics;
2982
4114
  return { ...first, primary: first, secondary: rest };
2983
4115
  }
2984
4116
  function faultInfo(fault) {
2985
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"];
2986
4124
  if (fault.cancelled)
2987
4125
  return [-1, "Cancelled"];
2988
4126
  if (fault.deadline)
@@ -2994,13 +4132,21 @@ function faultInfo(fault) {
2994
4132
  divide_by_zero: [202, "DivideByZero"],
2995
4133
  shift_out_of_range: [204, "ShiftOutOfRange"],
2996
4134
  vector_bounds: [205, "IndexOutOfBounds"],
4135
+ actor_refused: [-4, "Refused"],
2997
4136
  }[fault.trap];
2998
4137
  }
2999
- function faultReport(fault, secondary = false) {
4138
+ function faultDiagnostic(fault, secondary = false) {
3000
4139
  const [code, reason] = faultInfo(fault);
3001
4140
  const label = secondary ? "secondary failure" : "failure";
3002
- const message = fault.message === undefined ? "" : `: ${fault.message}`;
3003
- 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) +
3004
4150
  (fault.secondary ?? []).map((next) => faultReport(next, true)).join(""));
3005
4151
  }
3006
4152
  // ── refs ────────────────────────────────────────────────────────────────────
@@ -3063,6 +4209,8 @@ function actorErrorRole(reason) {
3063
4209
  return "ActorErrorFailed";
3064
4210
  case "Trapped":
3065
4211
  return "ActorErrorTrapped";
4212
+ case "Refused":
4213
+ return "ActorErrorRefused";
3066
4214
  case "Dead":
3067
4215
  return "ActorErrorDead";
3068
4216
  case "TimedOut":
@@ -3105,12 +4253,17 @@ function sendErrorRole(reason) {
3105
4253
  throw new Error(`unknown send error reason ${reason}`);
3106
4254
  }
3107
4255
  }
3108
- function calleeFunction(value) {
4256
+ function calleeFunction(value, specialization = null) {
3109
4257
  switch (value.kind) {
3110
4258
  case "function":
3111
4259
  return Number(value.id);
3112
- case "closure":
3113
- return value.body;
4260
+ case "closure": {
4261
+ const selected = specialization ?? value.specialization;
4262
+ const entry = value.entries.find((candidate) => candidate.specialization === selected);
4263
+ if (!entry)
4264
+ throw new Error("callable has no selected concrete invocation entry");
4265
+ return entry.body;
4266
+ }
3114
4267
  default:
3115
4268
  throw new TypeError(`indirect.call expected a callable value, got ${value.kind}`);
3116
4269
  }
@@ -3147,7 +4300,9 @@ function toComparable(value) {
3147
4300
  case "vector":
3148
4301
  return value.items.map(toComparable);
3149
4302
  case "rc":
3150
- return toComparable(value.cell.value);
4303
+ return "Rc";
4304
+ case "weak":
4305
+ return "Weak";
3151
4306
  case "map":
3152
4307
  return [...value.entries.values()].map((entry) => [
3153
4308
  toComparable(entry.key),
@@ -3187,6 +4342,8 @@ function trapMessage(trap) {
3187
4342
  return "shift amount out of range";
3188
4343
  case "vector_bounds":
3189
4344
  return "index out of bounds";
4345
+ case "actor_refused":
4346
+ return "delivery refused by interceptor";
3190
4347
  }
3191
4348
  }
3192
4349
  /// The operand type the package records. `isize` and `usize` are 64-bit in the
@@ -3318,7 +4475,7 @@ function applyBinary(op, lhs, rhs, ty) {
3318
4475
  };
3319
4476
  }
3320
4477
  if (lhs.kind === "string") {
3321
- return { kind: "string", value: lhs.value + asString(rhs) };
4478
+ return stringValue(lhs.value + asString(rhs));
3322
4479
  }
3323
4480
  return {
3324
4481
  kind: "i64",
@@ -3384,7 +4541,7 @@ function applyCast(value, to) {
3384
4541
  return { kind: "bool", value: truth(value) };
3385
4542
  case "char":
3386
4543
  case "string":
3387
- return { kind: "string", value: asString(value) };
4544
+ return stringValue(asString(value));
3388
4545
  default:
3389
4546
  break;
3390
4547
  }