@hew-lang/sandbox-vm 0.6.0-rc7 → 0.6.1

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.
@@ -5,6 +5,7 @@
5
5
  /// where the package says `copy_value`, releases where it says `destroy_value`,
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
+ import { executeCodec } from "./codecs.js";
8
9
  import { TraceBuilder, runtimeFailure } from "../trace.js";
9
10
  import { UNIT, cloneValue, toJsonValue } from "../values.js";
10
11
  import { runtimeTag } from "./package.js";
@@ -69,7 +70,7 @@ export function runPackageV1(pkg, options = {}) {
69
70
  trace.reject(rejection);
70
71
  return trace.finish();
71
72
  }
72
- const vm = new ExecutorV1(pkg, trace, StdinReader.fromReplay(replay.inputs), options.schedulerPolicy ?? "round_robin");
73
+ const vm = new ExecutorV1(pkg, trace, StdinReader.fromReplay(replay.inputs), options.schedulerPolicy ?? "round_robin", options);
73
74
  try {
74
75
  vm.run();
75
76
  }
@@ -85,6 +86,7 @@ class ExecutorV1 {
85
86
  pkg;
86
87
  trace;
87
88
  stdin;
89
+ options;
88
90
  current;
89
91
  host;
90
92
  scheduler;
@@ -96,6 +98,7 @@ class ExecutorV1 {
96
98
  generators = new Map();
97
99
  tasks = new Map();
98
100
  completedTasks = 0;
101
+ actorIdentity = 0n;
99
102
  faultDebts = new Set();
100
103
  nextFaultDebt = 0;
101
104
  closingResources = new WeakSet();
@@ -103,10 +106,11 @@ class ExecutorV1 {
103
106
  running = false;
104
107
  rootComplete = false;
105
108
  rootFault = null;
106
- constructor(pkg, trace, stdin, policy) {
109
+ constructor(pkg, trace, stdin, policy, options) {
107
110
  this.pkg = pkg;
108
111
  this.trace = trace;
109
112
  this.stdin = stdin;
113
+ this.options = options;
110
114
  this.scheduler = new FrameScheduler(trace, policy);
111
115
  this.ids = new DeterministicIds(trace.replay.seed);
112
116
  this.host = {
@@ -171,7 +175,8 @@ class ExecutorV1 {
171
175
  const act = this.current;
172
176
  while (act.opIndex < act.block.ops.length) {
173
177
  const op = act.block.ops[act.opIndex++];
174
- this.commitStep();
178
+ if (op.op !== "coverage.hit")
179
+ this.commitStep();
175
180
  try {
176
181
  this.execute(act, op);
177
182
  }
@@ -350,6 +355,11 @@ class ExecutorV1 {
350
355
  // ── instructions ─────────────────────────────────────────────────────────
351
356
  execute(act, op) {
352
357
  switch (op.op) {
358
+ case "coverage.hit": {
359
+ const key = String(op.counter);
360
+ this.trace.coverage[key] = (this.trace.coverage[key] ?? 0) + 1;
361
+ return;
362
+ }
353
363
  case "const.int":
354
364
  this.define(act, op.dst, {
355
365
  kind: "i64",
@@ -404,9 +414,19 @@ class ExecutorV1 {
404
414
  act.places.set(op.place, { value: UNIT, valid: false });
405
415
  return;
406
416
  case "store.init":
407
- case "store.assign":
408
417
  writeRef(this.placeRef(act, op.place), this.read(act, op.value));
409
418
  return;
419
+ case "store.assign": {
420
+ const ref = this.placeRef(act, op.place);
421
+ const replacement = this.read(act, op.value);
422
+ const previous = liveRef(ref) ? readRef(ref) : UNIT;
423
+ // Retire the old owner before invoking user close code. Publish the
424
+ // replacement after cleanup, including when close records a fault;
425
+ // the following cleanup.dispatch then drains the new owner once.
426
+ invalidateRef(ref);
427
+ this.releaseOperation(act, previous, () => writeRef(ref, replacement));
428
+ return;
429
+ }
410
430
  case "load.copy":
411
431
  this.define(act, op.dst, cloneValue(readRef(this.placeRef(act, op.place))));
412
432
  return;
@@ -531,7 +551,8 @@ class ExecutorV1 {
531
551
  const shape = this.closureAt(op.closure);
532
552
  this.define(act, op.dst, {
533
553
  kind: "closure",
534
- body: shape.body,
554
+ entries: shape.entries,
555
+ specialization: null,
535
556
  environment: {
536
557
  kind: "record",
537
558
  typeId: "",
@@ -540,12 +561,23 @@ class ExecutorV1 {
540
561
  });
541
562
  return;
542
563
  }
543
- case "callable.coerce":
544
- // A coercion only weakens the callable's proved capabilities; SIR
545
- // refuses one that changes an argument, a result or a capture shape,
546
- // so the value itself is unchanged.
547
- this.define(act, op.dst, this.read(act, op.source));
564
+ case "callable.coerce": {
565
+ const callable = this.read(act, op.source);
566
+ if (op.specialization === null) {
567
+ this.define(act, op.dst, callable);
568
+ }
569
+ else {
570
+ if (callable.kind !== "closure" ||
571
+ !callable.entries.some((entry) => entry.specialization === op.specialization)) {
572
+ throw new Error("callable specialization has no checked invocation entry");
573
+ }
574
+ this.define(act, op.dst, {
575
+ ...callable,
576
+ specialization: op.specialization,
577
+ });
578
+ }
548
579
  return;
580
+ }
549
581
  case "dyn.make":
550
582
  this.define(act, op.dst, {
551
583
  kind: "dyn",
@@ -576,6 +608,9 @@ class ExecutorV1 {
576
608
  const duration = this.read(act, op.duration);
577
609
  if (duration.kind !== "i64")
578
610
  throw new Error("scope duration is not a duration");
611
+ group.expiresAt =
612
+ this.trace.virtualTimeMs +
613
+ Math.max(0, Number(duration.value) / 1_000_000);
579
614
  group.cancelTimer = this.scheduler.after(duration.value, () => {
580
615
  group.deadline = {
581
616
  kind: "panic",
@@ -617,6 +652,7 @@ class ExecutorV1 {
617
652
  const context = {
618
653
  id,
619
654
  actor: act.context.actor,
655
+ requestDeadline: this.requestDeadline(act),
620
656
  returned: (value) => this.closeValueAsync(value, null, (fault) => this.finishGenerator(generator, fault)),
621
657
  failed: (fault) => this.finishGenerator(generator, fault),
622
658
  };
@@ -634,6 +670,106 @@ class ExecutorV1 {
634
670
  this.define(act, op.dst, { kind: "generator", id });
635
671
  return;
636
672
  }
673
+ case "task.race": {
674
+ const scope = act.scopes.get(op.scope);
675
+ if (!scope)
676
+ throw new Error("race has no task scope");
677
+ const members = op.members.map((operand) => {
678
+ const member = this.taskFor(this.read(act, operand));
679
+ this.invalidate(act, operand);
680
+ if (member.grouped)
681
+ throw new Error("race reuses a task handle");
682
+ member.grouped = true;
683
+ return member;
684
+ });
685
+ if (!members.length)
686
+ throw new Error("race requires members");
687
+ const task = this.newTask();
688
+ scope.tasks.push(task);
689
+ let selected = false;
690
+ let winner = null;
691
+ let value = UNIT;
692
+ let failure = null;
693
+ let cancelled = false;
694
+ const losersCancelled = new Set();
695
+ const ready = () => {
696
+ if (task.done || task.finishing)
697
+ return;
698
+ if (!selected) {
699
+ const first = members
700
+ .filter((member) => member.done)
701
+ .sort((a, b) => a.completedAt - b.completedAt)[0];
702
+ if (!first)
703
+ return;
704
+ selected = true;
705
+ if (!first.fault) {
706
+ winner = first;
707
+ value = first.value;
708
+ first.value = UNIT;
709
+ }
710
+ }
711
+ if (!cancelled) {
712
+ cancelled = true;
713
+ for (const member of members)
714
+ if (!member.done) {
715
+ losersCancelled.add(member);
716
+ member.cancel();
717
+ }
718
+ }
719
+ if (members.some((member) => !member.done))
720
+ return;
721
+ task.finishing = true;
722
+ for (const member of members) {
723
+ const index = member.changed.indexOf(ready);
724
+ if (index >= 0)
725
+ member.changed.splice(index, 1);
726
+ }
727
+ for (const member of [...members].sort((a, b) => a.completedAt - b.completedAt)) {
728
+ if (member.fault &&
729
+ !(member.fault.kind === "panic" &&
730
+ member.fault.cancelled &&
731
+ losersCancelled.has(member)))
732
+ failure = combineFaults(failure, member.fault);
733
+ member.fault = null;
734
+ }
735
+ const losers = members
736
+ .filter((member) => member !== winner)
737
+ .map((member) => {
738
+ const result = member.value;
739
+ member.value = UNIT;
740
+ return result;
741
+ });
742
+ this.closeValueAsync({ kind: "vector", elementType: "race result", items: losers }, this.faultText(failure), (closeFault) => {
743
+ failure = combineFaults(failure, closeFault);
744
+ const finish = (releaseFault = null) => {
745
+ failure = combineFaults(failure, releaseFault);
746
+ this.settleTask(task, failure ? UNIT : value, failure);
747
+ for (const changed of [...scope.changed])
748
+ changed();
749
+ };
750
+ if (failure && winner)
751
+ this.closeValueAsync(value, this.faultText(failure), finish, act.context.actor);
752
+ else
753
+ finish();
754
+ }, act.context.actor);
755
+ };
756
+ task.cancel = () => {
757
+ if (task.done || task.finishing)
758
+ return;
759
+ failure ??= {
760
+ kind: "panic",
761
+ message: "task cancelled",
762
+ cancelled: true,
763
+ };
764
+ selected = true;
765
+ ready();
766
+ };
767
+ for (const member of members)
768
+ member.changed.push(ready);
769
+ this.define(act, op.dst, { kind: "task", id: task.id });
770
+ ready();
771
+ return;
772
+ }
637
773
  case "task.spawn": {
638
774
  const group = act.scopes.get(op.scope);
639
775
  if (!group)
@@ -656,7 +792,9 @@ class ExecutorV1 {
656
792
  if (!fault || (fault.kind === "panic" && fault.cancelled))
657
793
  fault = closeFault ?? fault;
658
794
  this.settleTask(task, value, fault);
659
- if (fault && !(fault.kind === "panic" && fault.cancelled)) {
795
+ if (!task.grouped &&
796
+ fault &&
797
+ !(fault.kind === "panic" && fault.cancelled)) {
660
798
  for (const sibling of group.tasks)
661
799
  if (!sibling.done)
662
800
  sibling.cancel();
@@ -670,6 +808,7 @@ class ExecutorV1 {
670
808
  const context = {
671
809
  id: task.id,
672
810
  actor: act.context.actor,
811
+ requestDeadline: this.requestDeadline(act),
673
812
  returned: (value) => settle(value, null),
674
813
  failed: (fault) => settle(UNIT, fault),
675
814
  };
@@ -783,6 +922,8 @@ class ExecutorV1 {
783
922
  ...term.args.map((operand) => this.boundary(act, operand)),
784
923
  ];
785
924
  this.current = this.activate(this.functionAt(entry.callee), args, act, term.result, term.normal, term.unwind);
925
+ if (entry.receiver_handback)
926
+ this.current.erasedHandback = receiver;
786
927
  return;
787
928
  }
788
929
  case "call": {
@@ -797,7 +938,7 @@ class ExecutorV1 {
797
938
  // `self`. A plain function value carries none.
798
939
  const callee = this.boundary(act, term.callee);
799
940
  const args = term.args.map((operand) => this.boundary(act, operand));
800
- this.current = this.activate(this.functionAt(calleeFunction(callee)), callee.kind === "closure" ? [callee.environment, ...args] : args, act, term.result, term.normal, term.unwind);
941
+ this.current = this.activate(this.functionAt(calleeFunction(callee, term.specialization)), callee.kind === "closure" ? [callee.environment, ...args] : args, act, term.result, term.normal, term.unwind);
801
942
  return;
802
943
  }
803
944
  case "actor.call":
@@ -805,6 +946,38 @@ class ExecutorV1 {
805
946
  return;
806
947
  case "runtime.call": {
807
948
  const entry = this.pkg.runtime_families[term.family];
949
+ if (entry?.family === "ActorCaller") {
950
+ const caller = act.context.caller ?? { kind: "Task" };
951
+ const payload = [];
952
+ if (caller.kind === "Actor") {
953
+ const shape = this.pkg.variants[term.result_shape];
954
+ const tag = runtimeTag(shape, "CallerActor");
955
+ payload.push({
956
+ kind: "record",
957
+ typeId: shape.cases[tag].fields[0],
958
+ fields: [{ kind: "i64", value: caller.identity }],
959
+ });
960
+ }
961
+ this.completeShim(act, term, this.variant(term.result_shape, caller.kind === "Actor"
962
+ ? "CallerActor"
963
+ : caller.kind === "Timer"
964
+ ? "CallerTimer"
965
+ : "CallerTask", payload));
966
+ return;
967
+ }
968
+ if (entry?.family === "ActorRefusalLog") {
969
+ const [message, handler, reason] = term.args.map((argument) => this.boundary(act, argument));
970
+ this.trace.snapshot("actor.delivery_refused", {
971
+ actor_id: act.context.actor?.id ?? null,
972
+ handler: toJsonValue(handler),
973
+ message: toJsonValue(message),
974
+ reason: toJsonValue(reason),
975
+ });
976
+ if (!act.context.reply)
977
+ this.trace.writeStderr(`WARN actor.delivery_refused actor=${act.context.actor?.identity ?? 0n} handler=${asString(handler)} reason=${JSON.stringify(asString(reason))}\n`, null);
978
+ this.completeShim(act, term, UNIT);
979
+ return;
980
+ }
808
981
  if (entry &&
809
982
  (entry.family === "Map" ||
810
983
  entry.family === "Set" ||
@@ -866,6 +1039,11 @@ class ExecutorV1 {
866
1039
  this.callShim(act, term, shim);
867
1040
  return;
868
1041
  }
1042
+ case "wire.codec": {
1043
+ const args = term.args.map((operand) => this.boundary(act, operand));
1044
+ this.runValueProgram(act, term, args, executeCodec(this.pkg, term, args[0], this.options.codecBridge, this.options.propertyInput ?? []));
1045
+ return;
1046
+ }
869
1047
  case "value.call": {
870
1048
  const args = term.args.map((operand) => this.boundary(act, operand));
871
1049
  this.runValueProgram(act, term, args, selectedValue(this.pkg, term.plan, args));
@@ -960,8 +1138,10 @@ class ExecutorV1 {
960
1138
  return;
961
1139
  case "unreachable":
962
1140
  throw new Error(`${act.fn.name}: reached an unreachable endpoint`);
963
- default:
964
- throw new Error(`terminator ${term.op} has no sequential executor`);
1141
+ default: {
1142
+ const exhaustive = term;
1143
+ throw new Error(`unhandled terminator: ${String(exhaustive)}`);
1144
+ }
965
1145
  }
966
1146
  }
967
1147
  takeEdge(act, edge) {
@@ -992,6 +1172,7 @@ class ExecutorV1 {
992
1172
  }
993
1173
  catch (error) {
994
1174
  if (error instanceof ShimFault) {
1175
+ this.keepInputs(act, term, args);
995
1176
  this.raiseFault(act, { kind: "trap", trap: error.trap, message: error.message }, term.unwind);
996
1177
  return;
997
1178
  }
@@ -999,6 +1180,20 @@ class ExecutorV1 {
999
1180
  }
1000
1181
  this.completeShim(act, term, value);
1001
1182
  }
1183
+ /// A runtime call whose contract keeps its inputs on failure fails before
1184
+ /// taking them, so each moved argument stays with the caller, whose unwind
1185
+ /// edge stores it back.
1186
+ keepsInputs(term) {
1187
+ return term.op === "runtime.call" && term.keeps_inputs_on_failure === true;
1188
+ }
1189
+ keepInputs(act, term, args) {
1190
+ if (!this.keepsInputs(term))
1191
+ return;
1192
+ term.args.forEach((operand, index) => {
1193
+ if (operand.decision === "move")
1194
+ this.define(act, operand.value, args[index]);
1195
+ });
1196
+ }
1002
1197
  runValueProgram(act, term, args, iterator) {
1003
1198
  this.running = false;
1004
1199
  let initial = true;
@@ -1010,10 +1205,16 @@ class ExecutorV1 {
1010
1205
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1011
1206
  };
1012
1207
  const fail = (fault, extra = UNIT) => {
1013
- const owned = args.filter((_arg, index) => ["move", "copy", "snapshot"].includes(term.args[index].decision));
1208
+ const kept = this.keepsInputs(term) ? ["move"] : [];
1209
+ const owned = args.filter((_arg, index) => {
1210
+ const decision = term.args[index].decision;
1211
+ return (["move", "copy", "snapshot"].includes(decision) &&
1212
+ !kept.includes(decision));
1213
+ });
1014
1214
  this.closeValueAsync({ kind: "vector", elementType: "", items: [...owned, extra] }, this.faultText(fault), (closeFault) => {
1015
1215
  if (fault.kind === "panic" && fault.cancelled && closeFault)
1016
1216
  fault = closeFault;
1217
+ this.keepInputs(act, term, args);
1017
1218
  this.raiseFault(act, fault, term.unwind);
1018
1219
  resume();
1019
1220
  });
@@ -1116,6 +1317,13 @@ class ExecutorV1 {
1116
1317
  if (!act.normal) {
1117
1318
  throw new Error(`${act.fn.name}: returned from a call with a "never" result`);
1118
1319
  }
1320
+ if (act.erasedHandback) {
1321
+ if (value.kind !== "record" || value.fields.length !== 2) {
1322
+ throw new Error(`${act.fn.name}: invalid erased receiver dual return`);
1323
+ }
1324
+ act.erasedHandback.value = value.fields[1];
1325
+ value = value.fields[0];
1326
+ }
1119
1327
  // The result is defined on the normal edge, like a block parameter, so it
1120
1328
  // is in scope for that edge's arguments.
1121
1329
  if (act.result && act.result !== "never") {
@@ -1166,12 +1374,15 @@ class ExecutorV1 {
1166
1374
  }
1167
1375
  // A failing `var self` method hands its receiver back, defined on the
1168
1376
  // unwind edge like a result on the normal edge.
1169
- if ((handback === null) !== !act.handback) {
1377
+ if ((handback === null) !== !(act.handback || act.erasedHandback)) {
1170
1378
  throw new Error(`${act.fn.name}: receiver handback disagrees with its call`);
1171
1379
  }
1172
1380
  if (handback !== null && act.handback) {
1173
1381
  this.define(caller, act.handback.value, handback);
1174
1382
  }
1383
+ if (handback !== null && act.erasedHandback) {
1384
+ act.erasedHandback.value = handback;
1385
+ }
1175
1386
  caller.fault = fault;
1176
1387
  this.current = caller;
1177
1388
  this.takeEdge(caller, act.unwind);
@@ -1305,11 +1516,10 @@ class ExecutorV1 {
1305
1516
  return;
1306
1517
  }
1307
1518
  const failure = group.deadline ??
1308
- group.tasks.find((task) => task.fault &&
1519
+ group.tasks.find((task) => !task.grouped &&
1520
+ task.fault &&
1309
1521
  !(task.fault.kind === "panic" && task.fault.cancelled))?.fault;
1310
- wake(UNIT, mode === "propagate_fault" || mode === "cancel_losers_after_fault"
1311
- ? null
1312
- : (failure ?? null));
1522
+ wake(UNIT, mode === "propagate_fault" ? null : (failure ?? null));
1313
1523
  };
1314
1524
  // Cancellation requests descendant cleanup; it cannot bypass the
1315
1525
  // join barrier and let the parent retire their captured resources.
@@ -1380,7 +1590,12 @@ class ExecutorV1 {
1380
1590
  return;
1381
1591
  }
1382
1592
  case "NativeIo": {
1383
- // Admission passed only `StdinReadLine`. The record is the raw line,
1593
+ if (term.detail?.operation === "SinkFinish") {
1594
+ this.host.pipes.close(this.boundary(act, term.inputs[0]));
1595
+ this.park(act, term)(UNIT);
1596
+ return;
1597
+ }
1598
+ // Admission passed `StdinReadLine`. The record is the raw line,
1384
1599
  // so the records concatenate to the stdin the program consumed.
1385
1600
  const line = this.stdin.readLine() ?? new Uint8Array();
1386
1601
  this.trace.recordReplayInput({ kind: "stdin", data: new TextDecoder().decode(line) }, false);
@@ -1608,6 +1823,7 @@ class ExecutorV1 {
1608
1823
  newTask() {
1609
1824
  const task = {
1610
1825
  id: this.ids.task(),
1826
+ grouped: false,
1611
1827
  done: false,
1612
1828
  finishing: false,
1613
1829
  value: UNIT,
@@ -1700,6 +1916,9 @@ class ExecutorV1 {
1700
1916
  actorCall(act, term) {
1701
1917
  const args = term.args.map((arg) => this.boundary(act, arg));
1702
1918
  const operation = term.operation;
1919
+ const caller = act.context.actor
1920
+ ? { kind: "Actor", identity: act.context.actor.identity }
1921
+ : { kind: "Task" };
1703
1922
  switch (operation.op) {
1704
1923
  case "spawn": {
1705
1924
  const layout = this.pkg.actors?.find((actor) => actor.id === operation.actor);
@@ -1707,6 +1926,7 @@ class ExecutorV1 {
1707
1926
  throw new Error(`actor layout ${operation.actor} is missing`);
1708
1927
  let supplied = 0;
1709
1928
  const actor = {
1929
+ identity: ++this.actorIdentity,
1710
1930
  id: this.ids.actor(),
1711
1931
  layout,
1712
1932
  state: {
@@ -1715,6 +1935,7 @@ class ExecutorV1 {
1715
1935
  fields: layout.state_fields.map((field) => field.deferred ? UNIT : (args[supplied++] ?? UNIT)),
1716
1936
  },
1717
1937
  mailbox: [],
1938
+ disposals: [],
1718
1939
  admission: [],
1719
1940
  busy: true,
1720
1941
  alive: true,
@@ -1789,9 +2010,11 @@ class ExecutorV1 {
1789
2010
  if (!handler)
1790
2011
  throw new Error("stream request has no producer handler");
1791
2012
  actor.mailbox.push({
2013
+ caller,
1792
2014
  handler,
1793
2015
  payload: fields,
1794
2016
  reply: false,
2017
+ obligated: true,
1795
2018
  complete: (_value, error, drained) => {
1796
2019
  if (error)
1797
2020
  this.closeValue(payload, error);
@@ -1871,13 +2094,13 @@ class ExecutorV1 {
1871
2094
  }
1872
2095
  const full = actor.layout.mailbox_capacity !== undefined &&
1873
2096
  actor.mailbox.length >= actor.layout.mailbox_capacity;
1874
- // The destination's declaration answers a full queue before the
1875
- // sender's policy: a coalescing mailbox supersedes the queued
1876
- // message with the same key, then its fallback or a declared
1877
- // `drop_new` / `drop_old` resolves the rest. Either loss reports a
1878
- // discard; only a declaration that refuses reaches the sender.
1879
- // A discard completes in this turn unless an authored close
1880
- // suspends: the sender keeps its turn, as it does natively.
2097
+ // Declared loss transfers cleanup to the receiver. Sender-selected
2098
+ // loss still closes the sender's unadmitted request in its own frame.
2099
+ const discardAtReceiver = (value, report) => {
2100
+ actor.disposals.push(value);
2101
+ this.dispatchActor(actor);
2102
+ finish(report);
2103
+ };
1881
2104
  const discard = (value, report) => {
1882
2105
  let settled = false;
1883
2106
  let waiting = false;
@@ -1900,7 +2123,7 @@ class ExecutorV1 {
1900
2123
  if (declared === "coalesce" && coalesce) {
1901
2124
  const key = coalesce.keys.find((key) => key.message === handler.message_id);
1902
2125
  const queued = key
1903
- ? actor.mailbox.findIndex((message) => !message.reply &&
2126
+ ? actor.mailbox.findIndex((message) => !message.obligated &&
1904
2127
  message.handler.message_id === handler.message_id &&
1905
2128
  equals(message.payload[key.param], payload.fields[key.param]))
1906
2129
  : -1;
@@ -1908,32 +2131,35 @@ class ExecutorV1 {
1908
2131
  const superseded = actor.mailbox[queued];
1909
2132
  actor.mailbox[queued] = {
1910
2133
  ...superseded,
2134
+ caller,
1911
2135
  payload: payload.fields,
1912
2136
  };
1913
- discard({ kind: "record", typeId: "", fields: superseded.payload }, accepted(true));
2137
+ discardAtReceiver({ kind: "record", typeId: "", fields: superseded.payload }, accepted(false));
1914
2138
  return;
1915
2139
  }
1916
2140
  declared = coalesce.fallback;
1917
2141
  }
1918
2142
  if (declared === "drop_new") {
1919
- discard(payload, accepted(true));
2143
+ discardAtReceiver(payload, accepted(true));
1920
2144
  return;
1921
2145
  }
1922
2146
  // A queued completion call owes its caller a reply, so `drop_old`
1923
2147
  // evicts only a one-way message and refuses when there is none.
1924
2148
  const oldest = declared === "drop_old"
1925
- ? actor.mailbox.findIndex((message) => !message.reply)
2149
+ ? actor.mailbox.findIndex((message) => !message.obligated)
1926
2150
  : -1;
1927
2151
  if (oldest >= 0) {
1928
2152
  const [evicted] = actor.mailbox.splice(oldest, 1);
1929
2153
  actor.mailbox.push({
2154
+ caller,
1930
2155
  handler,
1931
2156
  payload: payload.fields,
1932
2157
  reply: false,
2158
+ obligated: false,
1933
2159
  complete: (_value, _error, drained) => drained?.(null),
1934
2160
  });
1935
2161
  this.dispatchActor(actor);
1936
- discard({ kind: "record", typeId: "", fields: evicted.payload }, accepted(false));
2162
+ discardAtReceiver({ kind: "record", typeId: "", fields: evicted.payload }, accepted(false));
1937
2163
  return;
1938
2164
  }
1939
2165
  if (full) {
@@ -1955,9 +2181,11 @@ class ExecutorV1 {
1955
2181
  return;
1956
2182
  }
1957
2183
  actor.mailbox.push({
2184
+ caller,
1958
2185
  handler,
1959
2186
  payload: payload.fields,
1960
2187
  reply: false,
2188
+ obligated: false,
1961
2189
  complete: (_value, _error, drained) => drained?.(null),
1962
2190
  });
1963
2191
  this.trace.snapshot("actor.send", {
@@ -2142,15 +2370,32 @@ class ExecutorV1 {
2142
2370
  throw new Error(`actor operation ${operation.op} has no executor`);
2143
2371
  }
2144
2372
  }
2373
+ requestDeadline(act) {
2374
+ let deadline = act.context.requestDeadline;
2375
+ for (let frame = act; frame; frame = frame.caller) {
2376
+ for (const group of frame.scopes.values()) {
2377
+ if (group.expiresAt !== undefined)
2378
+ deadline = Math.min(deadline ?? Infinity, group.expiresAt);
2379
+ }
2380
+ }
2381
+ return deadline;
2382
+ }
2145
2383
  ask(protocol, inputs, complete, shapes) {
2146
2384
  let settled = false;
2147
2385
  let admitted = false;
2148
2386
  let closing = false;
2149
2387
  const owner = this.current.context.actor;
2388
+ const deadline = this.requestDeadline(this.current);
2389
+ let cancelTimer;
2390
+ let message;
2391
+ const caller = owner
2392
+ ? { kind: "Actor", identity: owner.identity }
2393
+ : { kind: "Task" };
2150
2394
  let receiver = null;
2151
2395
  const finish = (value, error, drained) => {
2152
2396
  const alreadySettled = settled;
2153
2397
  settled = true;
2398
+ cancelTimer?.();
2154
2399
  if (alreadySettled || complete(value, error) === false) {
2155
2400
  // The finishing receiver still owns a reply that its caller no
2156
2401
  // longer accepts. Its next turn must wait for this release.
@@ -2194,12 +2439,17 @@ class ExecutorV1 {
2194
2439
  if (settled)
2195
2440
  return;
2196
2441
  settled = true;
2442
+ cancelTimer?.();
2197
2443
  complete(null, reason, fault);
2198
2444
  });
2199
2445
  };
2200
2446
  const attempt = () => {
2201
2447
  if (settled || closing)
2202
2448
  return;
2449
+ if (deadline !== undefined && deadline <= this.trace.virtualTimeMs) {
2450
+ reject("TimedOut");
2451
+ return;
2452
+ }
2203
2453
  const role = "id" in target ? this.roles.get(target.id) : undefined;
2204
2454
  if (role && this.rolePending(role)) {
2205
2455
  role.waiting.push(attempt);
@@ -2252,17 +2502,50 @@ class ExecutorV1 {
2252
2502
  }
2253
2503
  admitted = true;
2254
2504
  receiver = actor;
2255
- actor.mailbox.push({ handler, payload, complete: finish, reply: true });
2505
+ message = {
2506
+ caller,
2507
+ handler,
2508
+ payload,
2509
+ complete: finish,
2510
+ reply: true,
2511
+ obligated: true,
2512
+ requestDeadline: deadline,
2513
+ };
2514
+ actor.mailbox.push(message);
2256
2515
  this.trace.snapshot("actor.ask", {
2257
2516
  actor_id: actor.id,
2258
2517
  handler: handler.name,
2259
2518
  });
2260
2519
  this.dispatchActor(actor);
2261
2520
  };
2521
+ const withdrawQueued = () => {
2522
+ if (!message || !receiver)
2523
+ return;
2524
+ const index = receiver.mailbox.indexOf(message);
2525
+ if (index < 0)
2526
+ return;
2527
+ receiver.mailbox.splice(index, 1);
2528
+ admitted = false;
2529
+ for (const admit of receiver.admission.splice(0))
2530
+ admit();
2531
+ };
2532
+ if (deadline !== undefined) {
2533
+ cancelTimer = this.scheduler.after(BigInt(Math.max(0, Math.round((deadline - this.trace.virtualTimeMs) * 1_000_000))), () => {
2534
+ if (settled || closing)
2535
+ return;
2536
+ withdrawQueued();
2537
+ if (admitted)
2538
+ finish(null, "TimedOut");
2539
+ else
2540
+ reject("TimedOut");
2541
+ });
2542
+ }
2262
2543
  attempt();
2263
2544
  return (done) => {
2264
2545
  const wasSettled = settled;
2265
2546
  settled = true;
2547
+ cancelTimer?.();
2548
+ withdrawQueued();
2266
2549
  if (admitted || (wasSettled && !closing))
2267
2550
  done(null);
2268
2551
  else
@@ -2272,6 +2555,18 @@ class ExecutorV1 {
2272
2555
  dispatchActor(actor) {
2273
2556
  if (actor.busy || !actor.alive || actor.drainingMailbox)
2274
2557
  return;
2558
+ if (actor.disposals.length > 0) {
2559
+ actor.busy = true;
2560
+ const values = actor.disposals.splice(0);
2561
+ this.closeValueAsync({ kind: "vector", elementType: "message", items: values }, this.faultText(actor.crashing), (fault) => {
2562
+ actor.busy = false;
2563
+ if (fault)
2564
+ this.crashActor(actor, fault);
2565
+ else
2566
+ this.dispatchActor(actor);
2567
+ }, actor);
2568
+ return;
2569
+ }
2275
2570
  if (actor.terminating && actor.mailbox.length > 0) {
2276
2571
  this.drainMailbox(actor, this.faultText(actor.closingFault), (fault) => {
2277
2572
  actor.closingFault = combineFaults(actor.closingFault, fault);
@@ -2301,6 +2596,18 @@ class ExecutorV1 {
2301
2596
  }
2302
2597
  for (const admit of actor.admission.splice(0))
2303
2598
  admit();
2599
+ if (message.requestDeadline !== undefined &&
2600
+ message.requestDeadline <= this.trace.virtualTimeMs) {
2601
+ this.closeValueAsync({ kind: "record", typeId: "", fields: message.payload }, null, (fault) => {
2602
+ message.complete(null, "TimedOut");
2603
+ actor.busy = false;
2604
+ if (fault)
2605
+ this.crashActor(actor, fault);
2606
+ else
2607
+ this.dispatchActor(actor);
2608
+ }, actor);
2609
+ return;
2610
+ }
2304
2611
  this.trace.snapshot("actor.receive", {
2305
2612
  actor_id: actor.id,
2306
2613
  handler: message.handler.name,
@@ -2308,6 +2615,9 @@ class ExecutorV1 {
2308
2615
  const frame = this.activate(this.functionAt(message.handler.callable), [actor.state, ...message.payload], null, null, undefined, null, {
2309
2616
  id: actor.id,
2310
2617
  actor,
2618
+ caller: message.caller,
2619
+ requestDeadline: message.requestDeadline,
2620
+ reply: message.reply,
2311
2621
  returned: (value) => {
2312
2622
  actor.active = null;
2313
2623
  const drained = (fault) => {
@@ -2345,7 +2655,14 @@ class ExecutorV1 {
2345
2655
  failed: (fault) => {
2346
2656
  actor.active = null;
2347
2657
  actor.busy = false;
2348
- if (fault.kind === "panic" && fault.cancelled) {
2658
+ if (fault.kind === "trap" &&
2659
+ fault.trap === "actor_refused" &&
2660
+ !fault.secondary?.length) {
2661
+ actor.crashing = null;
2662
+ message.complete(null, "Refused");
2663
+ this.dispatchActor(actor);
2664
+ }
2665
+ else if (fault.kind === "panic" && fault.cancelled) {
2349
2666
  message.complete(null, "Dead");
2350
2667
  this.dispatchActor(actor);
2351
2668
  }
@@ -2649,11 +2966,14 @@ class ExecutorV1 {
2649
2966
  this.closeValueAsync({
2650
2967
  kind: "vector",
2651
2968
  elementType: "message",
2652
- items: messages.map((message) => ({
2653
- kind: "record",
2654
- typeId: "",
2655
- fields: message.payload,
2656
- })),
2969
+ items: [
2970
+ ...actor.disposals.splice(0),
2971
+ ...messages.map((message) => ({
2972
+ kind: "record",
2973
+ typeId: "",
2974
+ fields: message.payload,
2975
+ })),
2976
+ ],
2657
2977
  }, fault, (closeFault) => {
2658
2978
  actor.drainingMailbox = false;
2659
2979
  for (const message of messages)
@@ -2784,8 +3104,10 @@ function combineFaults(primary, secondary) {
2784
3104
  // Reports own an immutable ordered view of original diagnostics. Faults can
2785
3105
  // be shared by a parked unwind, actor completion and multiple observers;
2786
3106
  // combining those views must neither duplicate nor mutate their graph.
2787
- const retained = primary.kind === "panic" && primary.cancelled
2788
- ? diagnostics.filter((fault) => !(fault.kind === "panic" && fault.cancelled))
3107
+ const control = (fault) => (fault.kind === "panic" && fault.cancelled) ||
3108
+ (fault.kind === "trap" && fault.trap === "actor_refused");
3109
+ const retained = control(primary)
3110
+ ? diagnostics.filter((fault) => !control(fault))
2789
3111
  : diagnostics;
2790
3112
  const [first, ...rest] = retained.length ? retained : diagnostics;
2791
3113
  return { ...first, primary: first, secondary: rest };
@@ -2803,6 +3125,7 @@ function faultInfo(fault) {
2803
3125
  divide_by_zero: [202, "DivideByZero"],
2804
3126
  shift_out_of_range: [204, "ShiftOutOfRange"],
2805
3127
  vector_bounds: [205, "IndexOutOfBounds"],
3128
+ actor_refused: [-4, "Refused"],
2806
3129
  }[fault.trap];
2807
3130
  }
2808
3131
  function faultReport(fault, secondary = false) {
@@ -2872,6 +3195,8 @@ function actorErrorRole(reason) {
2872
3195
  return "ActorErrorFailed";
2873
3196
  case "Trapped":
2874
3197
  return "ActorErrorTrapped";
3198
+ case "Refused":
3199
+ return "ActorErrorRefused";
2875
3200
  case "Dead":
2876
3201
  return "ActorErrorDead";
2877
3202
  case "TimedOut":
@@ -2906,8 +3231,6 @@ function sendErrorRole(reason) {
2906
3231
  return "SendErrorCancelled";
2907
3232
  case "VersionMismatch":
2908
3233
  return "SendErrorVersionMismatch";
2909
- case "Unauthorized":
2910
- return "SendErrorUnauthorized";
2911
3234
  case "Backpressure":
2912
3235
  return "SendErrorBackpressure";
2913
3236
  case "Dead":
@@ -2916,12 +3239,17 @@ function sendErrorRole(reason) {
2916
3239
  throw new Error(`unknown send error reason ${reason}`);
2917
3240
  }
2918
3241
  }
2919
- function calleeFunction(value) {
3242
+ function calleeFunction(value, specialization = null) {
2920
3243
  switch (value.kind) {
2921
3244
  case "function":
2922
3245
  return Number(value.id);
2923
- case "closure":
2924
- return value.body;
3246
+ case "closure": {
3247
+ const selected = specialization ?? value.specialization;
3248
+ const entry = value.entries.find((candidate) => candidate.specialization === selected);
3249
+ if (!entry)
3250
+ throw new Error("callable has no selected concrete invocation entry");
3251
+ return entry.body;
3252
+ }
2925
3253
  default:
2926
3254
  throw new TypeError(`indirect.call expected a callable value, got ${value.kind}`);
2927
3255
  }
@@ -2998,6 +3326,8 @@ function trapMessage(trap) {
2998
3326
  return "shift amount out of range";
2999
3327
  case "vector_bounds":
3000
3328
  return "index out of bounds";
3329
+ case "actor_refused":
3330
+ return "delivery refused by interceptor";
3001
3331
  }
3002
3332
  }
3003
3333
  /// The operand type the package records. `isize` and `usize` are 64-bit in the
@@ -3104,6 +3434,21 @@ function applyBinary(op, lhs, rhs, ty) {
3104
3434
  return { kind: "bool", value: truth(lhs) && truth(rhs) };
3105
3435
  case "Or":
3106
3436
  return { kind: "bool", value: truth(lhs) || truth(rhs) };
3437
+ case "BitAnd":
3438
+ case "BitOr":
3439
+ case "BitXor":
3440
+ // Both operands were already evaluated; bool operands stay bool.
3441
+ if (lhs.kind === "bool" && rhs.kind === "bool") {
3442
+ return {
3443
+ kind: "bool",
3444
+ value: op === "BitAnd"
3445
+ ? lhs.value && rhs.value
3446
+ : op === "BitOr"
3447
+ ? lhs.value || rhs.value
3448
+ : lhs.value !== rhs.value,
3449
+ };
3450
+ }
3451
+ break;
3107
3452
  default:
3108
3453
  break;
3109
3454
  }