@hew-lang/sandbox-vm 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +43 -4
  2. package/cli/test-runner.mjs +4 -1
  3. package/dist/interpreter/interpreter.d.ts +1 -1
  4. package/dist/interpreter/interpreter.js +1 -1
  5. package/dist/interpreter/parity-runner.js +26 -1
  6. package/dist/interpreter/run-program.d.ts +3 -2
  7. package/dist/interpreter/run-program.js +5 -3
  8. package/dist/interpreter/schema-validator.d.ts +9 -4
  9. package/dist/interpreter/schema-validator.js +15 -49
  10. package/dist/interpreter/types.d.ts +4 -0
  11. package/dist/interpreter/v1/browser-directory.d.ts +107 -0
  12. package/dist/interpreter/v1/browser-directory.js +288 -0
  13. package/dist/interpreter/v1/calendar.d.ts +4 -0
  14. package/dist/interpreter/v1/calendar.js +64 -0
  15. package/dist/interpreter/v1/codecs.js +2 -2
  16. package/dist/interpreter/v1/exec.d.ts +2 -2
  17. package/dist/interpreter/v1/exec.js +439 -183
  18. package/dist/interpreter/v1/instructions.generated.d.ts +1135 -0
  19. package/dist/interpreter/v1/instructions.generated.js +106 -0
  20. package/dist/interpreter/v1/package-graph.d.ts +2 -0
  21. package/dist/interpreter/v1/package-graph.js +729 -0
  22. package/dist/interpreter/v1/package-validator.generated.d.ts +3 -0
  23. package/dist/interpreter/v1/package-validator.generated.js +75846 -0
  24. package/dist/interpreter/v1/package.d.ts +5 -680
  25. package/dist/interpreter/v1/package.js +0 -12
  26. package/dist/interpreter/v1/scheduler.d.ts +5 -1
  27. package/dist/interpreter/v1/scheduler.js +32 -3
  28. package/dist/interpreter/v1/shims.d.ts +250 -7
  29. package/dist/interpreter/v1/shims.js +429 -150
  30. package/dist/interpreter/v1/structural.d.ts +2 -28
  31. package/dist/interpreter/v1/timestamp.d.ts +7 -0
  32. package/dist/interpreter/v1/timestamp.js +54 -0
  33. package/dist/interpreter/v1/validate.d.ts +8 -2
  34. package/dist/interpreter/v1/validate.js +190 -202
  35. package/dist/interpreter/v1/value-operations.d.ts +7 -2
  36. package/dist/interpreter/v1/value-operations.js +13 -6
  37. package/dist/interpreter/values.d.ts +4 -0
  38. package/dist/interpreter/values.js +6 -0
  39. package/dist/interpreter/worker.js +4 -3
  40. package/package.json +4 -4
@@ -1,3 +1,4 @@
1
+ import { BrowserDirectories, } from "./browser-directory.js";
1
2
  /// The `hew.sandbox.bytecode.v1` executor.
2
3
  ///
3
4
  /// The package is a projection of verified ownership SIR, so this executor
@@ -12,13 +13,14 @@ import { TraceBuilder, runtimeFailure } from "../trace.js";
12
13
  import { UNIT, cloneValue, stringValue, toJsonValue, } from "../values.js";
13
14
  import { runtimeTag } from "./package.js";
14
15
  import { StructuralRenderer } from "./structural.js";
16
+ import { timestampBinary } from "./timestamp.js";
15
17
  import { selectedValue, collectionOperation, sharedOperation, } from "./value-operations.js";
16
18
  import { Pipes } from "./pipes.js";
17
19
  import { FrameScheduler } from "./scheduler.js";
18
20
  import { DeterministicIds } from "../../scheduler/ids.js";
19
21
  import { Mt19937 } from "./mt19937.js";
20
- import { ShimFault, decodeUtf8Checked, resolveExternShim, resolveRuntimeShim, } from "./shims.js";
21
- import { admitPackage } from "./validate.js";
22
+ import { ShimFault, CODEC_REGISTRATION, decodeUtf8Checked, resolveExtern, resolveRuntime, resolveSuspension, } from "./shims.js";
23
+ import { AdmittedPackage } from "./validate.js";
22
24
  const DEFAULT_STEP_BUDGET = 1_000_000;
23
25
  const DEFAULT_ACTOR_STOP_NS = 5_000_000_000;
24
26
  /// Ends the run. The status is already recorded on the trace.
@@ -55,7 +57,7 @@ class StdinReader {
55
57
  return this.bytes.slice(start, this.offset);
56
58
  }
57
59
  }
58
- export function runPackageV1(pkg, options = {}) {
60
+ export async function runPackageV1(pkg, options = {}) {
59
61
  const fixtureId = options.fixtureId ?? "package";
60
62
  const replay = {
61
63
  seed: options.replay?.seed ?? 0,
@@ -68,14 +70,25 @@ export function runPackageV1(pkg, options = {}) {
68
70
  const trace = new TraceBuilder(pkg, fixtureId, options.traceId ?? `trace:${fixtureId}`, replay, options.sandboxVersion ?? "0.0.0-spec");
69
71
  // Admission is a load-time decision: nothing runs when the package names a
70
72
  // capability the VM has no shim for.
71
- const rejection = admitPackage(pkg);
72
- if (rejection) {
73
- trace.reject(rejection);
73
+ const admission = AdmittedPackage.load(pkg, options);
74
+ if (!(admission instanceof AdmittedPackage)) {
75
+ trace.reject(admission);
74
76
  return trace.finish();
75
77
  }
76
- const vm = new ExecutorV1(pkg, trace, StdinReader.fromReplay(replay.inputs), options.schedulerPolicy ?? "round_robin", options);
78
+ const directoryRefusal = await BrowserDirectories.admission(options.directoryRoots);
79
+ if (directoryRefusal) {
80
+ trace.reject({
81
+ category: "not_implemented",
82
+ code: "sandbox.host.directory",
83
+ capability: "directoryRoots",
84
+ message: directoryRefusal,
85
+ span: null,
86
+ });
87
+ return trace.finish();
88
+ }
89
+ const vm = new ExecutorV1(admission, trace, StdinReader.fromReplay(replay.inputs), options.schedulerPolicy ?? "round_robin", options);
77
90
  try {
78
- vm.run();
91
+ await vm.run();
79
92
  }
80
93
  catch (error) {
81
94
  if (!(error instanceof Halt)) {
@@ -86,7 +99,6 @@ export function runPackageV1(pkg, options = {}) {
86
99
  return trace.finish();
87
100
  }
88
101
  class ExecutorV1 {
89
- pkg;
90
102
  trace;
91
103
  stdin;
92
104
  options;
@@ -111,16 +123,39 @@ class ExecutorV1 {
111
123
  consumedResources = new WeakSet();
112
124
  running = false;
113
125
  rootComplete = false;
126
+ terminating = false;
114
127
  rootFault = null;
115
- constructor(pkg, trace, stdin, policy, options) {
116
- this.pkg = pkg;
128
+ pkg;
129
+ directories;
130
+ constructor(admission, trace, stdin, policy, options) {
117
131
  this.trace = trace;
118
132
  this.stdin = stdin;
119
133
  this.options = options;
134
+ const pkg = admission.bytecode;
135
+ this.pkg = pkg;
136
+ this.directories = new BrowserDirectories(options.directoryRoots);
120
137
  this.scheduler = new FrameScheduler(trace, policy);
121
138
  this.ids = new DeterministicIds(trace.replay.seed);
122
139
  this.host = {
123
140
  argv: [...(options.argv ?? ["main.hew"])],
141
+ unixTimeMillis: () => this.trace.virtualTimeMs,
142
+ calendarBridge: options.calendarBridge,
143
+ fileError: (operation) => {
144
+ const error = this.current.context.fileError;
145
+ switch (operation) {
146
+ case "HasError":
147
+ return { kind: "bool", value: error != null };
148
+ case "LastErrorKind":
149
+ return { kind: "i64", value: BigInt(error?.kind ?? 0) };
150
+ case "LastErrno":
151
+ if (error)
152
+ error.kind = 0;
153
+ return { kind: "i64", value: 0n };
154
+ case "LastError":
155
+ this.current.context.fileError = null;
156
+ return stringValue(error?.message ?? "");
157
+ }
158
+ },
124
159
  pipes: this.pipes,
125
160
  generatorStream: (value) => {
126
161
  const id = this.ids.channel();
@@ -150,7 +185,6 @@ class ExecutorV1 {
150
185
  : this.pipes.cloneSink(value);
151
186
  },
152
187
  newPipe: (capacity) => this.newPipe(capacity),
153
- closePipe: (value) => this.pipes.close(value, this.pipeFault(this.current)),
154
188
  releaseValue: (value) => this.closeValue(value, this.pipeFault(this.current)),
155
189
  writeStdout: (text) => this.trace.writeStdout(text, null),
156
190
  writeStderr: (text) => this.trace.writeStderr(text, null),
@@ -183,24 +217,47 @@ class ExecutorV1 {
183
217
  }
184
218
  /// One instruction or terminator executed is one step, ownership ops
185
219
  /// included: the package's ops are what the program does.
186
- run() {
187
- this.runFrame(this.current);
188
- this.scheduler.run();
189
- if (!this.rootComplete) {
190
- throw new Error("the root frame is waiting with no runnable work");
220
+ async run() {
221
+ const root = this.current.context;
222
+ const cancel = () => {
223
+ const fault = {
224
+ kind: "panic",
225
+ message: "execution cancelled",
226
+ cancelled: true,
227
+ };
228
+ if (root.cancel)
229
+ root.cancel(fault);
230
+ else
231
+ root.pendingCancellation ??= fault;
232
+ };
233
+ this.options.signal?.addEventListener("abort", cancel, { once: true });
234
+ if (this.options.signal?.aborted)
235
+ cancel();
236
+ try {
237
+ this.runFrame(this.current);
238
+ await this.scheduler.run();
239
+ if (!this.rootComplete) {
240
+ throw new Error("the root frame is waiting with no runnable work");
241
+ }
242
+ // Native main return drains accepted work before closing idle actors.
243
+ for (const owner of this.supervisors.values())
244
+ if (!owner.supervisor)
245
+ this.stopSupervisor(owner);
246
+ for (const actor of this.actors.values())
247
+ if (!actor.supervisor && actor.alive)
248
+ this.requestActorStop(actor);
249
+ await this.scheduler.run();
250
+ if (this.rootFault)
251
+ this.haltWithFault(this.rootFault);
252
+ if (this.trace.exitCode === 0 && this.faultDebts.size)
253
+ this.trace.exitCode = 1;
254
+ }
255
+ finally {
256
+ this.terminating = true;
257
+ await this.scheduler.retireHost();
258
+ this.directories.retire((value) => this.closeHostResource(value));
259
+ this.options.signal?.removeEventListener("abort", cancel);
191
260
  }
192
- // Native main return drains accepted work before closing idle actors.
193
- for (const owner of this.supervisors.values())
194
- if (!owner.supervisor)
195
- this.stopSupervisor(owner);
196
- for (const actor of this.actors.values())
197
- if (!actor.supervisor && actor.alive)
198
- this.requestActorStop(actor);
199
- this.scheduler.run();
200
- if (this.rootFault)
201
- this.haltWithFault(this.rootFault);
202
- if (this.trace.exitCode === 0 && this.faultDebts.size)
203
- this.trace.exitCode = 1;
204
261
  }
205
262
  runFrame(frame) {
206
263
  this.current = frame;
@@ -241,7 +298,7 @@ class ExecutorV1 {
241
298
  }
242
299
  // ── frames ───────────────────────────────────────────────────────────────
243
300
  activate(fn, args, caller, result, normal, unwind, context) {
244
- const mandatoryClose = (this.pkg.resources ?? []).some((resource) => resource.close === fn.id);
301
+ const mandatoryClose = (this.pkg.resources ?? []).some((resource) => "close" in resource && resource.close === fn.id);
245
302
  const parentContext = caller?.context ?? context;
246
303
  parentContext.pollBudget ??= { remaining: 0 };
247
304
  if (mandatoryClose && args[0])
@@ -268,7 +325,7 @@ class ExecutorV1 {
268
325
  for (const block of fn.blocks) {
269
326
  block.params.forEach(record);
270
327
  for (const op of block.ops) {
271
- if (op.dst != null && op.own === "none")
328
+ if ("dst" in op && op.dst != null && op.own === "none")
272
329
  trivial.add(op.dst);
273
330
  }
274
331
  const term = block.term;
@@ -297,7 +354,7 @@ class ExecutorV1 {
297
354
  parked: [],
298
355
  caller,
299
356
  result,
300
- normal,
357
+ normal: normal ?? undefined,
301
358
  unwind,
302
359
  };
303
360
  }
@@ -510,7 +567,7 @@ class ExecutorV1 {
510
567
  case "aggregate.make":
511
568
  this.define(act, op.dst, {
512
569
  kind: "record",
513
- typeId: this.aggregateName(op.shape),
570
+ typeId: op.shape === null ? "" : this.aggregateName(op.shape),
514
571
  shape: op.shape ?? undefined,
515
572
  fields: op.fields.map((id) => this.read(act, id)),
516
573
  });
@@ -977,8 +1034,12 @@ class ExecutorV1 {
977
1034
  this.define(act, op.dst, { kind: "task", id: task.id });
978
1035
  return;
979
1036
  }
980
- default:
981
- throw new Error(`opcode ${op.op} has no sequential executor`);
1037
+ case "actor.ingress_adapter":
1038
+ throw new Error("actor ingress adapters are not admitted in the VM");
1039
+ default: {
1040
+ const exhaustive = op;
1041
+ throw new Error("unhandled instruction: " + String(exhaustive));
1042
+ }
982
1043
  }
983
1044
  }
984
1045
  /// Hand each field of a consumed container, or each payload of a matched
@@ -1028,6 +1089,20 @@ class ExecutorV1 {
1028
1089
  case "return":
1029
1090
  this.returnFrom(act, term.value ? this.boundary(act, term.value) : UNIT);
1030
1091
  return;
1092
+ case "timestamp.binary": {
1093
+ const outcome = timestampBinary(this.pkg, term, this.read(act, term.lhs), this.read(act, term.rhs));
1094
+ if (outcome === null) {
1095
+ const failure = term.failures.find((edge) => edge.trap === "integer_overflow");
1096
+ if (!failure)
1097
+ throw new Error("timestamp arithmetic lacks its overflow edge");
1098
+ this.takeEdge(act, failure.edge);
1099
+ }
1100
+ else {
1101
+ this.define(act, term.result.value, outcome);
1102
+ this.takeEdge(act, term.normal);
1103
+ }
1104
+ return;
1105
+ }
1031
1106
  case "checked.binary": {
1032
1107
  const failure = checkedFailure(term.binary_op, this.read(act, term.lhs), this.read(act, term.rhs), term.ty);
1033
1108
  if (failure) {
@@ -1080,132 +1155,128 @@ class ExecutorV1 {
1080
1155
  return;
1081
1156
  case "runtime.call": {
1082
1157
  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
- }
1115
- if (entry &&
1116
- (entry.family === "Map" ||
1117
- entry.family === "Set" ||
1118
- (entry.family === "Array" && entry.detail === "Set") ||
1119
- (entry.family === "Vector" &&
1120
- ["Contains", "Clear", "Set"].includes(String(entry.detail))))) {
1121
- const args = term.args.map((operand) => this.boundary(act, operand));
1122
- const iterator = collectionOperation(this.pkg, entry.family === "Array" ? "Vector" : entry.family, String(entry.detail), args, term.callbacks ?? [], term.result_member_shapes?.[1] ?? term.result_shape);
1123
- this.runValueProgram(act, term, args, iterator);
1124
- return;
1125
- }
1126
- if (entry &&
1127
- [
1128
- "RcNew",
1129
- "RcClone",
1130
- "RcDowngrade",
1131
- "WeakCloneRc",
1132
- "WeakUpgradeRc",
1133
- "RcGet",
1134
- "RcIsUnique",
1135
- "RcSet",
1136
- "RcStrongCount",
1137
- "RcWeakCount",
1138
- ].includes(entry.family)) {
1139
- const args = term.args.map((operand) => this.boundary(act, operand));
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] : [])));
1141
- return;
1142
- }
1143
- if (entry?.family === "SupervisorPool") {
1144
- this.supervisorPool(act, term, String(entry.detail));
1145
- return;
1146
- }
1147
- if (entry && ["SinkClose", "StreamClose"].includes(entry.family)) {
1148
- const value = this.boundary(act, term.args[0]);
1149
- this.running = false;
1150
- this.closeValueAsync(value, this.pipeFault(act), (fault) => {
1151
- if (fault)
1152
- this.raiseFault(act, fault, term.unwind);
1153
- else
1154
- this.completeShim(act, term, UNIT);
1155
- this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1156
- }, act.context.actor, act.rcOwner);
1158
+ if (!entry)
1159
+ throw new Error("runtime family is absent after admission");
1160
+ const registration = resolveRuntime(entry);
1161
+ if (registration.kind === "ordinary") {
1162
+ this.callShim(act, term, registration.shim);
1157
1163
  return;
1158
1164
  }
1159
- if (entry?.family === "StructuralFormat") {
1160
- this.structuralFormat(act, term);
1161
- return;
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
- {
1165
+ if (registration.kind === "refused")
1166
+ throw new Error(`runtime family ${registration.capability} was refused after admission`);
1167
+ switch (registration.handler) {
1168
+ case "actor_caller": {
1169
+ const caller = act.context.caller ?? { kind: "Task" };
1170
+ const payload = [];
1171
+ if (caller.kind === "Actor") {
1172
+ const shape = this.pkg.variants[term.result_shape];
1173
+ const tag = runtimeTag(shape, "CallerActor");
1174
+ payload.push({
1175
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
- ]);
1176
+ typeId: shape.cases[tag].fields[0],
1177
+ fields: [{ kind: "i64", value: caller.identity }],
1178
+ });
1179
+ }
1180
+ this.completeShim(act, term, this.variant(term.result_shape, caller.kind === "Actor"
1181
+ ? "CallerActor"
1182
+ : caller.kind === "Timer"
1183
+ ? "CallerTimer"
1184
+ : "CallerTask", payload));
1185
+ return;
1186
+ }
1187
+ case "actor_refusal_log": {
1188
+ const [message, handler, reason] = term.args.map((argument) => this.boundary(act, argument));
1189
+ this.trace.snapshot("actor.delivery_refused", {
1190
+ actor_id: act.context.actor?.id ?? null,
1191
+ handler: toJsonValue(handler),
1192
+ message: toJsonValue(message),
1193
+ reason: toJsonValue(reason),
1194
+ });
1195
+ if (!act.context.reply)
1196
+ this.trace.writeStderr(`WARN actor.delivery_refused actor=${act.context.actor?.identity ?? 0n} handler=${asString(handler)} reason=${JSON.stringify(asString(reason))}\n`, null);
1197
+ this.completeShim(act, term, UNIT);
1198
+ return;
1199
+ }
1200
+ case "collection": {
1201
+ const args = term.args.map((operand) => this.boundary(act, operand));
1202
+ const iterator = collectionOperation(this.pkg, registration, args, term.callbacks ?? [], term.result_member_shapes?.[1] ?? term.result_shape);
1203
+ this.runValueProgram(act, term, args, iterator);
1204
+ return;
1205
+ }
1206
+ case "shared": {
1207
+ const args = term.args.map((operand) => this.boundary(act, operand));
1208
+ this.runValueProgram(act, term, args, sharedOperation(registration.operation, args, act.rcOwner, term.shared_payload_type ?? "", (value) => this.variant(term.result_shape, value ? "OptionSome" : "OptionNone", value ? [value] : [])));
1209
+ return;
1210
+ }
1211
+ case "supervisor_pool": {
1212
+ this.supervisorPool(act, term, registration.operation);
1213
+ return;
1214
+ }
1215
+ case "close": {
1216
+ const value = this.boundary(act, term.args[0]);
1217
+ this.running = false;
1218
+ this.closeValueAsync(value, this.pipeFault(act), (fault) => {
1219
+ if (fault)
1220
+ this.raiseFault(act, fault, term.unwind);
1221
+ else
1222
+ this.completeShim(act, term, UNIT);
1223
+ this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1224
+ }, act.context.actor, act.rcOwner);
1225
+ return;
1226
+ }
1227
+ case "structural_format": {
1228
+ this.structuralFormat(act, term);
1229
+ return;
1230
+ }
1231
+ case "utf8_decode": {
1232
+ const value = decodeUtf8Checked(this.boundary(act, term.args[0]));
1233
+ let result;
1234
+ if ("value" in value)
1235
+ result = this.variant(term.result_shape, "ResultOk", [
1236
+ stringValue(value.value),
1237
+ ]);
1238
+ else {
1239
+ const shapes = term.variant_result_shapes;
1240
+ const error = this.pkg.aggregates[shapes.error];
1241
+ result = this.variant(term.result_shape, "ResultErr", [
1242
+ {
1243
+ kind: "record",
1244
+ typeId: error.name,
1245
+ shape: error.id,
1246
+ fields: [
1247
+ { kind: "i64", value: BigInt(value.validUpTo) },
1248
+ this.variant(shapes.error_len, value.errorLen === null ? "OptionNone" : "OptionSome", value.errorLen === null
1249
+ ? []
1250
+ : [{ kind: "i64", value: BigInt(value.errorLen) }]),
1251
+ ],
1252
+ },
1253
+ ]);
1254
+ }
1255
+ this.completeShim(act, term, result);
1256
+ return;
1257
+ }
1258
+ default: {
1259
+ const exhaustive = registration;
1260
+ throw new Error(`unknown runtime continuation ${String(exhaustive)}`);
1186
1261
  }
1187
- this.completeShim(act, term, result);
1188
- return;
1189
- }
1190
- const shim = entry ? resolveRuntimeShim(entry) : undefined;
1191
- if (!entry || !shim) {
1192
- throw new Error(`runtime family ${term.family} resolved no shim after admission`);
1193
1262
  }
1194
- this.callShim(act, term, shim);
1195
- return;
1196
1263
  }
1197
1264
  case "extern.call": {
1198
1265
  const entry = this.pkg.externs[term.extern];
1199
- const shim = entry ? resolveExternShim(entry.symbol) : undefined;
1200
- if (!entry || !shim) {
1201
- throw new Error(`extern ${term.extern} resolved no shim after admission`);
1202
- }
1203
- this.callShim(act, term, shim);
1266
+ if (!entry)
1267
+ throw new Error("external operation is absent after admission");
1268
+ const registration = resolveExtern(entry);
1269
+ if (registration.kind === "refused")
1270
+ throw new Error(`external operation ${registration.capability} was refused after admission`);
1271
+ if (registration.kind === "directory")
1272
+ this.callDirectory(act, term, registration.operation);
1273
+ else
1274
+ this.callShim(act, term, registration.shim);
1204
1275
  return;
1205
1276
  }
1206
1277
  case "wire.codec": {
1207
1278
  const args = term.args.map((operand) => this.boundary(act, operand));
1208
- this.runValueProgram(act, term, args, executeCodec(this.pkg, term, args[0], this.options.codecBridge, this.options.propertyInput ?? []));
1279
+ this.runValueProgram(act, term, args, executeCodec(this.pkg, term, args[0], this.options[CODEC_REGISTRATION.bridge], this.options.propertyInput ?? []));
1209
1280
  return;
1210
1281
  }
1211
1282
  case "value.call": {
@@ -1316,6 +1387,29 @@ class ExecutorV1 {
1316
1387
  act.block = target;
1317
1388
  act.opIndex = 0;
1318
1389
  }
1390
+ callDirectory(act, term, operation) {
1391
+ const args = term.args.map((operand) => this.boundary(act, operand));
1392
+ const result = this.directories.call(operation, args, term.result_resource);
1393
+ if (!(result instanceof Promise)) {
1394
+ this.completeShim(act, term, result);
1395
+ return;
1396
+ }
1397
+ // Acquisitions are synchronous in the language. Retain borrowed inputs and
1398
+ // publish the new owner before the next authored cancellation safepoint.
1399
+ this.running = false;
1400
+ act.context.parked = true;
1401
+ act.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
1402
+ act.context.pendingCancellation ??= fault;
1403
+ };
1404
+ this.scheduler.submitHost(act.context.id, result, (outcome) => {
1405
+ act.context.parked = false;
1406
+ act.context.fileError = outcome.error;
1407
+ this.completeShim(act, term, outcome.value);
1408
+ this.runFrame(act);
1409
+ }, (error) => {
1410
+ throw error;
1411
+ });
1412
+ }
1319
1413
  /// Run a runtime or extern shim. A fault it raises takes the call's `unwind`
1320
1414
  /// edge so the cleanup the package names runs before the program ends.
1321
1415
  callShim(act, term, shim) {
@@ -1329,7 +1423,9 @@ class ExecutorV1 {
1329
1423
  : (this.pkg.variants[shapeId] ?? null);
1330
1424
  let value;
1331
1425
  try {
1332
- value = shim(this.host, args, shape);
1426
+ const aggregateId = term.aggregate_result_shape;
1427
+ const aggregate = aggregateId == null ? null : (this.pkg.aggregates[aggregateId] ?? null);
1428
+ value = shim(this.host, args, shape, aggregate);
1333
1429
  }
1334
1430
  catch (error) {
1335
1431
  if (error instanceof ShimFault) {
@@ -1441,11 +1537,19 @@ class ExecutorV1 {
1441
1537
  this.completeShim(act, term, this.variant(term.result_shape, value ? "OptionSome" : "OptionNone", value ? [value] : []));
1442
1538
  return;
1443
1539
  }
1444
- if (!value) {
1445
- this.raiseFault(act, { kind: "trap", trap: "vector_bounds" }, term.unwind);
1446
- return;
1540
+ switch (operation) {
1541
+ case "Member":
1542
+ if (!value) {
1543
+ this.raiseFault(act, { kind: "trap", trap: "vector_bounds" }, term.unwind);
1544
+ return;
1545
+ }
1546
+ this.completeShim(act, term, value);
1547
+ return;
1548
+ default: {
1549
+ const exhaustive = operation;
1550
+ throw new Error(`unknown supervisor pool operation ${String(exhaustive)}`);
1551
+ }
1447
1552
  }
1448
- this.completeShim(act, term, value);
1449
1553
  }
1450
1554
  structuralFormat(act, term) {
1451
1555
  if (term.structural == null)
@@ -1655,7 +1759,11 @@ class ExecutorV1 {
1655
1759
  this.current = caller;
1656
1760
  this.takeEdge(caller, act.unwind);
1657
1761
  }
1658
- suspend(act, term) {
1762
+ suspend(act, input) {
1763
+ const registration = resolveSuspension(input);
1764
+ if (registration.kind === "refused")
1765
+ throw new Error(`suspension ${registration.capability} was refused after admission`);
1766
+ const term = registration.term;
1659
1767
  switch (term.kind) {
1660
1768
  case "Yield": {
1661
1769
  const generator = act.context.generator;
@@ -1868,6 +1976,35 @@ class ExecutorV1 {
1868
1976
  return;
1869
1977
  }
1870
1978
  case "NativeIo": {
1979
+ if (term.detail.operation === "FileRead" ||
1980
+ term.detail.operation === "FileWrite") {
1981
+ const args = term.inputs.map((operand) => this.boundary(act, operand));
1982
+ const wake = this.park(act, term);
1983
+ const cancel = act.context.cancel;
1984
+ let stopped = act.context.pendingCancellation ?? this.scopeDeadline(act) ?? null;
1985
+ act.context.cancel = (fault = {
1986
+ kind: "panic",
1987
+ message: "task cancelled",
1988
+ cancelled: true,
1989
+ }) => {
1990
+ stopped ??= fault;
1991
+ };
1992
+ this.scheduler.submitHost(act.context.id, this.directories.io(term.detail.operation, args, () => stopped !== null || this.terminating), (outcome) => {
1993
+ // The producer is quiescent. Restore park's latch so cancellation
1994
+ // between readiness and frame resumption still retires the result.
1995
+ act.context.cancel = cancel;
1996
+ act.context.fileError = outcome.error;
1997
+ if (stopped) {
1998
+ this.closeValue(outcome.value, null);
1999
+ cancel(stopped);
2000
+ }
2001
+ else
2002
+ wake(outcome.value);
2003
+ }, (error) => {
2004
+ throw error;
2005
+ });
2006
+ return;
2007
+ }
1871
2008
  if (term.detail?.operation === "SinkFinish") {
1872
2009
  const wake = this.park(act, term);
1873
2010
  const cancel = act.context.cancel;
@@ -1887,7 +2024,11 @@ class ExecutorV1 {
1887
2024
  });
1888
2025
  return;
1889
2026
  }
1890
- // Admission passed `StdinReadLine`. The record is the raw line,
2027
+ if (term.detail.operation !== "StdinReadLine") {
2028
+ const exhaustive = term.detail.operation;
2029
+ throw new Error(`unknown native I/O handler ${String(exhaustive)}`);
2030
+ }
2031
+ // The record is the raw line,
1891
2032
  // so the records concatenate to the stdin the program consumed.
1892
2033
  const line = this.stdin.readLine() ?? new Uint8Array();
1893
2034
  this.trace.recordReplayInput({ kind: "stdin", data: new TextDecoder().decode(line) }, false);
@@ -1938,8 +2079,10 @@ class ExecutorV1 {
1938
2079
  cancelTimer = this.scheduler.after(nanos, () => wake(UNIT));
1939
2080
  return;
1940
2081
  }
1941
- default:
1942
- throw new Error(`suspension kind ${term.kind} resolved no handler after admission`);
2082
+ default: {
2083
+ const exhaustive = term;
2084
+ throw new Error(`unknown suspension handler ${String(exhaustive)}`);
2085
+ }
1943
2086
  }
1944
2087
  }
1945
2088
  pendingActorWait(act, targets, externalProgress, operation, fail) {
@@ -2323,12 +2466,18 @@ class ExecutorV1 {
2323
2466
  value.cell.weak -= 1;
2324
2467
  }
2325
2468
  break;
2469
+ case "host_resource": {
2470
+ if (value.owner.closed)
2471
+ break;
2472
+ this.closeHostResource(value);
2473
+ break;
2474
+ }
2326
2475
  case "timer": {
2327
2476
  if (value.closed)
2328
2477
  break;
2329
2478
  const resource = this.pkg.resources?.[value.resource];
2330
- if (resource?.close === undefined)
2331
- throw new Error("timer lost its selected close");
2479
+ if (resource?.kind !== "opaque")
2480
+ throw new Error("opaque owner lost its selected close");
2332
2481
  this.invokeFrame(actor, resource.close, [value], () => next(), next, rcOwner);
2333
2482
  return;
2334
2483
  }
@@ -2363,7 +2512,7 @@ class ExecutorV1 {
2363
2512
  break;
2364
2513
  case "record": {
2365
2514
  const resource = (this.pkg.resources ?? []).find((resource) => resource.kind === "record" && resource.shape === value.shape);
2366
- if (resource?.close !== undefined) {
2515
+ if (resource?.kind === "record") {
2367
2516
  if (this.consumedResources.has(value))
2368
2517
  break;
2369
2518
  if (!this.closingResources.has(value)) {
@@ -2417,7 +2566,20 @@ class ExecutorV1 {
2417
2566
  };
2418
2567
  next();
2419
2568
  }
2569
+ closeHostResource(value) {
2570
+ if (value.owner.closed)
2571
+ return;
2572
+ const resource = this.pkg.resources?.[value.resource];
2573
+ const release = resource?.kind === "nominal"
2574
+ ? resolveExtern({ symbol: resource.release })
2575
+ : null;
2576
+ if (release?.kind !== "directory" || release.operation.action !== "close")
2577
+ throw new Error("filesystem owner lost its selected release");
2578
+ this.directories.call(release.operation, [value]);
2579
+ }
2420
2580
  closeValue(value, fault) {
2581
+ if (value.kind === "host_resource")
2582
+ this.closeHostResource(value);
2421
2583
  if (value.kind === "string")
2422
2584
  value.release();
2423
2585
  if (value.kind === "closure")
@@ -2587,7 +2749,19 @@ class ExecutorV1 {
2587
2749
  this.trace.allocateId("actor", actor.id, act.context.id);
2588
2750
  this.trace.snapshot("actor.spawn", { actor_id: actor.id });
2589
2751
  const handle = { kind: "actor", id: actor.id };
2752
+ act.context.cancel = (fault = {
2753
+ kind: "panic",
2754
+ message: "actor construction cancelled",
2755
+ cancelled: true,
2756
+ }) => {
2757
+ actor.closing = true;
2758
+ if (actor.active) {
2759
+ actor.active.pendingCancellation ??= fault;
2760
+ actor.active.cancel?.(actor.active.pendingCancellation);
2761
+ }
2762
+ };
2590
2763
  const failed = (fault) => {
2764
+ act.context.cancel = undefined;
2591
2765
  actor.active = null;
2592
2766
  actor.alive = false;
2593
2767
  actor.constructing = false;
@@ -2618,6 +2792,7 @@ class ExecutorV1 {
2618
2792
  }
2619
2793
  actor.constructing = false;
2620
2794
  actor.active = null;
2795
+ act.context.cancel = undefined;
2621
2796
  act.rcOwner = null;
2622
2797
  actor.busy = false;
2623
2798
  this.startPeriodicTimers(actor);
@@ -2629,12 +2804,12 @@ class ExecutorV1 {
2629
2804
  // Construction enters the same interpreter loop as its caller. Only
2630
2805
  // a real suspension queues work and advances deterministic scheduling.
2631
2806
  const start = () => {
2632
- if (layout.start !== undefined)
2807
+ if (layout.start != null)
2633
2808
  actor.active = this.invokeFrame(actor, layout.start, [actor.state], started, failed, actor.rcOwner, true);
2634
2809
  else
2635
2810
  started();
2636
2811
  };
2637
- if (layout.init !== undefined)
2812
+ if (layout.init != null)
2638
2813
  actor.active = this.invokeFrame(actor, layout.init, [actor.state, ...args.slice(supplied)], start, failed, actor.rcOwner, true);
2639
2814
  else
2640
2815
  start();
@@ -2771,7 +2946,7 @@ class ExecutorV1 {
2771
2946
  this.raiseFault(act, { kind: "panic", message: "stream producer is closed" }, term.unwind);
2772
2947
  }
2773
2948
  else {
2774
- if (actor.layout.mailbox_capacity !== undefined &&
2949
+ if (actor.layout.mailbox_capacity != null &&
2775
2950
  actor.mailbox.length >= actor.layout.mailbox_capacity) {
2776
2951
  actor.admission.push(admit);
2777
2952
  return;
@@ -2946,7 +3121,8 @@ class ExecutorV1 {
2946
3121
  specs: layout.children.flatMap((child) => Array.from({ length: members }, () => child)),
2947
3122
  offsets: layout.children.map((_child, index) => index),
2948
3123
  recovering: new Set(),
2949
- restartBusy: false,
3124
+ restartBusy: true,
3125
+ constructor: null,
2950
3126
  pendingRestarts: [],
2951
3127
  spent: new Set(),
2952
3128
  completed: false,
@@ -2961,20 +3137,39 @@ class ExecutorV1 {
2961
3137
  this.trace.allocateId("supervisor", owner.id, act.context.id);
2962
3138
  this.trace.snapshot("supervisor.spawn", { supervisor_id: owner.id });
2963
3139
  this.running = false;
2964
- let child = 0;
3140
+ const order = this.supervisorStartupOrder(owner);
3141
+ let fault = null;
3142
+ const fail = (cause) => {
3143
+ if (fault)
3144
+ return;
3145
+ fault = cause;
3146
+ owner.closed.push(() => {
3147
+ act.context.cancel = undefined;
3148
+ this.raiseFault(act, fault, term.unwind);
3149
+ this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
3150
+ });
3151
+ if (owner.constructor)
3152
+ owner.constructor.pendingCancellation ??= cause;
3153
+ this.stopSupervisor(owner);
3154
+ };
3155
+ act.context.cancel = (cause = {
3156
+ kind: "panic",
3157
+ message: "construction cancelled",
3158
+ cancelled: true,
3159
+ }) => fail(cause);
2965
3160
  const next = () => {
2966
- if (child < owner.children.length) {
2967
- this.startChild(owner, child++, next, (fault) => {
2968
- owner.closed.push(() => {
2969
- this.raiseFault(act, fault, term.unwind);
2970
- this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2971
- });
2972
- this.stopSupervisor(owner);
2973
- });
3161
+ const child = order.shift();
3162
+ if (child !== undefined) {
3163
+ this.startChild(owner, child, next, fail);
2974
3164
  }
2975
3165
  else {
3166
+ act.context.cancel = undefined;
3167
+ owner.restartBusy = false;
2976
3168
  this.completeShim(act, term, { kind: "supervisor", id: owner.id });
2977
3169
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
3170
+ const pending = owner.pendingRestarts.shift();
3171
+ if (pending)
3172
+ this.restartChild(owner, pending.child, pending.debts);
2978
3173
  }
2979
3174
  };
2980
3175
  next();
@@ -3107,8 +3302,12 @@ class ExecutorV1 {
3107
3302
  id: act.context.actor.id,
3108
3303
  });
3109
3304
  return;
3110
- default:
3111
- throw new Error(`actor operation ${operation.op} has no executor`);
3305
+ case "local_observation":
3306
+ throw new Error("actor observations are not admitted in the VM");
3307
+ default: {
3308
+ const exhaustive = operation;
3309
+ throw new Error("unhandled actor operation: " + String(exhaustive));
3310
+ }
3112
3311
  }
3113
3312
  }
3114
3313
  scopeDeadline(act, expected) {
@@ -3216,7 +3415,7 @@ class ExecutorV1 {
3216
3415
  selectedTarget?.(actor);
3217
3416
  if (settled || closing)
3218
3417
  return;
3219
- const full = actor.layout.mailbox_capacity !== undefined &&
3418
+ const full = actor.layout.mailbox_capacity != null &&
3220
3419
  actor.mailbox.length >= actor.layout.mailbox_capacity;
3221
3420
  if (full) {
3222
3421
  if (protocol.policy === "reject") {
@@ -3308,7 +3507,7 @@ class ExecutorV1 {
3308
3507
  admitActorMessage(actor, message) {
3309
3508
  if (!actor.alive || actor.closing)
3310
3509
  return "closed";
3311
- const full = actor.layout.mailbox_capacity !== undefined &&
3510
+ const full = actor.layout.mailbox_capacity != null &&
3312
3511
  actor.mailbox.length >= actor.layout.mailbox_capacity;
3313
3512
  let declared = full ? actor.layout.overflow : null;
3314
3513
  const discard = (lost) => {
@@ -3608,7 +3807,7 @@ class ExecutorV1 {
3608
3807
  }, actor);
3609
3808
  };
3610
3809
  const runHook = () => {
3611
- if (actor.layout.crash !== undefined) {
3810
+ if (actor.layout.crash != null) {
3612
3811
  const [code, message] = faultInfo(fault);
3613
3812
  const info = {
3614
3813
  kind: "record",
@@ -3689,9 +3888,42 @@ class ExecutorV1 {
3689
3888
  throw new Error("supervisor does not exist");
3690
3889
  return owner;
3691
3890
  }
3891
+ supervisorStartupOrder(owner) {
3892
+ const declarations = owner.layout.startup_order ??
3893
+ owner.layout.children.map((_child, index) => index);
3894
+ return declarations.flatMap((declaration) => {
3895
+ const offset = owner.offsets[declaration];
3896
+ const end = owner.offsets[declaration + 1] ?? owner.children.length;
3897
+ return Array.from({ length: end - offset }, (_value, index) => offset + index);
3898
+ });
3899
+ }
3692
3900
  startChild(owner, child, done, failed) {
3693
3901
  const spec = owner.specs[child];
3694
- this.invokeFrame(null, spec.spawn, owner.config, (value) => {
3902
+ const arity = this.functionAt(spec.spawn).params.length;
3903
+ // Packages predating sibling wiring have config-only constructors. Their
3904
+ // actual callable signature, rather than a source name, selects that ABI.
3905
+ const args = arity === owner.config.length + 1
3906
+ ? [{ kind: "supervisor", id: owner.id }, ...owner.config]
3907
+ : arity === owner.config.length
3908
+ ? owner.config
3909
+ : null;
3910
+ if (!args)
3911
+ throw new Error("supervisor constructor signature does not match its config");
3912
+ owner.constructor = this.invokeFrame(null, spec.spawn, args, (value) => {
3913
+ const cancellation = owner.constructor?.pendingCancellation;
3914
+ owner.constructor = null;
3915
+ if (!owner.alive) {
3916
+ // The result is still owned here. Its declaration slot hands it to
3917
+ // canonical reverse-order teardown, including the configured budget.
3918
+ owner.children[child] = value;
3919
+ failed(cancellation ?? {
3920
+ kind: "panic",
3921
+ message: "supervisor construction cancelled",
3922
+ cancelled: true,
3923
+ });
3924
+ this.finishSupervisorStop(owner);
3925
+ return;
3926
+ }
3695
3927
  owner.children[child] = value;
3696
3928
  if (value.kind === "actor")
3697
3929
  this.actorFor(value).supervisor = { owner, child };
@@ -3704,7 +3936,15 @@ class ExecutorV1 {
3704
3936
  child,
3705
3937
  });
3706
3938
  done();
3707
- }, failed);
3939
+ }, (fault) => {
3940
+ const stopped = !owner.alive;
3941
+ owner.constructor = null;
3942
+ // The initial caller still needs one terminal notification if the
3943
+ // supervisor was stopped directly rather than through caller cancel.
3944
+ failed(fault);
3945
+ if (stopped)
3946
+ this.finishSupervisorStop(owner);
3947
+ });
3708
3948
  }
3709
3949
  roleOwner(role) {
3710
3950
  const parentRole = this.roles.get(role.ownerId);
@@ -3799,7 +4039,7 @@ class ExecutorV1 {
3799
4039
  stop();
3800
4040
  });
3801
4041
  };
3802
- const starting = [...affected];
4042
+ const starting = this.supervisorStartupOrder(owner).filter((index) => affected.includes(index));
3803
4043
  const start = () => {
3804
4044
  if (!owner.alive) {
3805
4045
  owner.restartBusy = false;
@@ -3880,7 +4120,7 @@ class ExecutorV1 {
3880
4120
  }
3881
4121
  childStopBudget(owner, child) {
3882
4122
  const spec = owner.specs[child];
3883
- if (spec.stop_config !== undefined) {
4123
+ if (spec.stop_config != null) {
3884
4124
  const duration = owner.config[spec.stop_config];
3885
4125
  if (duration?.kind !== "i64" || duration.value < 0n)
3886
4126
  throw new Error("child stop budget is not a non-negative duration");
@@ -3898,6 +4138,18 @@ class ExecutorV1 {
3898
4138
  if (!owner.alive)
3899
4139
  return;
3900
4140
  owner.alive = false;
4141
+ if (owner.constructor) {
4142
+ owner.constructor.pendingCancellation ??= {
4143
+ kind: "panic",
4144
+ message: "supervisor construction cancelled",
4145
+ cancelled: true,
4146
+ };
4147
+ owner.constructor.cancel?.(owner.constructor.pendingCancellation);
4148
+ return;
4149
+ }
4150
+ this.finishSupervisorStop(owner);
4151
+ }
4152
+ finishSupervisorStop(owner) {
3901
4153
  let childIndex = owner.children.length - 1;
3902
4154
  const next = () => {
3903
4155
  owner.teardownCancelTimer?.();
@@ -4221,6 +4473,8 @@ function actorErrorRole(reason) {
4221
4473
  return "ActorErrorRoutingFailed";
4222
4474
  case "EncodeFailed":
4223
4475
  return "ActorErrorEncodeFailed";
4476
+ case "MessageTooLarge":
4477
+ return "ActorErrorMessageTooLarge";
4224
4478
  case "ConnectionDropped":
4225
4479
  return "ActorErrorConnectionDropped";
4226
4480
  case "Partition":
@@ -4231,6 +4485,8 @@ function actorErrorRole(reason) {
4231
4485
  }
4232
4486
  function sendErrorRole(reason) {
4233
4487
  switch (reason) {
4488
+ case "MessageTooLarge":
4489
+ return "SendErrorMessageTooLarge";
4234
4490
  case "Full":
4235
4491
  return "SendErrorFull";
4236
4492
  case "Closed":