@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.
@@ -1,4 +1,4 @@
1
- import { UNIT, cloneValue, compareUtf8, } from "../values.js";
1
+ import { UNIT, mutableVector, vectorItems, vectorLength, vectorElement, cloneValue, compareUtf8, } from "../values.js";
2
2
  import { runtimeTag, } from "./package.js";
3
3
  import { ShimFault } from "./shims.js";
4
4
  import { actorIdentical } from "./actor-identity.js";
@@ -109,13 +109,13 @@ export function* selectedValue(pkg, id, args) {
109
109
  if (plan.ty === "bytes") {
110
110
  if (left.kind !== "vector")
111
111
  throw new Error("bytes capability has no bytes");
112
- const bytes = left.items.map((value) => Number(bits(value, "u8")));
112
+ const bytes = vectorItems(left).map((value) => Number(bits(value, "u8")));
113
113
  if (hash)
114
114
  return integer(bytesHash(bytes));
115
115
  if (equality)
116
116
  return boolean(right?.kind === "vector" &&
117
- bytes.length === right.items.length &&
118
- bytes.every((value, i) => BigInt(value) === bits(right.items[i], "u8")));
117
+ bytes.length === vectorLength(right) &&
118
+ bytes.every((value, i) => BigInt(value) === bits(vectorElement(right, i), "u8")));
119
119
  throw new Error("bytes ordering is outside the checked capability domain");
120
120
  }
121
121
  if (left.kind === "actor" || left.kind === "supervisor") {
@@ -151,7 +151,7 @@ export function* selectedValue(pkg, id, args) {
151
151
  selections = plan.variants?.[left.tag] ?? [];
152
152
  }
153
153
  else if (left.kind === "vector") {
154
- fields = left.items;
154
+ fields = vectorItems(left);
155
155
  if (hash) {
156
156
  const elementSelection = components[0];
157
157
  if (!elementSelection)
@@ -160,9 +160,10 @@ export function* selectedValue(pkg, id, args) {
160
160
  vectorHashLength = fields.length;
161
161
  }
162
162
  else {
163
- if (right?.kind !== "vector" || left.items.length !== right.items.length)
163
+ if (right?.kind !== "vector" ||
164
+ vectorLength(left) !== vectorLength(right))
164
165
  return boolean(false);
165
- others = right.items;
166
+ others = vectorItems(right);
166
167
  selections = fields.map(() => components[0]);
167
168
  }
168
169
  }
@@ -324,17 +325,20 @@ export function* sharedOperation(operation, args, owner, typeName, option) {
324
325
  /** Open-addressed collection operations preserve native callback ordering:
325
326
  * growth hashes occupied slots before probing the incoming key. Staged slots
326
327
  * borrow their owners until every fallible callback has completed. */
327
- export function* collectionOperation(pkg, request, args, callbacks, shape) {
328
+ export function* collectionOperation(pkg, request, args, callbacks, shape, elementType) {
328
329
  const { family, operation } = request;
329
330
  if (family === "Vector") {
330
331
  const receiver = args[0];
331
332
  if (receiver?.kind !== "vector")
332
333
  throw new Error("vector operation has no vector");
333
334
  if (operation === "Clear") {
335
+ mutableVector(receiver);
334
336
  const items = receiver.items.splice(0);
335
337
  yield {
336
338
  kind: "release",
337
339
  value: { kind: "vector", elementType: receiver.elementType, items },
340
+ retained: items,
341
+ retainedType: elementType,
338
342
  };
339
343
  return receiver;
340
344
  }
@@ -343,14 +347,15 @@ export function* collectionOperation(pkg, request, args, callbacks, shape) {
343
347
  if (index?.kind !== "i64")
344
348
  throw new Error("vector mutation has no index");
345
349
  const at = Number(index.value);
346
- if (at < 0 || at >= receiver.items.length)
347
- throw new ShimFault("vector_bounds", "vector index out of bounds");
350
+ if (at < 0 || at >= vectorLength(receiver))
351
+ throw new ShimFault("vector_bounds", `vector index ${at} out of bounds for length ${vectorLength(receiver)}`);
352
+ mutableVector(receiver);
348
353
  const old = receiver.items[at];
349
354
  receiver.items[at] = args[2];
350
- yield { kind: "release", value: old };
355
+ yield { kind: "release", value: old, retainedType: elementType };
351
356
  return receiver;
352
357
  }
353
- for (const item of receiver.items) {
358
+ for (const item of vectorItems(receiver)) {
354
359
  const equal = yield* selectedValue(pkg, callbacks[0], [item, args[1]]);
355
360
  if (equal.kind !== "bool")
356
361
  throw new Error("Eq returned a non-boolean value");
@@ -377,6 +382,7 @@ export function* collectionOperation(pkg, request, args, callbacks, shape) {
377
382
  receiver.tombstones = new Set();
378
383
  yield {
379
384
  kind: "release",
385
+ retained: entries.flatMap((entry) => [entry.key, entry.value]),
380
386
  value: {
381
387
  kind: "vector",
382
388
  elementType: "",
@@ -389,15 +395,22 @@ export function* collectionOperation(pkg, request, args, callbacks, shape) {
389
395
  operation === "Values" ||
390
396
  operation === "Elements" ||
391
397
  operation === "Entries") {
392
- return {
398
+ const result = {
393
399
  kind: "vector",
394
400
  elementType: "",
395
- items: [...receiver.entries.values()].map(({ key, value }) => operation === "Values"
396
- ? cloneValue(value)
397
- : operation === "Entries"
398
- ? tuple(cloneValue(key), cloneValue(value))
399
- : cloneValue(key)),
401
+ items: [],
400
402
  };
403
+ const items = [...receiver.entries.values()].map(({ key, value }) => operation === "Values"
404
+ ? cloneValue(value)
405
+ : operation === "Entries"
406
+ ? tuple(cloneValue(key), cloneValue(value))
407
+ : cloneValue(key));
408
+ // Native snapshots push into an initially empty Vec, retaining its normal
409
+ // growth capacity. A later semantic clone may use exact live length.
410
+ mutableVector(result, items.length);
411
+ for (const item of items)
412
+ result.items.push(item);
413
+ return result;
401
414
  }
402
415
  let staged = receiver;
403
416
  if (operation === "Insert" &&
@@ -435,6 +448,7 @@ export function* collectionOperation(pkg, request, args, callbacks, shape) {
435
448
  return cloneValue(entry.value);
436
449
  }
437
450
  if (operation === "Insert") {
451
+ receiver.prepareInsert?.(staged, result.slot, entry?.key ?? args[1], family === "Set" ? UNIT : args[2], family !== "Set" && !entry);
438
452
  receiver.entries = staged.entries;
439
453
  receiver.capacity = staged.capacity;
440
454
  receiver.tombstones = staged.tombstones;
@@ -1,6 +1,5 @@
1
1
  import type { JsonValue } from "./types.js";
2
2
  import { IndexedString } from "./strings.js";
3
- import type { EncodingNode, EncodingFormat } from "./v1/encoding-values.js";
4
3
  export { stringValue } from "./strings.js";
5
4
  /** One creator-owned membership list; payload destruction uses the ordinary VM release path. */
6
5
  export interface RcOwner {
@@ -8,6 +7,8 @@ export interface RcOwner {
8
7
  closed: boolean;
9
8
  }
10
9
  export interface RcCell {
10
+ managedLease?: import("./v1/managed-budget.js").ManagedLease;
11
+ managedPayload?: number;
11
12
  value: VmValue;
12
13
  refs: number;
13
14
  weak: number;
@@ -15,11 +16,27 @@ export interface RcCell {
15
16
  owner: RcOwner | null;
16
17
  typeName: string;
17
18
  }
18
- export type VmValue = import("./v1/semaphores.js").SemaphoreHandle | {
19
- kind: "encoding";
20
- format: EncodingFormat;
21
- value: EncodingNode | null;
22
- } | {
19
+ /** The language payload allocation. JS arrays holding view metadata do not
20
+ * determine its capacity or identity. Bytes views retain this complete owner. */
21
+ export interface VectorStorage {
22
+ items: VmValue[];
23
+ capacity: number;
24
+ owners: number;
25
+ bytes: boolean;
26
+ shareOnCopy: boolean;
27
+ }
28
+ export interface VmVector {
29
+ kind: "vector";
30
+ elementType: string;
31
+ items: VmValue[];
32
+ storage?: VectorStorage;
33
+ offset?: number;
34
+ /** Normal Vec live length; absent for materialized vectors and byte views. */
35
+ length?: number;
36
+ closed?: boolean;
37
+ prepareMutation?: (capacity: number, detached: boolean, appended?: readonly VmValue[]) => void;
38
+ }
39
+ export type VmValue = import("./v1/semaphores.js").SemaphoreHandle | import("./v1/encoding-storage.js").EncodingOwner | {
23
40
  kind: "rc";
24
41
  cell: RcCell | null;
25
42
  closed: boolean;
@@ -86,17 +103,14 @@ export type VmValue = import("./v1/semaphores.js").SemaphoreHandle | {
86
103
  typeId: string;
87
104
  shape?: number;
88
105
  fields: VmValue[];
106
+ managedCaptures?: readonly number[];
89
107
  } | {
90
108
  kind: "enum";
91
109
  typeId: string;
92
110
  shape: number;
93
111
  tag: number;
94
112
  payload: VmValue[];
95
- } | {
96
- kind: "vector";
97
- elementType: string;
98
- items: VmValue[];
99
- }
113
+ } | VmVector
100
114
  /** A map or set owns its occupied slots. Selected Hash/Eq operations probe
101
115
  * the table; tombstones preserve collision chains across removal. */
102
116
  | {
@@ -107,6 +121,9 @@ export type VmValue = import("./v1/semaphores.js").SemaphoreHandle | {
107
121
  }>;
108
122
  capacity?: number;
109
123
  tombstones?: Set<number>;
124
+ prepareInsert?: (staged: Extract<VmValue, {
125
+ kind: "map";
126
+ }>, slot: string, key: VmValue, value: VmValue, copyKey: boolean) => void;
110
127
  }
111
128
  /** A trait object: the vtable the checker selected, and the value it wraps.
112
129
  * A `dyn.call` resolves its method through `vtable`, never through the
@@ -115,6 +132,7 @@ export type VmValue = import("./v1/semaphores.js").SemaphoreHandle | {
115
132
  kind: "dyn";
116
133
  vtable: number;
117
134
  value: VmValue;
135
+ managedPayload?: number;
118
136
  }
119
137
  /** A first-class function reference materialised by `const.function`.
120
138
  * `id` is the bytecode function id (e.g. `"fn:my_handler"`).
@@ -135,10 +153,26 @@ export type VmValue = import("./v1/semaphores.js").SemaphoreHandle | {
135
153
  }>;
136
154
  specialization: number | null;
137
155
  environment: VmValue;
156
+ managedCaptures?: readonly number[];
138
157
  };
139
158
  export declare const UNIT: VmValue;
140
159
  export declare function compareUtf8(a: string, b: string): number;
141
160
  export declare function cloneValue(value: VmValue): VmValue;
161
+ /** The live range is header metadata; the backing keeps its full capacity. */
162
+ export declare function vectorLength(value: VmVector): number;
163
+ export declare function vectorElement(value: VmVector, index: number): VmValue | undefined;
164
+ /** Materialize a view only for operations that already traverse its elements. */
165
+ export declare function vectorItems(value: VmVector): VmValue[];
166
+ export declare function vectorStorage(value: VmVector): VectorStorage;
167
+ export declare function byteVectorValue(items: VmValue[]): VmVector;
168
+ export declare function sliceByteVector(value: VmVector, start: number, end: number): VmVector;
169
+ /** Establish uniqueness before any mutation, preserving native capacity rules. */
170
+ export declare function mutableVector(value: VmVector, needed?: number, appended?: readonly VmValue[]): VmVector;
171
+ /** Move a normal Vec end in constant time, retaining its allocation identity. */
172
+ export declare function takeVectorEnd(value: VmVector, back: boolean): VmValue;
173
+ /** Consume one handle; only its remaining live elements need destruction. */
174
+ export declare function releaseVector(value: VmVector): VmValue[];
175
+ export declare function clearByteVector(value: VmVector): VmVector;
142
176
  export declare function valueFromLiteral(value: JsonValue): VmValue;
143
177
  export declare function renderF64(value: number): string;
144
178
  export declare function renderStdout(value: VmValue): string;
@@ -1,4 +1,5 @@
1
1
  import { stringValue } from "./strings.js";
2
+ import { cloneEncoding } from "./v1/encoding-storage.js";
2
3
  export { stringValue } from "./strings.js";
3
4
  export const UNIT = { kind: "unit" };
4
5
  const UTF8_ENCODER = new TextEncoder();
@@ -18,7 +19,7 @@ const I64_LITERAL_DECIMAL = /^(?:0|-?[1-9]\d*)$/;
18
19
  export function cloneValue(value) {
19
20
  switch (value.kind) {
20
21
  case "encoding":
21
- return { ...value, value: structuredClone(value.value) };
22
+ return cloneEncoding(value);
22
23
  case "rc":
23
24
  if (value.closed)
24
25
  throw new Error("cannot copy a released Rc");
@@ -69,6 +70,7 @@ export function cloneValue(value) {
69
70
  kind: "record",
70
71
  typeId: value.typeId,
71
72
  shape: value.shape,
73
+ managedCaptures: value.managedCaptures,
72
74
  fields: value.fields.map(cloneValue),
73
75
  };
74
76
  case "enum":
@@ -80,16 +82,13 @@ export function cloneValue(value) {
80
82
  payload: value.payload.map(cloneValue),
81
83
  };
82
84
  case "vector":
83
- return {
84
- kind: "vector",
85
- elementType: value.elementType,
86
- items: value.items.map(cloneValue),
87
- };
85
+ return cloneVector(value);
88
86
  case "dyn":
89
87
  return {
90
88
  kind: "dyn",
91
89
  vtable: value.vtable,
92
90
  value: cloneValue(value.value),
91
+ managedPayload: value.managedPayload,
93
92
  };
94
93
  case "closure":
95
94
  return {
@@ -97,6 +96,7 @@ export function cloneValue(value) {
97
96
  entries: value.entries,
98
97
  specialization: value.specialization,
99
98
  environment: cloneValue(value.environment),
99
+ managedCaptures: value.managedCaptures,
100
100
  };
101
101
  case "map":
102
102
  return {
@@ -110,6 +110,174 @@ export function cloneValue(value) {
110
110
  };
111
111
  }
112
112
  }
113
+ /** The live range is header metadata; the backing keeps its full capacity. */
114
+ export function vectorLength(value) {
115
+ return value.length ?? value.items.length;
116
+ }
117
+ export function vectorElement(value, index) {
118
+ if (index < 0 || index >= vectorLength(value))
119
+ return undefined;
120
+ return value.items[(value.length === undefined ? 0 : (value.offset ?? 0)) + index];
121
+ }
122
+ /** Materialize a view only for operations that already traverse its elements. */
123
+ export function vectorItems(value) {
124
+ return value.length === undefined
125
+ ? value.items
126
+ : value.items.slice(value.offset ?? 0, (value.offset ?? 0) + value.length);
127
+ }
128
+ export function vectorStorage(value) {
129
+ if (value.closed)
130
+ throw new Error("cannot read a released vector");
131
+ return (value.storage ??= {
132
+ items: value.items,
133
+ capacity: value.items.length,
134
+ owners: 1,
135
+ bytes: false,
136
+ shareOnCopy: false,
137
+ });
138
+ }
139
+ export function byteVectorValue(items) {
140
+ return {
141
+ kind: "vector",
142
+ elementType: "u8",
143
+ items,
144
+ storage: {
145
+ items,
146
+ capacity: items.length ? Math.max(items.length, 16) : 0,
147
+ owners: 1,
148
+ bytes: true,
149
+ shareOnCopy: true,
150
+ },
151
+ offset: 0,
152
+ };
153
+ }
154
+ function cloneVector(value) {
155
+ const storage = vectorStorage(value);
156
+ // Native Vec clones discard an empty receiver's spare capacity. Bytes
157
+ // clones retain their allocation even when the visible length is zero.
158
+ if (storage.bytes || (storage.shareOnCopy && vectorLength(value) !== 0)) {
159
+ storage.owners++;
160
+ return { ...value, storage, closed: false };
161
+ }
162
+ const items = vectorItems(value).map(cloneValue);
163
+ return {
164
+ kind: "vector",
165
+ elementType: value.elementType,
166
+ items,
167
+ storage: { ...storage, items, capacity: items.length, owners: 1 },
168
+ };
169
+ }
170
+ export function sliceByteVector(value, start, end) {
171
+ const storage = vectorStorage(value);
172
+ if (!storage.bytes)
173
+ throw new Error("bytes slice has no bytes storage");
174
+ if (start === end)
175
+ return byteVectorValue([]);
176
+ storage.owners++;
177
+ return {
178
+ kind: "vector",
179
+ elementType: value.elementType,
180
+ items: value.items.slice(start, end),
181
+ storage,
182
+ offset: (value.offset ?? 0) + start,
183
+ };
184
+ }
185
+ /** Establish uniqueness before any mutation, preserving native capacity rules. */
186
+ export function mutableVector(value, needed = vectorLength(value), appended) {
187
+ let storage = vectorStorage(value);
188
+ let capacity = storage.capacity;
189
+ const detached = storage.owners !== 1;
190
+ if (detached && storage.bytes)
191
+ capacity = Math.max(vectorLength(value), 16);
192
+ if (needed > capacity) {
193
+ if (storage.bytes)
194
+ capacity = Math.max(Math.min(capacity * 2, 0xffffffff), needed, 16);
195
+ else {
196
+ capacity = capacity || 4;
197
+ while (capacity < needed)
198
+ capacity *= 2;
199
+ }
200
+ }
201
+ if (!Number.isSafeInteger(capacity) ||
202
+ (storage.bytes && capacity > 0xffffffff))
203
+ throw new Error("vector capacity overflow");
204
+ if (detached || capacity !== storage.capacity || appended?.length)
205
+ value.prepareMutation?.(capacity, detached, appended);
206
+ if (detached) {
207
+ const items = vectorItems(value).map(cloneValue);
208
+ storage.owners--;
209
+ storage = { ...storage, items, capacity, owners: 1 };
210
+ value.storage = storage;
211
+ value.items = items;
212
+ value.offset = 0;
213
+ value.length = undefined;
214
+ }
215
+ else if (!storage.bytes && value.length !== undefined) {
216
+ // A sole remaining view can reclaim its consumed prefix without
217
+ // changing the language allocation's identity or retained capacity.
218
+ if ((value.offset ?? 0) === 0)
219
+ value.items.length = value.length;
220
+ else
221
+ value.items = vectorItems(value);
222
+ storage.items = value.items;
223
+ value.offset = 0;
224
+ value.length = undefined;
225
+ }
226
+ if (capacity !== storage.capacity) {
227
+ storage.capacity = capacity;
228
+ value.offset = 0;
229
+ }
230
+ else if (storage.bytes && (value.offset ?? 0) + needed > capacity) {
231
+ value.offset = 0;
232
+ }
233
+ return value;
234
+ }
235
+ /** Move a normal Vec end in constant time, retaining its allocation identity. */
236
+ export function takeVectorEnd(value, back) {
237
+ const storage = vectorStorage(value);
238
+ const length = vectorLength(value);
239
+ if (length === 0)
240
+ throw new Error("cannot take an empty vector end");
241
+ if (storage.bytes) {
242
+ mutableVector(value);
243
+ return (back ? value.items.pop() : value.items.shift());
244
+ }
245
+ const start = value.offset ?? 0;
246
+ const index = start + (back ? length - 1 : 0);
247
+ const item = value.items[index];
248
+ // Managed/affine storage is unique. Clear its moved slot immediately so
249
+ // the backing cannot retain payload references after ownership transfers.
250
+ // Shared callback-free plain bytes remain readable by other headers.
251
+ if (storage.owners === 1)
252
+ value.items[index] = UNIT;
253
+ else if (!storage.shareOnCopy)
254
+ throw new Error("managed vector unexpectedly shares its backing");
255
+ value.offset = start + (back ? 0 : 1);
256
+ value.length = length - 1;
257
+ return item;
258
+ }
259
+ /** Consume one handle; only its remaining live elements need destruction. */
260
+ export function releaseVector(value) {
261
+ if (value.closed)
262
+ return [];
263
+ const storage = vectorStorage(value);
264
+ const items = --storage.owners === 0
265
+ ? storage.bytes
266
+ ? storage.items
267
+ : vectorItems(value)
268
+ : [];
269
+ value.closed = true;
270
+ return items;
271
+ }
272
+ export function clearByteVector(value) {
273
+ const storage = vectorStorage(value);
274
+ storage.owners--;
275
+ const empty = byteVectorValue([]);
276
+ value.items = empty.items;
277
+ value.storage = empty.storage;
278
+ value.offset = 0;
279
+ return value;
280
+ }
113
281
  export function valueFromLiteral(value) {
114
282
  const taggedI64 = taggedI64Literal(value);
115
283
  if (taggedI64 !== null) {
@@ -258,7 +426,7 @@ export function renderStdout(value) {
258
426
  payload: value.payload.map((field) => toJsonValue(field)),
259
427
  });
260
428
  case "vector":
261
- return canonicalJson(value.items.map((item) => toJsonValue(item)));
429
+ return canonicalJson(vectorItems(value).map((item) => toJsonValue(item)));
262
430
  case "rc":
263
431
  return "Rc";
264
432
  case "weak":
@@ -316,7 +484,7 @@ export function toJsonValue(value) {
316
484
  payload: value.payload.map(toJsonValue),
317
485
  };
318
486
  case "vector":
319
- return value.items.map(toJsonValue);
487
+ return vectorItems(value).map(toJsonValue);
320
488
  case "rc":
321
489
  return "Rc";
322
490
  case "weak":
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@hew-lang/sandbox-vm",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
4
4
  "hewSource": {
5
- "commit": "6e4bcf6b089c260bedae59320c68dab2137f5754",
6
- "tag": "v0.9.3",
5
+ "commit": "75e14a2775f9af8fd77a6a907da277b2de666824",
6
+ "tag": "v0.10.0",
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.9.3"
24
+ "@hew-lang/wasm": "0.10.0"
25
25
  },
26
26
  "keywords": [
27
27
  "hew",