@hew-lang/sandbox-vm 0.9.3 → 0.10.0

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.
@@ -54,17 +54,22 @@ var SandboxExitCode;
54
54
  SandboxExitCode[SandboxExitCode["HeapExceeded"] = 200] = "HeapExceeded";
55
55
  SandboxExitCode[SandboxExitCode["IntegerOverflow"] = 201] = "IntegerOverflow";
56
56
  SandboxExitCode[SandboxExitCode["DivideByZero"] = 202] = "DivideByZero";
57
+ SandboxExitCode[SandboxExitCode["SignedMinDivNegOne"] = 203] = "SignedMinDivNegOne";
57
58
  SandboxExitCode[SandboxExitCode["ShiftOutOfRange"] = 204] = "ShiftOutOfRange";
58
59
  SandboxExitCode[SandboxExitCode["IndexOutOfBounds"] = 205] = "IndexOutOfBounds";
60
+ SandboxExitCode[SandboxExitCode["StackOverflow"] = 215] = "StackOverflow";
59
61
  SandboxExitCode[SandboxExitCode["ActorSendFailed"] = 206] = "ActorSendFailed";
60
62
  SandboxExitCode[SandboxExitCode["MachineDispatchUnreachable"] = 207] = "MachineDispatchUnreachable";
61
63
  SandboxExitCode[SandboxExitCode["ExhaustivenessFallthrough"] = 208] = "ExhaustivenessFallthrough";
62
64
  })(SandboxExitCode || (SandboxExitCode = {}));
63
65
  const TRAP_EXIT_CODES = {
66
+ heap_exceeded: SandboxExitCode.HeapExceeded,
64
67
  actor_refused: SandboxExitCode.InternalError,
65
68
  integer_overflow: SandboxExitCode.IntegerOverflow,
66
69
  shift_out_of_range: SandboxExitCode.ShiftOutOfRange,
67
70
  divide_by_zero: SandboxExitCode.DivideByZero,
71
+ signed_min_div_neg_one: SandboxExitCode.SignedMinDivNegOne,
72
+ stack_overflow: SandboxExitCode.StackOverflow,
68
73
  invalid_local: SandboxExitCode.InternalError,
69
74
  invalid_block: SandboxExitCode.InternalError,
70
75
  invalid_call: SandboxExitCode.InternalError,
@@ -1,5 +1,6 @@
1
1
  export declare class IndexedString {
2
2
  readonly kind = "string";
3
+ prepareGrowth?: (capacity: number, detached: boolean) => void;
3
4
  private storage;
4
5
  private constructor();
5
6
  static fromText(text: string): IndexedString;
@@ -7,6 +8,11 @@ export declare class IndexedString {
7
8
  get value(): string;
8
9
  get scalarLength(): number;
9
10
  get byteLength(): number;
11
+ /** The same UTF-8 backing retained by clones; indexing metadata is excluded. */
12
+ managedStorage(): {
13
+ identity: object;
14
+ capacity: number;
15
+ };
10
16
  encoded(): Uint8Array;
11
17
  clone(): IndexedString;
12
18
  release(): void;
@@ -34,6 +34,7 @@ function newStorage(bytes, capacity = bytes.length) {
34
34
  }
35
35
  export class IndexedString {
36
36
  kind = "string";
37
+ prepareGrowth;
37
38
  storage;
38
39
  constructor(storage) {
39
40
  this.storage = storage;
@@ -55,6 +56,11 @@ export class IndexedString {
55
56
  get byteLength() {
56
57
  return this.live().byteLength;
57
58
  }
59
+ /** The same UTF-8 backing retained by clones; indexing metadata is excluded. */
60
+ managedStorage() {
61
+ const storage = this.live();
62
+ return { identity: storage, capacity: storage.bytes.length };
63
+ }
58
64
  encoded() {
59
65
  const storage = this.live();
60
66
  return storage.bytes.subarray(0, storage.byteLength);
@@ -162,6 +168,8 @@ export class IndexedString {
162
168
  const capacity = newLength > storage.bytes.length
163
169
  ? Math.max(newLength, storage.bytes.length * 2, 16)
164
170
  : storage.bytes.length;
171
+ if (storage.owners !== 1 || capacity !== storage.bytes.length)
172
+ this.prepareGrowth?.(capacity, storage.owners !== 1);
165
173
  if (storage.owners !== 1) {
166
174
  storage.owners--;
167
175
  storage = newStorage(this.encoded(), capacity);
@@ -2,7 +2,7 @@ export type JsonValue = null | boolean | number | string | JsonValue[] | {
2
2
  [key: string]: JsonValue;
3
3
  };
4
4
  export type SandboxBytecodePackage = import("./v1/package.js").PackageV1;
5
- export type TrapKind = "actor_refused" | "integer_overflow" | "shift_out_of_range" | "divide_by_zero" | "invalid_local" | "invalid_block" | "invalid_call" | "invalid_enum_tag" | "invalid_record_field" | "vector_bounds" | "string_bounds" | "regex_compile" | "capability_missing" | "budget_exhausted" | "panic" | "unsupported_instruction" | "internal_error";
5
+ export type TrapKind = import("./v1/package.js").TrapName | "invalid_local" | "invalid_block" | "invalid_call" | "invalid_enum_tag" | "invalid_record_field" | "string_bounds" | "regex_compile" | "capability_missing" | "budget_exhausted" | "panic" | "unsupported_instruction" | "internal_error";
6
6
  export type RuntimeStatus = "ok" | "compile_error" | "sandbox_rejected" | "runtime_failure" | "budget_exhausted" | "panic" | "trap";
7
7
  export interface RuntimeFailure {
8
8
  kind: "panic" | "trap" | "budget_exhausted" | "unsupported" | "internal_error";
@@ -1,4 +1,4 @@
1
- import { UNIT } from "../values.js";
1
+ import { vectorItems, UNIT } from "../values.js";
2
2
  export const DIRECTORY_OPERATIONS = {
3
3
  hew_dir_open_read: {
4
4
  release: "hew_dir_read_close",
@@ -250,7 +250,7 @@ export class BrowserDirectories {
250
250
  const data = args[1];
251
251
  if (data?.kind !== "vector")
252
252
  throw new Error("file write requires bytes");
253
- const bytes = Uint8Array.from(data.items, (value) => {
253
+ const bytes = Uint8Array.from(vectorItems(data), (value) => {
254
254
  if (value.kind !== "i64")
255
255
  throw new Error("file write requires byte elements");
256
256
  return Number(value.value);
@@ -1,4 +1,4 @@
1
- import { UNIT, stringValue } from "../values.js";
1
+ import { vectorItems, UNIT, stringValue } from "../values.js";
2
2
  import { runtimeTag } from "./package.js";
3
3
  import { collectionOperation } from "./value-operations.js";
4
4
  class DecodeFailure extends Error {
@@ -19,7 +19,7 @@ function fields(value) {
19
19
  if (value.kind === "record")
20
20
  return value.fields;
21
21
  if (value.kind === "vector")
22
- return value.items;
22
+ return vectorItems(value);
23
23
  throw new Error(`codec aggregate received ${value.kind}`);
24
24
  }
25
25
  function toBridge(pkg, shape, id, value) {
@@ -0,0 +1,33 @@
1
+ import type { EncodingFormat, EncodingNode } from "./encoding-values.js";
2
+ export interface EncodingStorage {
3
+ owners: number;
4
+ bytes?: bigint;
5
+ copyBytes?: bigint;
6
+ capacities?: unknown;
7
+ sharedCopy?: boolean;
8
+ }
9
+ export interface EncodingOwner {
10
+ kind: "encoding";
11
+ format: EncodingFormat;
12
+ value: EncodingNode | null;
13
+ storage?: EncodingStorage;
14
+ bridge?: (request: string) => string;
15
+ prepareMutation?: (replacement: EncodingOwner, detached: boolean) => void;
16
+ }
17
+ interface ManagedTree {
18
+ bytes: string;
19
+ copy_bytes: string;
20
+ shared_copy: boolean;
21
+ capacities: unknown;
22
+ }
23
+ export declare function extractEncoding(value: EncodingOwner, child: EncodingNode | null, path: number[]): EncodingOwner;
24
+ export declare function encodingOwner(format: EncodingFormat, value: EncodingNode | null, bridge?: EncodingOwner["bridge"], managed?: ManagedTree): EncodingOwner;
25
+ /** The native provider measures actual typed capacities. The VM retains this
26
+ * whole-tree backing; no JavaScript allocation estimate enters its charge. */
27
+ export declare function encodingStorage(value: EncodingOwner): EncodingStorage;
28
+ export declare function cloneEncoding(value: EncodingOwner, preview?: boolean): EncodingOwner;
29
+ export declare function releaseEncoding(value: EncodingOwner): void;
30
+ /** Stage the real provider operation before committing either consumed input.
31
+ * A refused copy-on-write detach leaves both trees and aliases unchanged. */
32
+ export declare function mutateEncoding(value: EncodingOwner, operation: "array_push" | "object_set", child: EncodingOwner, key?: string): boolean;
33
+ export {};
@@ -0,0 +1,113 @@
1
+ import { HeapExceeded } from "./managed-budget.js";
2
+ export function extractEncoding(value, child, path) {
3
+ if (!value.bridge || child === null)
4
+ return encodingOwner(value.format, structuredClone(child), value.bridge);
5
+ let capacities = encodingStorage(value).capacities;
6
+ for (const index of path)
7
+ capacities = Array.isArray(capacities) ? capacities[index] : undefined;
8
+ const result = encodingOwner(value.format, child, value.bridge);
9
+ storage(result).capacities = capacities;
10
+ return native(result, "clone");
11
+ }
12
+ export function encodingOwner(format, value, bridge, managed) {
13
+ return {
14
+ kind: "encoding",
15
+ format,
16
+ value,
17
+ bridge,
18
+ storage: {
19
+ owners: 1,
20
+ ...(managed
21
+ ? {
22
+ bytes: BigInt(managed.bytes),
23
+ copyBytes: BigInt(managed.copy_bytes),
24
+ capacities: managed.capacities,
25
+ sharedCopy: managed.shared_copy,
26
+ }
27
+ : {}),
28
+ },
29
+ };
30
+ }
31
+ function storage(value) {
32
+ return (value.storage ??= { owners: 1 });
33
+ }
34
+ function native(value, operation, fields = {}) {
35
+ if (!value.bridge)
36
+ throw new Error("managed encoding storage requires the native codec bridge");
37
+ const response = JSON.parse(value.bridge(JSON.stringify({
38
+ encoding_value: {
39
+ format: value.format,
40
+ operation,
41
+ value: value.value,
42
+ capacities: storage(value).capacities,
43
+ ...fields,
44
+ },
45
+ })));
46
+ if (response.failure) {
47
+ if (response.failure.includes("capacity overflow"))
48
+ throw new HeapExceeded();
49
+ throw new Error(response.failure);
50
+ }
51
+ return encodingOwner(value.format, response.value ?? null, value.bridge, response.managed);
52
+ }
53
+ /** The native provider measures actual typed capacities. The VM retains this
54
+ * whole-tree backing; no JavaScript allocation estimate enters its charge. */
55
+ export function encodingStorage(value) {
56
+ const retained = storage(value);
57
+ if (value.value !== null && retained.bytes == null) {
58
+ const measured = native(value, "retain");
59
+ Object.assign(retained, measured.storage, { owners: retained.owners });
60
+ }
61
+ return retained;
62
+ }
63
+ export function cloneEncoding(value, preview = false) {
64
+ const retained = value.bridge && value.value !== null
65
+ ? encodingStorage(value)
66
+ : storage(value);
67
+ if (retained.sharedCopy ?? value.format === "Yaml") {
68
+ if (preview)
69
+ return value;
70
+ retained.owners++;
71
+ return { ...value, storage: retained, prepareMutation: undefined };
72
+ }
73
+ if (value.bridge && value.value !== null)
74
+ return native(value, "clone");
75
+ return encodingOwner(value.format, structuredClone(value.value), value.bridge);
76
+ }
77
+ export function releaseEncoding(value) {
78
+ if (value.value !== null)
79
+ storage(value).owners--;
80
+ value.value = null;
81
+ }
82
+ /** Stage the real provider operation before committing either consumed input.
83
+ * A refused copy-on-write detach leaves both trees and aliases unchanged. */
84
+ export function mutateEncoding(value, operation, child, key) {
85
+ if (!value.bridge) {
86
+ if (value.format === "Yaml" && storage(value).owners > 1) {
87
+ storage(value).owners--;
88
+ value.value = structuredClone(value.value);
89
+ value.storage = { owners: 1 };
90
+ }
91
+ if (child.format === "Yaml" && storage(child).owners > 1) {
92
+ storage(child).owners--;
93
+ child.value = structuredClone(child.value);
94
+ child.storage = { owners: 1 };
95
+ }
96
+ return false;
97
+ }
98
+ const retained = encodingStorage(value);
99
+ const childStorage = encodingStorage(child);
100
+ const replacement = native(value, operation, {
101
+ detach: retained.sharedCopy && retained.owners > 1,
102
+ child: child.value,
103
+ child_capacities: childStorage.capacities,
104
+ key,
105
+ child_detach: childStorage.sharedCopy && childStorage.owners > 1,
106
+ });
107
+ value.prepareMutation?.(replacement, !!retained.sharedCopy && retained.owners > 1);
108
+ retained.owners--;
109
+ value.value = replacement.value;
110
+ value.storage = replacement.storage;
111
+ releaseEncoding(child);
112
+ return true;
113
+ }
@@ -1,5 +1,5 @@
1
1
  import { cloneValue, compareUtf8, stringValue, UNIT, } from "../values.js";
2
- const owner = (format, value) => ({ kind: "encoding", format, value });
2
+ import { encodingOwner, extractEncoding, mutateEncoding, releaseEncoding, } from "./encoding-storage.js";
3
3
  const integer = (value) => ({
4
4
  kind: "i64",
5
5
  value: BigInt(value),
@@ -59,9 +59,8 @@ function equal(format, left, right) {
59
59
  }
60
60
  }
61
61
  export function executeEncoding(host, format, operation, args) {
62
- const owned = (node) => owner(format, node);
62
+ const owned = (node) => encodingOwner(format, node, host.codecBridge);
63
63
  const value = (arg) => checkedValue(arg, format);
64
- const copy = (node) => cloneValue(owned(node));
65
64
  if (operation === "Parse" || operation === "Stringify") {
66
65
  if (!host.codecBridge)
67
66
  throw new Error("Encoding text operations require the hew-wasm executeCodec bridge");
@@ -74,7 +73,7 @@ export function executeEncoding(host, format, operation, args) {
74
73
  throw new Error(result.failure);
75
74
  host.encodingError(format, result.error ?? "");
76
75
  return operation === "Parse"
77
- ? owned(result.value ?? null)
76
+ ? encodingOwner(format, result.value ?? null, host.codecBridge, result.managed)
78
77
  : stringValue(result.text ?? "");
79
78
  }
80
79
  switch (operation) {
@@ -105,14 +104,15 @@ export function executeEncoding(host, format, operation, args) {
105
104
  case "Eq":
106
105
  return integer(equal(format, value(args[0]).value, value(args[1]).value) ? 1 : 0);
107
106
  case "Clone":
108
- return copy(value(args[0]).value);
107
+ return cloneValue(value(args[0]));
109
108
  case "Free":
110
- value(args[0]).value = null;
109
+ releaseEncoding(value(args[0]));
111
110
  return UNIT;
112
111
  default:
113
112
  break;
114
113
  }
115
- const receiver = value(args[0]), node = receiver.value;
114
+ const receiver = value(args[0]);
115
+ let node = receiver.value;
116
116
  let scalar = node;
117
117
  while (scalar?.[0] === "tagged")
118
118
  scalar = scalar[2];
@@ -160,16 +160,20 @@ export function executeEncoding(host, format, operation, args) {
160
160
  return integer(node?.[0] === "array" ? node[1].length : -1);
161
161
  case "GetDatetime":
162
162
  return stringValue(node?.[0] === "datetime" ? node[1] : "");
163
- case "GetField":
164
- return copy(node?.[0] === "object"
165
- ? (node[1].find(([key]) => key === text(args[1]))?.[1] ?? null)
163
+ case "GetField": {
164
+ const index = node?.[0] === "object"
165
+ ? node[1].findIndex(([key]) => key === text(args[1]))
166
166
  : node?.[0] === "mapping"
167
- ? (node[1].find(([key]) => key[0] === "string" && key[1] === text(args[1]))?.[1] ?? null)
168
- : null);
169
- case "ArrayGet":
170
- return copy(node?.[0] === "array"
171
- ? (node[1][Number(BigInt.asIntN(32, number(args[1])))] ?? null)
172
- : null);
167
+ ? node[1].findIndex(([key]) => key[0] === "string" && key[1] === text(args[1]))
168
+ : -1;
169
+ return extractEncoding(receiver, index < 0 || (node?.[0] !== "object" && node?.[0] !== "mapping")
170
+ ? null
171
+ : node[1][index][1], node?.[0] === "mapping" ? [1, index, 1] : [index, 1]);
172
+ }
173
+ case "ArrayGet": {
174
+ const index = Number(BigInt.asIntN(32, number(args[1])));
175
+ return extractEncoding(receiver, node?.[0] === "array" ? (node[1][index] ?? null) : null, [1, index]);
176
+ }
173
177
  case "ObjectKeys":
174
178
  return owned(node?.[0] === "object"
175
179
  ? ["array", node[1].map(([key]) => ["string", key])]
@@ -178,6 +182,9 @@ export function executeEncoding(host, format, operation, args) {
178
182
  : null);
179
183
  case "ObjectSet": {
180
184
  const child = value(args[2]), key = text(args[1]);
185
+ if (mutateEncoding(receiver, "object_set", child, key))
186
+ return receiver;
187
+ node = receiver.value;
181
188
  if (node?.[0] === "object" && child.value) {
182
189
  const index = node[1].findIndex(([name]) => name === key);
183
190
  if (index < 0) {
@@ -194,14 +201,17 @@ export function executeEncoding(host, format, operation, args) {
194
201
  else
195
202
  node[1][index] = [["string", key], child.value];
196
203
  }
197
- child.value = null;
204
+ releaseEncoding(child);
198
205
  return receiver;
199
206
  }
200
207
  case "ArrayPush": {
201
208
  const child = value(args[1]);
209
+ if (mutateEncoding(receiver, "array_push", child))
210
+ return receiver;
211
+ node = receiver.value;
202
212
  if (node?.[0] === "array" && child.value)
203
213
  node[1].push(child.value);
204
- child.value = null;
214
+ releaseEncoding(child);
205
215
  return receiver;
206
216
  }
207
217
  }