@hew-lang/sandbox-vm 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@
11
11
  import { SUPPORTED_NATIVE_IO, SUPPORTED_SUSPEND_KINDS, resolveExternShim, resolveRuntimeShim, } from "./shims.js";
12
12
  const UNSUPPORTED = "sandbox.capability.unsupported";
13
13
  const NATIVE_FAMILIES = new Set(["AsyncIo", "FileRead", "Tcp"]);
14
- const NATIVE_SUSPENSIONS = new Set(["NativeIo", "RemoteAsk", "Read", "Accept"]);
14
+ const NATIVE_SUSPENSIONS = new Set(["NativeIo", "Read", "Accept"]);
15
15
  const OPS = new Set([
16
16
  "coverage.hit",
17
17
  "const.int",
@@ -64,6 +64,7 @@ const OPS = new Set([
64
64
  "task_scope.enter",
65
65
  "task_scope.close",
66
66
  "task.spawn",
67
+ "task.invoke",
67
68
  "task.race",
68
69
  ]);
69
70
  const TERMS = new Set([
@@ -92,6 +93,7 @@ const TERMS = new Set([
92
93
  "unreachable",
93
94
  ]);
94
95
  const ACTOR_OPS = new Set([
96
+ "identity",
95
97
  "spawn",
96
98
  "self_handle",
97
99
  "stop",
@@ -102,6 +104,7 @@ const ACTOR_OPS = new Set([
102
104
  "call_take",
103
105
  "submit",
104
106
  "stream_start",
107
+ "timer_schedule",
105
108
  "supervisor_spawn",
106
109
  "supervisor_child",
107
110
  "supervisor_pool_view",
@@ -114,13 +117,11 @@ function capabilityMessage(capability, native) {
114
117
  NativeIo: "Host input and output",
115
118
  Read: "Host input and output",
116
119
  Accept: "Accepting network connections",
117
- RemoteAsk: "Remote actor calls",
118
120
  };
119
121
  const features = {
120
122
  Vector: "This vector operation",
121
123
  Map: "This map operation",
122
124
  Set: "This set operation",
123
- "actor.periodic": "Periodic actor handlers",
124
125
  "actor.local_observation": "Actor links and monitors",
125
126
  "actor.heap_limit": "Per-actor heap limits",
126
127
  "actor.ingress_adapter": "Actor ingress adapters",
@@ -150,6 +151,7 @@ export function admitPackage(pkg) {
150
151
  span: null,
151
152
  };
152
153
  }
154
+ const timerResources = new Set();
153
155
  for (const function_ of pkg.functions) {
154
156
  for (const block of function_.blocks) {
155
157
  if (block.ops.some((op) => op.op === "actor.ingress_adapter"))
@@ -182,6 +184,66 @@ export function admitPackage(pkg) {
182
184
  span: null,
183
185
  };
184
186
  }
187
+ const term = block.term;
188
+ if (term.op === "runtime.call" &&
189
+ pkg.runtime_families[term.family]?.family === "BytesDecodeUtf8") {
190
+ const shapes = term.variant_result_shapes;
191
+ const error = shapes && pkg.aggregates[shapes.error];
192
+ const length = shapes && pkg.variants[shapes.error_len];
193
+ const result = term.result_shape === null
194
+ ? undefined
195
+ : pkg.variants[term.result_shape];
196
+ if (!error ||
197
+ error.fields.length !== 2 ||
198
+ !length ||
199
+ !result ||
200
+ length.runtime_tags?.OptionSome === undefined ||
201
+ length.runtime_tags?.OptionNone === undefined ||
202
+ result.runtime_tags?.ResultOk === undefined ||
203
+ result.runtime_tags?.ResultErr === undefined)
204
+ return {
205
+ category: "invalid_package",
206
+ code: "sandbox.package.variant_result_shapes",
207
+ capability: null,
208
+ message: "UTF-8 decode lacks its checked result carrier",
209
+ span: null,
210
+ };
211
+ }
212
+ if (term.op === "actor.call" && term.operation.op === "timer_schedule") {
213
+ const operation = term.operation;
214
+ const handler = pkg.actors
215
+ ?.find((actor) => actor.id === operation.actor)
216
+ ?.handlers.find((handler) => handler.message_id === operation.message);
217
+ const resource = pkg.resources?.[operation.resource];
218
+ if (!handler ||
219
+ handler.streams ||
220
+ term.args.length !== 5 ||
221
+ !["reject", "drop_newest"].includes(operation.policy) ||
222
+ resource?.kind !== "opaque" ||
223
+ resource.close === undefined ||
224
+ !pkg.functions.some((fn) => fn.id === resource.close))
225
+ return {
226
+ category: "invalid_package",
227
+ code: "sandbox.package.invalid_timer",
228
+ capability: "actor.timer_schedule",
229
+ message: "timer schedule lacks its checked handler, timing or non-waiting admission",
230
+ span: null,
231
+ };
232
+ timerResources.add(operation.resource);
233
+ }
234
+ if (term.op === "suspend") {
235
+ const mandatoryClose = (pkg.resources ?? []).some((resource) => resource.close === function_.id);
236
+ if (term.kind === "Safepoint"
237
+ ? (term.cancel == null) !== mandatoryClose
238
+ : term.cancel == null)
239
+ return {
240
+ category: "invalid_package",
241
+ code: "sandbox.package.cancellation_ancestry",
242
+ capability: null,
243
+ message: "safepoint cancellation ancestry must match its authored close lifecycle",
244
+ span: null,
245
+ };
246
+ }
185
247
  const unknown = block.ops.find((op) => !OPS.has(op.op))?.op ??
186
248
  (!TERMS.has(block.term.op) ? block.term.op : null);
187
249
  if (unknown)
@@ -194,7 +256,7 @@ export function admitPackage(pkg) {
194
256
  };
195
257
  }
196
258
  }
197
- for (const resource of pkg.resources ?? []) {
259
+ for (const [index, resource] of (pkg.resources ?? []).entries()) {
198
260
  if (resource.kind === "record" &&
199
261
  (resource.shape === undefined ||
200
262
  !pkg.aggregates.some((shape) => shape.id === resource.shape)))
@@ -205,15 +267,49 @@ export function admitPackage(pkg) {
205
267
  message: "record resource has no exact aggregate shape",
206
268
  span: null,
207
269
  };
208
- if (resource.kind === "opaque")
270
+ if (resource.kind === "opaque" && !timerResources.has(index))
209
271
  return unavailable("resource.opaque_close");
210
272
  if (resource.kind === "nominal" &&
211
273
  !resolveExternShim(resource.release ?? ""))
212
274
  return unavailable("resource.external_close");
213
275
  }
214
276
  for (const actor of pkg.actors ?? []) {
215
- if (actor.handlers.some((handler) => handler.every_ns != null))
216
- return unavailable("actor.periodic");
277
+ if (typeof actor.name !== "string" || actor.name.length === 0)
278
+ return {
279
+ category: "invalid_package",
280
+ code: "sandbox.package.actor_name",
281
+ capability: null,
282
+ message: "actor lacks its declared diagnostic name",
283
+ span: null,
284
+ };
285
+ if (actor.crash != null &&
286
+ (actor.crash_info == null ||
287
+ !pkg.aggregates[actor.crash_info] ||
288
+ actor.crash_kind == null ||
289
+ !pkg.variants[actor.crash_kind] ||
290
+ actor.crash_action == null ||
291
+ !pkg.variants[actor.crash_action]))
292
+ return {
293
+ category: "invalid_package",
294
+ code: "sandbox.package.crash_hook_shapes",
295
+ capability: null,
296
+ message: "crash hook lacks its exact info, kind or action shape",
297
+ span: null,
298
+ };
299
+ if (actor.handlers.some((handler) => handler.every_ns != null &&
300
+ (!Number.isInteger(handler.every_ns) ||
301
+ handler.every_ns < 1_000_000 ||
302
+ handler.params !== 0 ||
303
+ handler.streams ||
304
+ handler.fallible ||
305
+ !pkg.functions.some((fn) => fn.id === handler.callable && fn.params.length === 1))))
306
+ return {
307
+ category: "invalid_package",
308
+ code: "sandbox.package.invalid_periodic",
309
+ capability: "actor.periodic",
310
+ message: "periodic handler lacks its checked interval or zero-argument body",
311
+ span: null,
312
+ };
217
313
  if (actor.exit != null || actor.down != null)
218
314
  return unavailable("actor.local_observation");
219
315
  if (actor.max_heap_bytes != null)
@@ -1,6 +1,9 @@
1
- import { type VmValue } from "../values.js";
1
+ import { type VmValue, type RcOwner } from "../values.js";
2
2
  import { type PackageV1 } from "./package.js";
3
3
  export type ValueRequest = {
4
+ kind: "fault";
5
+ message: string;
6
+ } | {
4
7
  kind: "call";
5
8
  callee: number;
6
9
  args: VmValue[];
@@ -13,7 +16,7 @@ export type ValueProgram = Generator<ValueRequest, VmValue, VmValue>;
13
16
  /** Execute the exact checked plan, including every derived component selection.
14
17
  * A user method yields to an ordinary VM frame and can suspend or fail. */
15
18
  export declare function selectedValue(pkg: PackageV1, id: number, args: VmValue[]): ValueProgram;
16
- export declare function sharedOperation(operation: string, args: VmValue[]): ValueProgram;
19
+ export declare function sharedOperation(operation: string, args: VmValue[], owner: RcOwner | null, typeName: string, option: (value: VmValue | null) => VmValue): ValueProgram;
17
20
  /** Open-addressed collection operations preserve native callback ordering:
18
21
  * growth hashes occupied slots before probing the incoming key. Staged slots
19
22
  * borrow their owners until every fallible callback has completed. */
@@ -1,6 +1,7 @@
1
- import { UNIT, cloneValue, compareUtf8 } from "../values.js";
1
+ import { UNIT, cloneValue, compareUtf8, } from "../values.js";
2
2
  import { runtimeTag, } from "./package.js";
3
3
  import { ShimFault } from "./shims.js";
4
+ import { actorIdentical } from "./actor-identity.js";
4
5
  const OFFSET = 0xcbf29ce484222325n;
5
6
  const PRIME = 0x100000001b3n;
6
7
  const integer = (value) => ({
@@ -117,10 +118,16 @@ export function* selectedValue(pkg, id, args) {
117
118
  bytes.every((value, i) => BigInt(value) === bits(right.items[i], "u8")));
118
119
  throw new Error("bytes ordering is outside the checked capability domain");
119
120
  }
121
+ if (left.kind === "actor" || left.kind === "supervisor") {
122
+ if (equality)
123
+ return boolean(actorIdentical(left, right));
124
+ throw new Error("actor capability is outside the checked equality domain");
125
+ }
120
126
  const components = plan.components ?? [];
121
127
  let fields;
122
128
  let others = [];
123
129
  let selections;
130
+ let vectorHashLength;
124
131
  if (left.kind === "record") {
125
132
  fields = left.fields;
126
133
  selections = components;
@@ -144,13 +151,20 @@ export function* selectedValue(pkg, id, args) {
144
151
  selections = plan.variants?.[left.tag] ?? [];
145
152
  }
146
153
  else if (left.kind === "vector") {
147
- if (hash)
148
- throw new Error("derived vector Hash is outside the checked capability domain");
149
- if (right?.kind !== "vector" || left.items.length !== right.items.length)
150
- return boolean(false);
151
154
  fields = left.items;
152
- others = right.items;
153
- selections = fields.map(() => components[0]);
155
+ if (hash) {
156
+ const elementSelection = components[0];
157
+ if (!elementSelection)
158
+ throw new Error("derived vector Hash lacks its checked element selection");
159
+ selections = fields.map(() => elementSelection);
160
+ vectorHashLength = fields.length;
161
+ }
162
+ else {
163
+ if (right?.kind !== "vector" || left.items.length !== right.items.length)
164
+ return boolean(false);
165
+ others = right.items;
166
+ selections = fields.map(() => components[0]);
167
+ }
154
168
  }
155
169
  else {
156
170
  if (hash)
@@ -159,7 +173,9 @@ export function* selectedValue(pkg, id, args) {
159
173
  return boolean(!!right && bits(left, plan.ty) === bits(right, plan.ty));
160
174
  return scalarOrdering(left, right, plan.ty, ordering);
161
175
  }
162
- let state = OFFSET;
176
+ let state = vectorHashLength === undefined
177
+ ? OFFSET
178
+ : mix(OFFSET, BigInt(vectorHashLength));
163
179
  for (const [at, field] of fields.entries()) {
164
180
  const selection = selections[at];
165
181
  if (!selection)
@@ -247,15 +263,44 @@ function option(pkg, shape, value) {
247
263
  payload: value === undefined ? [] : [value],
248
264
  };
249
265
  }
250
- export function* sharedOperation(operation, args) {
251
- if (operation === "RcNew")
252
- return { kind: "rc", cell: { value: args[0], refs: 1 }, closed: false };
266
+ export function* sharedOperation(operation, args, owner, typeName, option) {
267
+ if (operation === "RcNew") {
268
+ if (owner?.closed)
269
+ throw new Error("Rc allocation after actor cleanup quiesced");
270
+ const cell = {
271
+ value: args[0],
272
+ refs: 1,
273
+ weak: 0,
274
+ dying: false,
275
+ owner,
276
+ typeName,
277
+ };
278
+ owner?.blocks.add(cell);
279
+ return { kind: "rc", cell, closed: false };
280
+ }
253
281
  const receiver = args[0];
282
+ if (operation === "WeakCloneRc" || operation === "WeakUpgradeRc") {
283
+ if (receiver?.kind !== "weak" || receiver.closed)
284
+ throw new Error("shared operation has no live Weak");
285
+ if (operation === "WeakCloneRc")
286
+ return cloneValue(receiver);
287
+ if (receiver.cell.dying || receiver.cell.refs === 0)
288
+ return option(null);
289
+ receiver.cell.refs += 1;
290
+ return option({ kind: "rc", cell: receiver.cell, closed: false });
291
+ }
254
292
  if (receiver?.kind !== "rc" || receiver.closed)
255
293
  throw new Error("shared operation has no live Rc");
294
+ if (!receiver.cell || receiver.cell.dying) {
295
+ yield { kind: "fault", message: "Rc payload is being released" };
296
+ return UNIT;
297
+ }
256
298
  switch (operation) {
257
299
  case "RcClone":
258
300
  return cloneValue(receiver);
301
+ case "RcDowngrade":
302
+ receiver.cell.weak += 1;
303
+ return { kind: "weak", cell: receiver.cell, closed: false };
259
304
  case "RcGet":
260
305
  return cloneValue(receiver.cell.value);
261
306
  case "RcIsUnique":
@@ -263,7 +308,7 @@ export function* sharedOperation(operation, args) {
263
308
  case "RcStrongCount":
264
309
  return integer(BigInt(receiver.cell.refs));
265
310
  case "RcWeakCount":
266
- return integer(0n);
311
+ return integer(BigInt(receiver.cell.weak));
267
312
  case "RcSet": {
268
313
  const old = receiver.cell.value;
269
314
  receiver.cell.value = args[1];
@@ -1,10 +1,26 @@
1
1
  import type { JsonValue } from "./types.js";
2
+ import { IndexedString } from "./strings.js";
3
+ export { stringValue } from "./strings.js";
4
+ /** One creator-owned membership list; payload destruction uses the ordinary VM release path. */
5
+ export interface RcOwner {
6
+ blocks: Set<RcCell>;
7
+ closed: boolean;
8
+ }
9
+ export interface RcCell {
10
+ value: VmValue;
11
+ refs: number;
12
+ weak: number;
13
+ dying: boolean;
14
+ owner: RcOwner | null;
15
+ typeName: string;
16
+ }
2
17
  export type VmValue = {
3
18
  kind: "rc";
4
- cell: {
5
- value: VmValue;
6
- refs: number;
7
- };
19
+ cell: RcCell | null;
20
+ closed: boolean;
21
+ } | {
22
+ kind: "weak";
23
+ cell: RcCell;
8
24
  closed: boolean;
9
25
  } | {
10
26
  kind: "unit";
@@ -17,10 +33,7 @@ export type VmValue = {
17
33
  } | {
18
34
  kind: "f64";
19
35
  value: number;
20
- } | {
21
- kind: "string";
22
- value: string;
23
- } | {
36
+ } | IndexedString | {
24
37
  kind: "actor";
25
38
  id: string;
26
39
  } | {
@@ -38,6 +51,11 @@ export type VmValue = {
38
51
  } | {
39
52
  kind: "task";
40
53
  id: string;
54
+ } | {
55
+ kind: "timer";
56
+ resource: number;
57
+ cancel(): void;
58
+ closed: boolean;
41
59
  } | {
42
60
  kind: "generator";
43
61
  id: string;
@@ -1,3 +1,5 @@
1
+ import { stringValue } from "./strings.js";
2
+ export { stringValue } from "./strings.js";
1
3
  export const UNIT = { kind: "unit" };
2
4
  const UTF8_ENCODER = new TextEncoder();
3
5
  export function compareUtf8(a, b) {
@@ -18,14 +20,22 @@ export function cloneValue(value) {
18
20
  case "rc":
19
21
  if (value.closed)
20
22
  throw new Error("cannot copy a released Rc");
23
+ if (!value.cell || value.cell.dying)
24
+ return { kind: "rc", cell: null, closed: false };
21
25
  value.cell.refs += 1;
22
26
  return { kind: "rc", cell: value.cell, closed: false };
27
+ case "weak":
28
+ if (value.closed)
29
+ throw new Error("cannot copy a released Weak");
30
+ value.cell.weak += 1;
31
+ return { kind: "weak", cell: value.cell, closed: false };
32
+ case "timer":
33
+ throw new Error("a timer cancellation handle cannot be copied");
23
34
  case "unit":
24
35
  return UNIT;
25
36
  case "bool":
26
37
  case "i64":
27
38
  case "f64":
28
- case "string":
29
39
  case "actor":
30
40
  case "supervisor":
31
41
  case "monitor":
@@ -38,6 +48,8 @@ export function cloneValue(value) {
38
48
  case "duplex":
39
49
  case "function": // function values are immutable — identity clone
40
50
  return { ...value };
51
+ case "string":
52
+ return value.clone();
41
53
  case "regex":
42
54
  return {
43
55
  kind: "regex",
@@ -106,9 +118,9 @@ export function valueFromLiteral(value) {
106
118
  ? { kind: "i64", value: BigInt(value) }
107
119
  : { kind: "f64", value };
108
120
  case "string":
109
- return { kind: "string", value };
121
+ return stringValue(value);
110
122
  default:
111
- return { kind: "string", value: canonicalJson(value) };
123
+ return stringValue(canonicalJson(value));
112
124
  }
113
125
  }
114
126
  function taggedI64Literal(value) {
@@ -192,6 +204,8 @@ export function renderF64(value) {
192
204
  }
193
205
  export function renderStdout(value) {
194
206
  switch (value.kind) {
207
+ case "timer":
208
+ return "Timer";
195
209
  case "unit":
196
210
  return "()";
197
211
  case "bool":
@@ -232,7 +246,9 @@ export function renderStdout(value) {
232
246
  case "vector":
233
247
  return canonicalJson(value.items.map((item) => toJsonValue(item)));
234
248
  case "rc":
235
- return renderStdout(value.cell.value);
249
+ return "Rc";
250
+ case "weak":
251
+ return "Weak";
236
252
  case "map":
237
253
  return canonicalJson(mapEntriesJson(value));
238
254
  case "dyn":
@@ -245,6 +261,8 @@ export function renderStdout(value) {
245
261
  }
246
262
  export function toJsonValue(value) {
247
263
  switch (value.kind) {
264
+ case "timer":
265
+ return "Timer";
248
266
  case "unit":
249
267
  return null;
250
268
  case "bool":
@@ -280,7 +298,9 @@ export function toJsonValue(value) {
280
298
  case "vector":
281
299
  return value.items.map(toJsonValue);
282
300
  case "rc":
283
- return toJsonValue(value.cell.value);
301
+ return "Rc";
302
+ case "weak":
303
+ return "Weak";
284
304
  case "map":
285
305
  return mapEntriesJson(value);
286
306
  case "dyn":
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@hew-lang/sandbox-vm",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "hewSource": {
5
- "commit": "6a446f773391f45d2c0ce0a799240b8b9afef54e",
6
- "tag": "v0.6.1",
5
+ "commit": "dd0911184af12136eda7e89052e5b564d56ba643",
6
+ "tag": "v0.6.2",
7
7
  "dirty": false
8
8
  },
9
9
  "description": "Deterministic TypeScript interpreter for the Hew educational sandbox",
@@ -21,7 +21,7 @@
21
21
  "cli/"
22
22
  ],
23
23
  "dependencies": {
24
- "@hew-lang/wasm": "0.6.1"
24
+ "@hew-lang/wasm": "0.6.2"
25
25
  },
26
26
  "keywords": [
27
27
  "hew",