@hew-lang/sandbox-vm 0.6.6 → 0.7.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/README.md +5 -0
- package/dist/interpreter/parity-runner.js +6 -3
- package/dist/interpreter/v1/encoding-values.d.ts +8 -0
- package/dist/interpreter/v1/encoding-values.js +215 -0
- package/dist/interpreter/v1/exec.js +16 -0
- package/dist/interpreter/v1/shims.d.ts +3 -0
- package/dist/interpreter/v1/shims.js +13 -0
- package/dist/interpreter/values.d.ts +5 -0
- package/dist/interpreter/values.js +6 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -94,6 +94,11 @@ option. The Node test host loads that package automatically; in a source
|
|
|
94
94
|
checkout, run `make wasm` to build `hew-wasm/pkg` first. An explicit
|
|
95
95
|
`HEW_WASM_MODULE` module URL can select another build of the same package.
|
|
96
96
|
|
|
97
|
+
JSON, TOML and YAML values use this same bridge for parsing and formatting.
|
|
98
|
+
Their ordinary value operations execute in the VM, including independent copies,
|
|
99
|
+
checked access, mutation and cleanup. The bridge preserves full-width JSON
|
|
100
|
+
integers, YAML mapping keys and tags, and TOML datetimes using the native engines.
|
|
101
|
+
|
|
97
102
|
UTC calendar operations also require the initialized `executeCalendar`
|
|
98
103
|
export as `calendarBridge`. Both hosts use the shared Chrono calendar engine;
|
|
99
104
|
the bridge preserves integer seconds and nanoseconds exactly.
|
|
@@ -50,9 +50,12 @@ try {
|
|
|
50
50
|
let codecBridge;
|
|
51
51
|
if (typeof bytecode === "object" &&
|
|
52
52
|
bytecode !== null &&
|
|
53
|
-
"codecs" in bytecode &&
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
(("codecs" in bytecode &&
|
|
54
|
+
Array.isArray(bytecode.codecs) &&
|
|
55
|
+
bytecode.codecs.length) ||
|
|
56
|
+
("runtime_families" in bytecode &&
|
|
57
|
+
Array.isArray(bytecode.runtime_families) &&
|
|
58
|
+
bytecode.runtime_families.some((entry) => entry?.family === "Encoding")))) {
|
|
56
59
|
const local = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../hew-wasm/pkg/hew_wasm.js");
|
|
57
60
|
const location = process.env.HEW_WASM_MODULE ?? pathToFileURL(local).href;
|
|
58
61
|
const wasm = await import(location);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type VmValue } from "../values.js";
|
|
2
|
+
import type { EncodingOp, EncodingFormat } from "./instructions.generated.js";
|
|
3
|
+
import type { ShimHost } from "./shims.js";
|
|
4
|
+
/** Lossless positional bridge to each format's native Value. Null denotes an invalid
|
|
5
|
+
* handle; a live JSON or YAML null is ["null"]. Numeric text retains native kinds. */
|
|
6
|
+
export type { EncodingFormat } from "./instructions.generated.js";
|
|
7
|
+
export type EncodingNode = ["null"] | ["bool", boolean] | ["integer" | "float" | "string", string] | ["array", EncodingNode[]] | ["object", [string, EncodingNode][]] | ["mapping", [EncodingNode, EncodingNode][]] | ["tagged", string, EncodingNode] | ["datetime", string];
|
|
8
|
+
export declare function executeEncoding(host: ShimHost, format: EncodingFormat, operation: EncodingOp, args: VmValue[]): VmValue;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { cloneValue, compareUtf8, stringValue, UNIT, } from "../values.js";
|
|
2
|
+
const owner = (format, value) => ({ kind: "encoding", format, value });
|
|
3
|
+
const integer = (value) => ({
|
|
4
|
+
kind: "i64",
|
|
5
|
+
value: BigInt(value),
|
|
6
|
+
});
|
|
7
|
+
function checkedValue(arg, format) {
|
|
8
|
+
if (arg?.kind !== "encoding" || arg.format !== format)
|
|
9
|
+
throw new TypeError("encoding operation requires a matching format value");
|
|
10
|
+
return arg;
|
|
11
|
+
}
|
|
12
|
+
function text(arg) {
|
|
13
|
+
if (arg?.kind !== "string")
|
|
14
|
+
throw new TypeError("encoding operation requires a string");
|
|
15
|
+
return arg.value;
|
|
16
|
+
}
|
|
17
|
+
function number(arg) {
|
|
18
|
+
if (arg?.kind !== "i64")
|
|
19
|
+
throw new TypeError("encoding operation requires an integer");
|
|
20
|
+
return arg.value;
|
|
21
|
+
}
|
|
22
|
+
function equal(format, left, right) {
|
|
23
|
+
if (!left || !right || left[0] !== right[0])
|
|
24
|
+
return false;
|
|
25
|
+
switch (left[0]) {
|
|
26
|
+
case "null":
|
|
27
|
+
return true;
|
|
28
|
+
case "float":
|
|
29
|
+
return (float(left[1]) === float(right[1]) ||
|
|
30
|
+
(format === "Yaml" &&
|
|
31
|
+
Number.isNaN(float(left[1])) &&
|
|
32
|
+
Number.isNaN(float(right[1]))));
|
|
33
|
+
case "integer":
|
|
34
|
+
return BigInt(left[1]) === BigInt(right[1]);
|
|
35
|
+
case "datetime":
|
|
36
|
+
case "string":
|
|
37
|
+
case "bool":
|
|
38
|
+
return left[1] === right[1];
|
|
39
|
+
case "array": {
|
|
40
|
+
const items = right[1];
|
|
41
|
+
return (left[1].length === items.length &&
|
|
42
|
+
left[1].every((item, i) => equal(format, item, items[i])));
|
|
43
|
+
}
|
|
44
|
+
case "tagged":
|
|
45
|
+
return (left[1] === right[1] && equal(format, left[2], right[2]));
|
|
46
|
+
case "mapping": {
|
|
47
|
+
const entries = right[1];
|
|
48
|
+
return (left[1].length === entries.length &&
|
|
49
|
+
left[1].every(([key, child]) => {
|
|
50
|
+
const entry = entries.find(([candidate]) => equal(format, key, candidate));
|
|
51
|
+
return entry !== undefined && equal(format, child, entry[1]);
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
case "object": {
|
|
55
|
+
const entries = new Map(right[1]);
|
|
56
|
+
return (left[1].length === entries.size &&
|
|
57
|
+
left[1].every(([key, child]) => equal(format, child, entries.get(key) ?? null)));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function executeEncoding(host, format, operation, args) {
|
|
62
|
+
const owned = (node) => owner(format, node);
|
|
63
|
+
const value = (arg) => checkedValue(arg, format);
|
|
64
|
+
const copy = (node) => cloneValue(owned(node));
|
|
65
|
+
if (operation === "Parse" || operation === "Stringify") {
|
|
66
|
+
if (!host.codecBridge)
|
|
67
|
+
throw new Error("Encoding text operations require the hew-wasm executeCodec bridge");
|
|
68
|
+
const result = JSON.parse(host.codecBridge(JSON.stringify({
|
|
69
|
+
encoding_value: operation === "Parse"
|
|
70
|
+
? { format, operation: "parse", text: text(args[0]) }
|
|
71
|
+
: { format, operation: "stringify", value: value(args[0]).value },
|
|
72
|
+
})));
|
|
73
|
+
if (result.failure)
|
|
74
|
+
throw new Error(result.failure);
|
|
75
|
+
host.encodingError(format, result.error ?? "");
|
|
76
|
+
return operation === "Parse"
|
|
77
|
+
? owned(result.value ?? null)
|
|
78
|
+
: stringValue(result.text ?? "");
|
|
79
|
+
}
|
|
80
|
+
switch (operation) {
|
|
81
|
+
case "LastError":
|
|
82
|
+
return stringValue(host.encodingError(format));
|
|
83
|
+
case "ObjectNew":
|
|
84
|
+
return owned(format === "Yaml" ? ["mapping", []] : ["object", []]);
|
|
85
|
+
case "ArrayNew":
|
|
86
|
+
return owned(["array", []]);
|
|
87
|
+
case "FromNull":
|
|
88
|
+
return owned(["null"]);
|
|
89
|
+
case "FromString":
|
|
90
|
+
return owned(["string", text(args[0])]);
|
|
91
|
+
case "FromBool":
|
|
92
|
+
return owned(["bool", number(args[0]) !== 0n]);
|
|
93
|
+
case "FromInt":
|
|
94
|
+
return owned(["integer", BigInt.asIntN(64, number(args[0])).toString()]);
|
|
95
|
+
case "FromU64":
|
|
96
|
+
return owned(["integer", BigInt.asUintN(64, number(args[0])).toString()]);
|
|
97
|
+
case "FromFloat": {
|
|
98
|
+
const arg = args[0];
|
|
99
|
+
if (arg?.kind !== "f64")
|
|
100
|
+
throw new TypeError("encoding operation requires a float");
|
|
101
|
+
return owned(format !== "Json" || Number.isFinite(arg.value)
|
|
102
|
+
? ["float", Object.is(arg.value, -0) ? "-0" : String(arg.value)]
|
|
103
|
+
: null);
|
|
104
|
+
}
|
|
105
|
+
case "Eq":
|
|
106
|
+
return integer(equal(format, value(args[0]).value, value(args[1]).value) ? 1 : 0);
|
|
107
|
+
case "Clone":
|
|
108
|
+
return copy(value(args[0]).value);
|
|
109
|
+
case "Free":
|
|
110
|
+
value(args[0]).value = null;
|
|
111
|
+
return UNIT;
|
|
112
|
+
default:
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
const receiver = value(args[0]), node = receiver.value;
|
|
116
|
+
let scalar = node;
|
|
117
|
+
while (scalar?.[0] === "tagged")
|
|
118
|
+
scalar = scalar[2];
|
|
119
|
+
switch (operation) {
|
|
120
|
+
case "Type":
|
|
121
|
+
return integer(node?.[0] === "tagged" || node?.[0] === "datetime"
|
|
122
|
+
? 7
|
|
123
|
+
: node?.[0] === "mapping"
|
|
124
|
+
? 6
|
|
125
|
+
: node
|
|
126
|
+
? [
|
|
127
|
+
"null",
|
|
128
|
+
"bool",
|
|
129
|
+
"integer",
|
|
130
|
+
"float",
|
|
131
|
+
"string",
|
|
132
|
+
"array",
|
|
133
|
+
"object",
|
|
134
|
+
].indexOf(node[0])
|
|
135
|
+
: -1);
|
|
136
|
+
case "IntStatus":
|
|
137
|
+
return integer(node?.[0] === "integer"
|
|
138
|
+
? BigInt(node[1]) <= (1n << 63n) - 1n
|
|
139
|
+
? 1
|
|
140
|
+
: 0
|
|
141
|
+
: -1);
|
|
142
|
+
case "GetBool":
|
|
143
|
+
return integer(scalar?.[0] === "bool" && scalar[1] ? 1 : 0);
|
|
144
|
+
case "GetInt":
|
|
145
|
+
return integer(scalar?.[0] === "integer" && BigInt(scalar[1]) <= (1n << 63n) - 1n
|
|
146
|
+
? BigInt(scalar[1])
|
|
147
|
+
: 0);
|
|
148
|
+
case "GetU64":
|
|
149
|
+
return integer(node?.[0] === "integer" && BigInt(node[1]) >= 0 ? BigInt(node[1]) : 0);
|
|
150
|
+
case "GetFloat":
|
|
151
|
+
return {
|
|
152
|
+
kind: "f64",
|
|
153
|
+
value: scalar?.[0] === "float" || scalar?.[0] === "integer"
|
|
154
|
+
? float(scalar[1])
|
|
155
|
+
: 0,
|
|
156
|
+
};
|
|
157
|
+
case "GetString":
|
|
158
|
+
return stringValue(scalar?.[0] === "string" ? scalar[1] : "");
|
|
159
|
+
case "ArrayLen":
|
|
160
|
+
return integer(node?.[0] === "array" ? node[1].length : -1);
|
|
161
|
+
case "GetDatetime":
|
|
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)
|
|
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);
|
|
173
|
+
case "ObjectKeys":
|
|
174
|
+
return owned(node?.[0] === "object"
|
|
175
|
+
? ["array", node[1].map(([key]) => ["string", key])]
|
|
176
|
+
: node?.[0] === "mapping"
|
|
177
|
+
? ["array", node[1].map(([key]) => structuredClone(key))]
|
|
178
|
+
: null);
|
|
179
|
+
case "ObjectSet": {
|
|
180
|
+
const child = value(args[2]), key = text(args[1]);
|
|
181
|
+
if (node?.[0] === "object" && child.value) {
|
|
182
|
+
const index = node[1].findIndex(([name]) => name === key);
|
|
183
|
+
if (index < 0) {
|
|
184
|
+
node[1].push([key, child.value]);
|
|
185
|
+
node[1].sort(([a], [b]) => compareUtf8(a, b));
|
|
186
|
+
}
|
|
187
|
+
else
|
|
188
|
+
node[1][index] = [key, child.value];
|
|
189
|
+
}
|
|
190
|
+
if (node?.[0] === "mapping" && child.value) {
|
|
191
|
+
const index = node[1].findIndex(([name]) => name[0] === "string" && name[1] === key);
|
|
192
|
+
if (index < 0)
|
|
193
|
+
node[1].push([["string", key], child.value]);
|
|
194
|
+
else
|
|
195
|
+
node[1][index] = [["string", key], child.value];
|
|
196
|
+
}
|
|
197
|
+
child.value = null;
|
|
198
|
+
return receiver;
|
|
199
|
+
}
|
|
200
|
+
case "ArrayPush": {
|
|
201
|
+
const child = value(args[1]);
|
|
202
|
+
if (node?.[0] === "array" && child.value)
|
|
203
|
+
node[1].push(child.value);
|
|
204
|
+
child.value = null;
|
|
205
|
+
return receiver;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function float(text) {
|
|
210
|
+
return text === "inf" || text === "Infinity"
|
|
211
|
+
? Infinity
|
|
212
|
+
: text === "-inf" || text === "-Infinity"
|
|
213
|
+
? -Infinity
|
|
214
|
+
: Number(text);
|
|
215
|
+
}
|
|
@@ -102,6 +102,7 @@ class ExecutorV1 {
|
|
|
102
102
|
trace;
|
|
103
103
|
stdin;
|
|
104
104
|
options;
|
|
105
|
+
encodingErrors = {};
|
|
105
106
|
current;
|
|
106
107
|
host;
|
|
107
108
|
scheduler;
|
|
@@ -140,6 +141,16 @@ class ExecutorV1 {
|
|
|
140
141
|
argv: [...(options.argv ?? ["main.hew"])],
|
|
141
142
|
unixTimeMillis: () => this.trace.virtualTimeMs,
|
|
142
143
|
calendarBridge: options.calendarBridge,
|
|
144
|
+
codecBridge: options.codecBridge,
|
|
145
|
+
encodingError: (format, message) => {
|
|
146
|
+
const actor = this.current.context.actor;
|
|
147
|
+
const errors = actor
|
|
148
|
+
? (actor.encodingErrors ??= {})
|
|
149
|
+
: this.encodingErrors;
|
|
150
|
+
if (message !== undefined)
|
|
151
|
+
errors[format] = message;
|
|
152
|
+
return errors[format] ?? "";
|
|
153
|
+
},
|
|
143
154
|
fileError: (operation) => {
|
|
144
155
|
const error = this.current.context.fileError;
|
|
145
156
|
switch (operation) {
|
|
@@ -2446,6 +2457,9 @@ class ExecutorV1 {
|
|
|
2446
2457
|
while (pending.length) {
|
|
2447
2458
|
const value = pending.pop();
|
|
2448
2459
|
switch (value.kind) {
|
|
2460
|
+
case "encoding":
|
|
2461
|
+
value.value = null;
|
|
2462
|
+
break;
|
|
2449
2463
|
case "string":
|
|
2450
2464
|
value.release();
|
|
2451
2465
|
break;
|
|
@@ -2578,6 +2592,8 @@ class ExecutorV1 {
|
|
|
2578
2592
|
this.directories.call(release.operation, [value]);
|
|
2579
2593
|
}
|
|
2580
2594
|
closeValue(value, fault) {
|
|
2595
|
+
if (value.kind === "encoding")
|
|
2596
|
+
value.value = null;
|
|
2581
2597
|
if (value.kind === "host_resource")
|
|
2582
2598
|
this.closeHostResource(value);
|
|
2583
2599
|
if (value.kind === "string")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DirectoryOperation } from "./browser-directory.js";
|
|
2
|
+
import { type EncodingFormat } from "./encoding-values.js";
|
|
2
3
|
import type { Pipes } from "./pipes.js";
|
|
3
4
|
import { type VmValue } from "../values.js";
|
|
4
5
|
import { Mt19937 } from "./mt19937.js";
|
|
@@ -12,6 +13,8 @@ export interface ShimHost {
|
|
|
12
13
|
unixTimeMillis(): number;
|
|
13
14
|
fileError(operation: "HasError" | "LastErrorKind" | "LastErrno" | "LastError"): VmValue;
|
|
14
15
|
calendarBridge?: (request: string) => string;
|
|
16
|
+
codecBridge?: (request: string) => string;
|
|
17
|
+
encodingError?(format: EncodingFormat, message?: string): string;
|
|
15
18
|
pipes?: Pipes;
|
|
16
19
|
newPipe?(capacity: number): VmValue;
|
|
17
20
|
generatorStream?(generator: VmValue): VmValue;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DIRECTORY_OPERATIONS, } from "./browser-directory.js";
|
|
2
2
|
import { executeCalendar } from "./calendar.js";
|
|
3
|
+
import { executeEncoding } from "./encoding-values.js";
|
|
3
4
|
import { charProperty, charCase } from "../unicode.js";
|
|
4
5
|
/// Runtime implementations and their host execution registration. Admission and
|
|
5
6
|
/// execution consume the same ordinary, resumable or refused disposition.
|
|
@@ -79,6 +80,18 @@ export function resolveRuntime(entry) {
|
|
|
79
80
|
case "VecSliceRange":
|
|
80
81
|
return refused(`${entry.family}::${entry.detail}`);
|
|
81
82
|
case "Encoding":
|
|
83
|
+
return (entry.detail.op === "GetDatetime"
|
|
84
|
+
? entry.detail.format === "Toml"
|
|
85
|
+
: entry.detail.format !== "Toml" ||
|
|
86
|
+
!["GetU64", "IntStatus", "FromU64", "FromNull"].includes(entry.detail.op))
|
|
87
|
+
? {
|
|
88
|
+
kind: "ordinary",
|
|
89
|
+
shim: (host, args) => executeEncoding(host, entry.detail.format, entry.detail.op, args),
|
|
90
|
+
...(entry.detail.op === "Parse" || entry.detail.op === "Stringify"
|
|
91
|
+
? { bridge: "codecBridge" }
|
|
92
|
+
: {}),
|
|
93
|
+
}
|
|
94
|
+
: refused(`Encoding::${entry.detail.format}`);
|
|
82
95
|
case "VecScalar":
|
|
83
96
|
return refused(entry.family);
|
|
84
97
|
case "Vector":
|
|
@@ -1,5 +1,6 @@
|
|
|
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";
|
|
3
4
|
export { stringValue } from "./strings.js";
|
|
4
5
|
/** One creator-owned membership list; payload destruction uses the ordinary VM release path. */
|
|
5
6
|
export interface RcOwner {
|
|
@@ -15,6 +16,10 @@ export interface RcCell {
|
|
|
15
16
|
typeName: string;
|
|
16
17
|
}
|
|
17
18
|
export type VmValue = {
|
|
19
|
+
kind: "encoding";
|
|
20
|
+
format: EncodingFormat;
|
|
21
|
+
value: EncodingNode | null;
|
|
22
|
+
} | {
|
|
18
23
|
kind: "rc";
|
|
19
24
|
cell: RcCell | null;
|
|
20
25
|
closed: boolean;
|
|
@@ -17,6 +17,8 @@ export function compareUtf8(a, b) {
|
|
|
17
17
|
const I64_LITERAL_DECIMAL = /^(?:0|-?[1-9]\d*)$/;
|
|
18
18
|
export function cloneValue(value) {
|
|
19
19
|
switch (value.kind) {
|
|
20
|
+
case "encoding":
|
|
21
|
+
return { ...value, value: structuredClone(value.value) };
|
|
20
22
|
case "rc":
|
|
21
23
|
if (value.closed)
|
|
22
24
|
throw new Error("cannot copy a released Rc");
|
|
@@ -206,6 +208,8 @@ export function renderF64(value) {
|
|
|
206
208
|
}
|
|
207
209
|
export function renderStdout(value) {
|
|
208
210
|
switch (value.kind) {
|
|
211
|
+
case "encoding":
|
|
212
|
+
return JSON.stringify(value.value);
|
|
209
213
|
case "host_resource":
|
|
210
214
|
return "FilesystemOwner";
|
|
211
215
|
case "timer":
|
|
@@ -265,6 +269,8 @@ export function renderStdout(value) {
|
|
|
265
269
|
}
|
|
266
270
|
export function toJsonValue(value) {
|
|
267
271
|
switch (value.kind) {
|
|
272
|
+
case "encoding":
|
|
273
|
+
return value.value;
|
|
268
274
|
case "host_resource":
|
|
269
275
|
return "FilesystemOwner";
|
|
270
276
|
case "timer":
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hew-lang/sandbox-vm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"hewSource": {
|
|
5
|
-
"commit": "
|
|
6
|
-
"tag": "v0.
|
|
5
|
+
"commit": "89cb17bde678afd8b1cdbd791be97a0df393e882",
|
|
6
|
+
"tag": "v0.7.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.
|
|
24
|
+
"@hew-lang/wasm": "0.7.0"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"hew",
|