@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.
- package/dist/interpreter/run-program.js +5 -0
- package/dist/interpreter/strings.d.ts +6 -0
- package/dist/interpreter/strings.js +8 -0
- package/dist/interpreter/types.d.ts +1 -1
- package/dist/interpreter/v1/browser-directory.js +2 -2
- package/dist/interpreter/v1/codecs.js +2 -2
- package/dist/interpreter/v1/encoding-storage.d.ts +33 -0
- package/dist/interpreter/v1/encoding-storage.js +113 -0
- package/dist/interpreter/v1/encoding-values.js +28 -18
- package/dist/interpreter/v1/exec.js +1506 -130
- package/dist/interpreter/v1/instructions.generated.d.ts +100 -8
- package/dist/interpreter/v1/instructions.generated.js +1 -0
- package/dist/interpreter/v1/managed-budget.d.ts +88 -0
- package/dist/interpreter/v1/managed-budget.js +561 -0
- package/dist/interpreter/v1/package-graph.js +77 -2
- package/dist/interpreter/v1/package-validator.generated.js +22796 -17531
- package/dist/interpreter/v1/package.d.ts +4 -2
- package/dist/interpreter/v1/package.js +6 -0
- package/dist/interpreter/v1/pipes.d.ts +24 -1
- package/dist/interpreter/v1/pipes.js +91 -8
- package/dist/interpreter/v1/shims.d.ts +1 -0
- package/dist/interpreter/v1/shims.js +88 -62
- package/dist/interpreter/v1/structural.js +4 -2
- package/dist/interpreter/v1/validate.js +4 -2
- package/dist/interpreter/v1/value-operations.d.ts +3 -1
- package/dist/interpreter/v1/value-operations.js +32 -18
- package/dist/interpreter/values.d.ts +45 -11
- package/dist/interpreter/values.js +176 -8
- package/package.json +4 -4
|
@@ -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 = "
|
|
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
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
?
|
|
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
|
|
107
|
+
return cloneValue(value(args[0]));
|
|
109
108
|
case "Free":
|
|
110
|
-
value(args[0])
|
|
109
|
+
releaseEncoding(value(args[0]));
|
|
111
110
|
return UNIT;
|
|
112
111
|
default:
|
|
113
112
|
break;
|
|
114
113
|
}
|
|
115
|
-
const receiver = value(args[0])
|
|
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
|
-
|
|
165
|
-
?
|
|
163
|
+
case "GetField": {
|
|
164
|
+
const index = node?.[0] === "object"
|
|
165
|
+
? node[1].findIndex(([key]) => key === text(args[1]))
|
|
166
166
|
: node?.[0] === "mapping"
|
|
167
|
-
?
|
|
168
|
-
:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
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
|
|
214
|
+
releaseEncoding(child);
|
|
205
215
|
return receiver;
|
|
206
216
|
}
|
|
207
217
|
}
|