@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
@@ -0,0 +1,288 @@
1
+ import { UNIT } from "../values.js";
2
+ export const DIRECTORY_OPERATIONS = {
3
+ hew_dir_open_read: {
4
+ release: "hew_dir_read_close",
5
+ action: "root",
6
+ access: "read",
7
+ },
8
+ hew_dir_open_write: {
9
+ release: "hew_dir_write_close",
10
+ action: "root",
11
+ access: "write",
12
+ },
13
+ hew_dir_read_subdir: {
14
+ release: "hew_dir_read_close",
15
+ action: "subdir",
16
+ access: "read",
17
+ },
18
+ hew_dir_write_subdir: {
19
+ release: "hew_dir_write_close",
20
+ action: "subdir",
21
+ access: "write",
22
+ },
23
+ hew_dir_read_open_file: {
24
+ release: "hew_file_read_close",
25
+ action: "file",
26
+ access: "read",
27
+ },
28
+ hew_dir_write_open_file: {
29
+ release: "hew_file_write_close",
30
+ action: "file",
31
+ access: "write",
32
+ },
33
+ hew_dir_read_is_valid: { action: "valid", access: "read" },
34
+ hew_dir_write_is_valid: { action: "valid", access: "write" },
35
+ hew_file_read_is_valid: { action: "valid", access: "read" },
36
+ hew_file_write_is_valid: { action: "valid", access: "write" },
37
+ hew_dir_read_close: { action: "close", access: "read" },
38
+ hew_dir_write_close: { action: "close", access: "write" },
39
+ hew_file_read_close: { action: "close", access: "read" },
40
+ hew_file_write_close: { action: "close", access: "write" },
41
+ };
42
+ function errorOf(error) {
43
+ const name = error instanceof DOMException ? error.name : "";
44
+ return {
45
+ kind: name === "NotFoundError"
46
+ ? 1
47
+ : name === "NotAllowedError" || name === "SecurityError"
48
+ ? 2
49
+ : 0,
50
+ message: error instanceof Error ? error.message : String(error),
51
+ };
52
+ }
53
+ function pathOf(value) {
54
+ if (value?.kind !== "string")
55
+ throw new Error("directory operation requires a string path");
56
+ const path = value.value;
57
+ if (!path ||
58
+ path.startsWith("/") ||
59
+ path.includes("\\") ||
60
+ path.includes("\0") ||
61
+ /^[A-Za-z]:/.test(path))
62
+ throw new DOMException("path must stay beneath the supplied directory", "NotAllowedError");
63
+ return path;
64
+ }
65
+ function ownerOf(value, access) {
66
+ if (value?.kind !== "host_resource" ||
67
+ value.owner.closed ||
68
+ !value.owner.handle ||
69
+ value.owner.access !== access)
70
+ throw new Error("closed or incompatible filesystem owner");
71
+ return value.owner;
72
+ }
73
+ /** Resolve actual entries in order. A child starts a new confinement boundary;
74
+ * parent components never recover the parent's authority. FSA exposes no symlink
75
+ * traversal primitive: each lookup stays beneath its concrete directory handle. */
76
+ async function directory(root, parts) {
77
+ const stack = [root];
78
+ for (const part of parts) {
79
+ if (!part || part === ".")
80
+ continue;
81
+ if (part === "..") {
82
+ if (stack.length === 1)
83
+ throw new DOMException("path escapes its directory owner", "NotAllowedError");
84
+ stack.pop();
85
+ }
86
+ else
87
+ stack.push(await stack[stack.length - 1].getDirectoryHandle(part));
88
+ }
89
+ return stack[stack.length - 1];
90
+ }
91
+ export class BrowserDirectories {
92
+ roots;
93
+ // The adapter retains its physical owner slots until selected release. This
94
+ // also covers acquisition completed after a terminal VM failure, when its
95
+ // source continuation must never run.
96
+ owners = new Set();
97
+ retire(release) {
98
+ for (const value of this.owners)
99
+ release(value);
100
+ }
101
+ /** OPFS has no symlink entries. Picker-backed filesystem link confinement is
102
+ * not standardized, so this adapter refuses those handles before execution. */
103
+ static async admission(roots) {
104
+ const supplied = [roots?.read, roots?.write].filter((root) => root !== undefined);
105
+ if (supplied.length === 0)
106
+ return null;
107
+ if (typeof navigator === "undefined" || !navigator.storage?.getDirectory)
108
+ return "This host does not provide the browser origin-private filesystem.";
109
+ try {
110
+ const canonical = await navigator.storage.getDirectory();
111
+ for (const root of supplied)
112
+ if (root.kind !== "directory" ||
113
+ (await canonical.resolve(root)) === null)
114
+ return "Only supplied origin-private directory handles are supported; external picker directory confinement is unavailable.";
115
+ return null;
116
+ }
117
+ catch (error) {
118
+ return `Cannot validate supplied directory authority: ${errorOf(error).message}`;
119
+ }
120
+ }
121
+ constructor(roots = {}) {
122
+ this.roots = roots;
123
+ }
124
+ call(operation, args, resource) {
125
+ if (operation.action === "valid") {
126
+ const value = args[0];
127
+ return {
128
+ kind: "bool",
129
+ value: value?.kind === "host_resource" &&
130
+ !value.owner.closed &&
131
+ value.owner.handle !== null &&
132
+ value.owner.access === operation.access,
133
+ };
134
+ }
135
+ if (operation.action === "close") {
136
+ const value = args[0];
137
+ if (value?.kind !== "host_resource")
138
+ throw new Error("close requires a filesystem owner");
139
+ value.owner.closed = true;
140
+ value.owner.handle = null;
141
+ this.owners.delete(value);
142
+ return UNIT;
143
+ }
144
+ if (resource == null)
145
+ throw new Error("filesystem acquisition has no compiler-selected owner slot");
146
+ return this.acquire(operation, args, resource);
147
+ }
148
+ async acquire(operation, args, resource) {
149
+ let owner = operation.action === "file"
150
+ ? {
151
+ access: operation.access,
152
+ handle: null,
153
+ cursor: 0,
154
+ closed: false,
155
+ tail: Promise.resolve(),
156
+ }
157
+ : { access: operation.access, handle: null, closed: false };
158
+ try {
159
+ const path = pathOf(args[operation.action === "root" ? 0 : 1]);
160
+ const parent = operation.action === "root"
161
+ ? this.roots[operation.access]
162
+ : ownerOf(args[0], operation.access).handle;
163
+ if (!parent || parent.kind !== "directory")
164
+ throw new DOMException("no supplied directory authority", "NotAllowedError");
165
+ const parts = path.split("/");
166
+ if (operation.action === "file") {
167
+ const name = parts.pop();
168
+ if (!name || name === "." || name === "..")
169
+ throw new Error("file path must name a regular file");
170
+ const target = await directory(parent, parts);
171
+ const file = await target.getFileHandle(name, {
172
+ create: operation.access === "write",
173
+ });
174
+ if (operation.access === "write") {
175
+ // Native open_write truncates before publishing the owner. FSA only
176
+ // publishes writes on close, so close this truncation before resuming.
177
+ const writer = await file.createWritable();
178
+ try {
179
+ await writer.close();
180
+ }
181
+ catch (error) {
182
+ await writer.abort().catch(() => { });
183
+ throw error;
184
+ }
185
+ }
186
+ owner = {
187
+ access: operation.access,
188
+ handle: file,
189
+ cursor: 0,
190
+ closed: false,
191
+ tail: Promise.resolve(),
192
+ };
193
+ }
194
+ else
195
+ owner.handle = await directory(parent, parts);
196
+ return this.owned(resource, owner, null);
197
+ }
198
+ catch (error) {
199
+ return this.owned(resource, owner, errorOf(error));
200
+ }
201
+ }
202
+ owned(resource, owner, error) {
203
+ const value = {
204
+ kind: "host_resource",
205
+ resource,
206
+ owner,
207
+ };
208
+ this.owners.add(value);
209
+ return { value, error };
210
+ }
211
+ io(operation, args, cancelled) {
212
+ const fallback = operation === "FileRead"
213
+ ? { kind: "vector", elementType: "u8", items: [] }
214
+ : { kind: "i64", value: -1n };
215
+ let owner;
216
+ try {
217
+ const selected = ownerOf(args[0], operation === "FileRead" ? "read" : "write");
218
+ if (!("cursor" in selected))
219
+ throw new Error("file operation requires a file owner");
220
+ owner = selected;
221
+ }
222
+ catch (error) {
223
+ return Promise.resolve({ value: fallback, error: errorOf(error) });
224
+ }
225
+ const result = owner.tail.then(async () => {
226
+ try {
227
+ if (cancelled())
228
+ return { value: fallback, error: null };
229
+ const handle = owner.handle;
230
+ if (!handle || owner.closed)
231
+ throw new Error("closed file owner");
232
+ if (operation === "FileRead") {
233
+ const file = await handle.getFile();
234
+ if (cancelled())
235
+ return { value: fallback, error: null };
236
+ const bytes = new Uint8Array(await file.slice(owner.cursor).arrayBuffer());
237
+ owner.cursor += bytes.length;
238
+ return {
239
+ value: {
240
+ kind: "vector",
241
+ elementType: "u8",
242
+ items: Array.from(bytes, (value) => ({
243
+ kind: "i64",
244
+ value: BigInt(value),
245
+ })),
246
+ },
247
+ error: null,
248
+ };
249
+ }
250
+ const data = args[1];
251
+ if (data?.kind !== "vector")
252
+ throw new Error("file write requires bytes");
253
+ const bytes = Uint8Array.from(data.items, (value) => {
254
+ if (value.kind !== "i64")
255
+ throw new Error("file write requires byte elements");
256
+ return Number(value.value);
257
+ });
258
+ const writer = await handle.createWritable({ keepExistingData: true });
259
+ try {
260
+ if (cancelled()) {
261
+ await writer.abort();
262
+ return { value: fallback, error: null };
263
+ }
264
+ await writer.seek(owner.cursor);
265
+ await writer.write(bytes);
266
+ if (cancelled()) {
267
+ await writer.abort();
268
+ return { value: fallback, error: null };
269
+ }
270
+ await writer.close();
271
+ }
272
+ catch (error) {
273
+ await writer.abort().catch(() => { });
274
+ throw error;
275
+ }
276
+ owner.cursor += bytes.length;
277
+ return { value: { kind: "i64", value: 0n }, error: null };
278
+ }
279
+ catch (error) {
280
+ return { value: fallback, error: errorOf(error) };
281
+ }
282
+ });
283
+ // Independent files progress independently; each cursor serializes its own
284
+ // borrowed operations as the native file owner's lock does.
285
+ owner.tail = result.then(() => { });
286
+ return result;
287
+ }
288
+ }
@@ -0,0 +1,4 @@
1
+ import { type VmValue } from "../values.js";
2
+ import type { AggregateShape } from "./package.js";
3
+ export declare function calendarResultFields(symbol: string): readonly string[] | undefined;
4
+ export declare function executeCalendar(bridge: ((request: string) => string) | undefined, symbol: string, args: VmValue[], shape: AggregateShape | null): VmValue;
@@ -0,0 +1,64 @@
1
+ import { stringValue } from "../values.js";
2
+ const TIME_FIELDS = ["status", "seconds", "nanoseconds", "message"];
3
+ const CALENDAR_FIELDS = {
4
+ hew_calendar_format: ["status", "text", "message"],
5
+ hew_calendar_parse: TIME_FIELDS,
6
+ hew_calendar_from_parts: TIME_FIELDS,
7
+ hew_calendar_components: [
8
+ "status",
9
+ "year",
10
+ "month",
11
+ "day",
12
+ "hour",
13
+ "minute",
14
+ "second",
15
+ "nanosecond",
16
+ "weekday",
17
+ "days_in_month",
18
+ "message",
19
+ ],
20
+ };
21
+ export function calendarResultFields(symbol) {
22
+ return CALENDAR_FIELDS[symbol];
23
+ }
24
+ export function executeCalendar(bridge, symbol, args, shape) {
25
+ if (!bridge)
26
+ throw new Error("calendar operations require the hew-wasm executeCalendar bridge");
27
+ const fields = calendarResultFields(symbol);
28
+ if (!shape ||
29
+ !fields ||
30
+ shape.fields.length !== fields.length ||
31
+ shape.fields.some((field, index) => field !== fields[index]))
32
+ throw new Error("calendar operation lacks its checked result shape");
33
+ const request = {
34
+ operation: symbol.slice("hew_calendar_".length),
35
+ args: args.map((value) => {
36
+ if (value.kind === "i64")
37
+ return value.value.toString();
38
+ if (value.kind === "string")
39
+ return value.value;
40
+ throw new Error("invalid calendar argument");
41
+ }),
42
+ };
43
+ const result = JSON.parse(bridge(JSON.stringify(request)));
44
+ if (typeof result.status !== "number" ||
45
+ ![0, 1, 2, 3].includes(result.status) ||
46
+ typeof result.message !== "string")
47
+ throw new Error("calendar bridge returned an invalid result");
48
+ return {
49
+ kind: "record",
50
+ typeId: shape.name,
51
+ shape: shape.id,
52
+ fields: shape.fields.map((field) => {
53
+ const value = result[field];
54
+ if (result.status === 0 && value === undefined)
55
+ throw new Error("calendar bridge omitted a successful result field");
56
+ if (field === "message" || field === "text")
57
+ return stringValue(value === undefined ? "" : String(value));
58
+ return {
59
+ kind: "i64",
60
+ value: BigInt(value === undefined ? 0 : String(value)),
61
+ };
62
+ }),
63
+ };
64
+ }
@@ -148,7 +148,7 @@ function* fromBridge(pkg, shape, id, value, context) {
148
148
  }
149
149
  if ("Map" in node || "Set" in node) {
150
150
  const family = "Map" in node ? "Map" : "Set";
151
- const map = yield* collectionOperation(pkg, family, "New", [], [], null);
151
+ const map = yield* collectionOperation(pkg, { handler: "collection", family, operation: "New" }, [], [], null);
152
152
  for (const item of array(value)) {
153
153
  const itemFields = array(item);
154
154
  const pair = "Map" in node ? itemFields : [itemFields[0], null];
@@ -159,7 +159,7 @@ function* fromBridge(pkg, shape, id, value, context) {
159
159
  if (map.kind !== "map")
160
160
  throw new Error("codec collection has no map storage");
161
161
  const size = map.entries.size;
162
- yield* collectionOperation(pkg, family, "Insert", [map, key, member], context.callbacks[id] ?? [], null);
162
+ yield* collectionOperation(pkg, { handler: "collection", family, operation: "Insert" }, [map, key, member], context.callbacks[id] ?? [], null);
163
163
  if (!context.propertyInput && map.entries.size === size)
164
164
  throw new Duplicate(itemFields["Map" in node ? 2 : 1]);
165
165
  }
@@ -1,3 +1,3 @@
1
1
  import type { RunOptions, SandboxTrace } from "../types.js";
2
- import type { PackageV1 } from "./package.js";
3
- export declare function runPackageV1(pkg: PackageV1, options?: RunOptions): SandboxTrace;
2
+ import type { CheckedPackage } from "../schema-validator.js";
3
+ export declare function runPackageV1(pkg: CheckedPackage, options?: RunOptions): Promise<SandboxTrace>;