@hew-lang/sandbox-vm 0.6.1 → 0.6.3
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/README.md +48 -4
- package/cli/test-runner.mjs +4 -1
- package/dist/interpreter/interpreter.d.ts +1 -1
- package/dist/interpreter/interpreter.js +1 -1
- package/dist/interpreter/parity-runner.js +1 -1
- package/dist/interpreter/run-program.d.ts +3 -2
- package/dist/interpreter/run-program.js +5 -3
- package/dist/interpreter/schema-validator.d.ts +9 -4
- package/dist/interpreter/schema-validator.js +15 -49
- package/dist/interpreter/strings.d.ts +21 -0
- package/dist/interpreter/strings.js +191 -0
- package/dist/interpreter/types.d.ts +6 -0
- package/dist/interpreter/unicode-data.d.ts +4 -0
- package/dist/interpreter/unicode-data.js +1673 -0
- package/dist/interpreter/unicode.d.ts +3 -0
- package/dist/interpreter/unicode.js +35 -0
- package/dist/interpreter/v1/actor-identity.d.ts +3 -0
- package/dist/interpreter/v1/actor-identity.js +7 -0
- package/dist/interpreter/v1/actor-waits.d.ts +6 -0
- package/dist/interpreter/v1/actor-waits.js +33 -0
- package/dist/interpreter/v1/browser-directory.d.ts +107 -0
- package/dist/interpreter/v1/browser-directory.js +288 -0
- package/dist/interpreter/v1/calendar.d.ts +4 -0
- package/dist/interpreter/v1/calendar.js +64 -0
- package/dist/interpreter/v1/codecs.js +8 -11
- package/dist/interpreter/v1/exec.d.ts +2 -2
- package/dist/interpreter/v1/exec.js +1761 -489
- package/dist/interpreter/v1/instructions.generated.d.ts +1135 -0
- package/dist/interpreter/v1/instructions.generated.js +106 -0
- package/dist/interpreter/v1/package-graph.d.ts +2 -0
- package/dist/interpreter/v1/package-graph.js +729 -0
- package/dist/interpreter/v1/package-validator.generated.d.ts +3 -0
- package/dist/interpreter/v1/package-validator.generated.js +75902 -0
- package/dist/interpreter/v1/package.d.ts +5 -645
- package/dist/interpreter/v1/package.js +0 -12
- package/dist/interpreter/v1/pipes.d.ts +1 -0
- package/dist/interpreter/v1/pipes.js +9 -6
- package/dist/interpreter/v1/scheduler.d.ts +8 -1
- package/dist/interpreter/v1/scheduler.js +50 -11
- package/dist/interpreter/v1/shims.d.ts +260 -7
- package/dist/interpreter/v1/shims.js +647 -175
- package/dist/interpreter/v1/structural.d.ts +2 -28
- package/dist/interpreter/v1/timestamp.d.ts +7 -0
- package/dist/interpreter/v1/timestamp.js +54 -0
- package/dist/interpreter/v1/validate.d.ts +8 -2
- package/dist/interpreter/v1/validate.js +284 -200
- package/dist/interpreter/v1/value-operations.d.ts +11 -3
- package/dist/interpreter/v1/value-operations.js +70 -18
- package/dist/interpreter/values.d.ts +30 -8
- package/dist/interpreter/values.js +31 -5
- package/dist/interpreter/worker.js +4 -3
- package/package.json +4 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CATEGORY_RANGES, UPPER_MAPPING, LOWER_MAPPING, TITLE_MAPPING, } from "./unicode-data.js";
|
|
2
|
+
/** The native Unicode operations, independent of the JavaScript host's version. */
|
|
3
|
+
export function charProperty(value, bit) {
|
|
4
|
+
const scalar = value.codePointAt(0);
|
|
5
|
+
let low = 0, high = CATEGORY_RANGES.length / 3;
|
|
6
|
+
while (low < high) {
|
|
7
|
+
const middle = Math.floor((low + high) / 2);
|
|
8
|
+
if (CATEGORY_RANGES[middle * 3 + 1] < scalar)
|
|
9
|
+
low = middle + 1;
|
|
10
|
+
else
|
|
11
|
+
high = middle;
|
|
12
|
+
}
|
|
13
|
+
return (low * 3 < CATEGORY_RANGES.length &&
|
|
14
|
+
CATEGORY_RANGES[low * 3] <= scalar &&
|
|
15
|
+
(CATEGORY_RANGES[low * 3 + 2] & (1 << bit)) !== 0);
|
|
16
|
+
}
|
|
17
|
+
export function charCase(value, kind) {
|
|
18
|
+
const table = kind === "upper"
|
|
19
|
+
? UPPER_MAPPING
|
|
20
|
+
: kind === "lower"
|
|
21
|
+
? LOWER_MAPPING
|
|
22
|
+
: TITLE_MAPPING;
|
|
23
|
+
const scalar = value.codePointAt(0);
|
|
24
|
+
let low = 0, high = table.length / 2;
|
|
25
|
+
while (low < high) {
|
|
26
|
+
const middle = Math.floor((low + high) / 2);
|
|
27
|
+
if (table[middle * 2] < scalar)
|
|
28
|
+
low = middle + 1;
|
|
29
|
+
else
|
|
30
|
+
high = middle;
|
|
31
|
+
}
|
|
32
|
+
return table[low * 2] === scalar
|
|
33
|
+
? String.fromCodePoint(table[low * 2 + 1])
|
|
34
|
+
: value;
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Role handles retain their own identity independently of their occupant. */
|
|
2
|
+
export function actorIdentical(left, right) {
|
|
3
|
+
if ((left?.kind !== "actor" && left?.kind !== "supervisor") ||
|
|
4
|
+
(right?.kind !== "actor" && right?.kind !== "supervisor"))
|
|
5
|
+
throw new Error("actor identity requires actor or supervisor handles");
|
|
6
|
+
return left.kind === right.kind && left.id === right.id;
|
|
7
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Pending strict actor turns. Keys identify incarnations, never role handles. */
|
|
2
|
+
export class ActorWaits {
|
|
3
|
+
edges = new Map();
|
|
4
|
+
ready(owner) {
|
|
5
|
+
this.edges.delete(owner);
|
|
6
|
+
}
|
|
7
|
+
pending(owner, targets) {
|
|
8
|
+
this.edges.set(owner, targets);
|
|
9
|
+
const blocked = new Set([...this.edges]
|
|
10
|
+
.filter(([, targets]) => targets.length > 0)
|
|
11
|
+
.map(([owner]) => owner));
|
|
12
|
+
for (;;) {
|
|
13
|
+
const escaped = [...blocked].filter((actor) => this.edges.get(actor).some((target) => !blocked.has(target)));
|
|
14
|
+
if (escaped.length === 0)
|
|
15
|
+
break;
|
|
16
|
+
for (const actor of escaped)
|
|
17
|
+
blocked.delete(actor);
|
|
18
|
+
}
|
|
19
|
+
if (!blocked.has(owner))
|
|
20
|
+
return null;
|
|
21
|
+
const path = [];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
let next = owner;
|
|
24
|
+
while (!seen.has(next)) {
|
|
25
|
+
seen.add(next);
|
|
26
|
+
path.push(next);
|
|
27
|
+
next = this.edges.get(next)[0];
|
|
28
|
+
}
|
|
29
|
+
path.push(next);
|
|
30
|
+
this.ready(owner);
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type VmValue } from "../values.js";
|
|
2
|
+
/** Authority comes only from the embedding host, never from a path or cwd. */
|
|
3
|
+
export interface DirectoryRoots {
|
|
4
|
+
read?: FileSystemDirectoryHandle;
|
|
5
|
+
write?: FileSystemDirectoryHandle;
|
|
6
|
+
}
|
|
7
|
+
export type BrowserFileOwner = {
|
|
8
|
+
access: "read" | "write";
|
|
9
|
+
handle: FileSystemFileHandle | null;
|
|
10
|
+
cursor: number;
|
|
11
|
+
closed: boolean;
|
|
12
|
+
tail: Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
export type BrowserDirectoryOwner = {
|
|
15
|
+
access: "read" | "write";
|
|
16
|
+
handle: FileSystemDirectoryHandle | null;
|
|
17
|
+
closed: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type BrowserFsOwner = BrowserFileOwner | BrowserDirectoryOwner;
|
|
20
|
+
export interface BrowserIoError {
|
|
21
|
+
kind: number;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
export interface BrowserIoResult {
|
|
25
|
+
value: VmValue;
|
|
26
|
+
error: BrowserIoError | null;
|
|
27
|
+
}
|
|
28
|
+
export declare const DIRECTORY_OPERATIONS: {
|
|
29
|
+
readonly hew_dir_open_read: {
|
|
30
|
+
readonly release: "hew_dir_read_close";
|
|
31
|
+
readonly action: "root";
|
|
32
|
+
readonly access: "read";
|
|
33
|
+
};
|
|
34
|
+
readonly hew_dir_open_write: {
|
|
35
|
+
readonly release: "hew_dir_write_close";
|
|
36
|
+
readonly action: "root";
|
|
37
|
+
readonly access: "write";
|
|
38
|
+
};
|
|
39
|
+
readonly hew_dir_read_subdir: {
|
|
40
|
+
readonly release: "hew_dir_read_close";
|
|
41
|
+
readonly action: "subdir";
|
|
42
|
+
readonly access: "read";
|
|
43
|
+
};
|
|
44
|
+
readonly hew_dir_write_subdir: {
|
|
45
|
+
readonly release: "hew_dir_write_close";
|
|
46
|
+
readonly action: "subdir";
|
|
47
|
+
readonly access: "write";
|
|
48
|
+
};
|
|
49
|
+
readonly hew_dir_read_open_file: {
|
|
50
|
+
readonly release: "hew_file_read_close";
|
|
51
|
+
readonly action: "file";
|
|
52
|
+
readonly access: "read";
|
|
53
|
+
};
|
|
54
|
+
readonly hew_dir_write_open_file: {
|
|
55
|
+
readonly release: "hew_file_write_close";
|
|
56
|
+
readonly action: "file";
|
|
57
|
+
readonly access: "write";
|
|
58
|
+
};
|
|
59
|
+
readonly hew_dir_read_is_valid: {
|
|
60
|
+
readonly action: "valid";
|
|
61
|
+
readonly access: "read";
|
|
62
|
+
};
|
|
63
|
+
readonly hew_dir_write_is_valid: {
|
|
64
|
+
readonly action: "valid";
|
|
65
|
+
readonly access: "write";
|
|
66
|
+
};
|
|
67
|
+
readonly hew_file_read_is_valid: {
|
|
68
|
+
readonly action: "valid";
|
|
69
|
+
readonly access: "read";
|
|
70
|
+
};
|
|
71
|
+
readonly hew_file_write_is_valid: {
|
|
72
|
+
readonly action: "valid";
|
|
73
|
+
readonly access: "write";
|
|
74
|
+
};
|
|
75
|
+
readonly hew_dir_read_close: {
|
|
76
|
+
readonly action: "close";
|
|
77
|
+
readonly access: "read";
|
|
78
|
+
};
|
|
79
|
+
readonly hew_dir_write_close: {
|
|
80
|
+
readonly action: "close";
|
|
81
|
+
readonly access: "write";
|
|
82
|
+
};
|
|
83
|
+
readonly hew_file_read_close: {
|
|
84
|
+
readonly action: "close";
|
|
85
|
+
readonly access: "read";
|
|
86
|
+
};
|
|
87
|
+
readonly hew_file_write_close: {
|
|
88
|
+
readonly action: "close";
|
|
89
|
+
readonly access: "write";
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
export type DirectoryOperation = (typeof DIRECTORY_OPERATIONS)[keyof typeof DIRECTORY_OPERATIONS];
|
|
93
|
+
export declare class BrowserDirectories {
|
|
94
|
+
private readonly roots;
|
|
95
|
+
private readonly owners;
|
|
96
|
+
retire(release: (value: Extract<VmValue, {
|
|
97
|
+
kind: "host_resource";
|
|
98
|
+
}>) => void): void;
|
|
99
|
+
/** OPFS has no symlink entries. Picker-backed filesystem link confinement is
|
|
100
|
+
* not standardized, so this adapter refuses those handles before execution. */
|
|
101
|
+
static admission(roots: DirectoryRoots | undefined): Promise<string | null>;
|
|
102
|
+
constructor(roots?: DirectoryRoots);
|
|
103
|
+
call(operation: DirectoryOperation, args: VmValue[], resource?: number | null): VmValue | Promise<BrowserIoResult>;
|
|
104
|
+
private acquire;
|
|
105
|
+
private owned;
|
|
106
|
+
io(operation: "FileRead" | "FileWrite", args: VmValue[], cancelled: () => boolean): Promise<BrowserIoResult>;
|
|
107
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UNIT } from "../values.js";
|
|
1
|
+
import { 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 {
|
|
@@ -102,7 +102,7 @@ function* fromBridge(pkg, shape, id, value, context) {
|
|
|
102
102
|
if (node === "Bool")
|
|
103
103
|
return { kind: "bool", value: Boolean(value) };
|
|
104
104
|
if (node === "Char" || node === "String")
|
|
105
|
-
return
|
|
105
|
+
return stringValue(String(value));
|
|
106
106
|
if (node === "Bytes")
|
|
107
107
|
return {
|
|
108
108
|
kind: "vector",
|
|
@@ -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
|
}
|
|
@@ -216,13 +216,10 @@ export function* executeCodec(pkg, term, input, bridge, propertyInput) {
|
|
|
216
216
|
elementType: "u8",
|
|
217
217
|
items: bytes.map((byte) => ({ kind: "i64", value: BigInt(byte) })),
|
|
218
218
|
}
|
|
219
|
-
: {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
ignoreBOM: true,
|
|
224
|
-
}).decode(new Uint8Array(bytes)),
|
|
225
|
-
};
|
|
219
|
+
: stringValue(new TextDecoder("utf-8", {
|
|
220
|
+
fatal: true,
|
|
221
|
+
ignoreBOM: true,
|
|
222
|
+
}).decode(new Uint8Array(bytes)));
|
|
226
223
|
}
|
|
227
224
|
const root = result.ok ? shape.root : term.decoded.error_root;
|
|
228
225
|
const context = {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { RunOptions, SandboxTrace } from "../types.js";
|
|
2
|
-
import type {
|
|
3
|
-
export declare function runPackageV1(pkg:
|
|
2
|
+
import type { CheckedPackage } from "../schema-validator.js";
|
|
3
|
+
export declare function runPackageV1(pkg: CheckedPackage, options?: RunOptions): Promise<SandboxTrace>;
|