@hew-lang/sandbox-vm 0.6.0-rc7 → 0.6.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 CHANGED
@@ -47,3 +47,9 @@ make sandbox-fixtures-check
47
47
  The [package contract](bytecode/package-v1.md) describes the SIR projection.
48
48
  Source fixtures are compiled by the public compiler and executed to record
49
49
  deterministic traces. Tests also compare executable sources with native Hew.
50
+
51
+ Typed serialization executes the shared Rust codecs in `@hew-lang/wasm`.
52
+ Pass its initialized `executeCodec` export as `runBytecode`'s `codecBridge`
53
+ option. The Node test host loads that package automatically; in a source
54
+ checkout, run `make wasm` to build `hew-wasm/pkg` first. An explicit
55
+ `HEW_WASM_MODULE` module URL can select another build of the same package.
@@ -0,0 +1,84 @@
1
+ import fs from "node:fs";
2
+ import process from "node:process";
3
+ import path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { classifyHewFault, runBytecode, traceToDownloadJson } from "../dist/interpreter/index.js";
6
+
7
+ function parseArgs(args) {
8
+ if (args.length !== 7 || args[1] !== "--schedule" || args[3] !== "--seed" || args[5] !== "--step-budget") {
9
+ throw new Error("usage: test-runner <bytecode.json> --schedule fifo|random --seed N --step-budget N");
10
+ }
11
+ const schedule = args[2];
12
+ if (schedule !== "fifo" && schedule !== "random") throw new Error(`unknown schedule: ${schedule}`);
13
+ const seed = BigInt(args[4]);
14
+ const budget = Number(args[6]);
15
+ if (!Number.isSafeInteger(budget) || budget <= 0) throw new Error("step budget must be a positive safe integer");
16
+ return { path: args[0], schedule, seed, budget };
17
+ }
18
+
19
+ const reportPath = process.env.HEW_TEST_REPORT;
20
+ const tracePath = process.env.HEW_TEST_TRACE_PATH;
21
+ try {
22
+ const args = parseArgs(process.argv.slice(2));
23
+ const packageValue = JSON.parse(fs.readFileSync(args.path, "utf8"));
24
+ let codecBridge;
25
+ if (packageValue.codecs?.length) {
26
+ const local = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../hew-wasm/pkg/hew_wasm.js");
27
+ const location = process.env.HEW_WASM_MODULE ?? (fs.existsSync(local) ? pathToFileURL(local).href : "@hew-lang/wasm");
28
+ const wasm = await import(location);
29
+ if (typeof wasm.default === "function") {
30
+ const resolved = location.startsWith("file:") ? location : import.meta.resolve(location);
31
+ await wasm.default({ module_or_path: fs.readFileSync(new URL(resolved.replace(/\.js$/, "_bg.wasm"))) });
32
+ }
33
+ codecBridge = wasm.executeCodec ?? wasm.default?.executeCodec;
34
+ if (typeof codecBridge !== "function") throw new Error("hew-wasm executeCodec export is missing; rebuild the Wasm package");
35
+ }
36
+ const propertyPath = reportPath ? path.join(path.dirname(reportPath), "property.cbor") : null;
37
+ const trace = runBytecode(packageValue, {
38
+ codecBridge,
39
+ propertyInput: propertyPath && fs.existsSync(propertyPath) ? [...fs.readFileSync(propertyPath)] : [],
40
+ fixtureId: "hew-test",
41
+ traceId: "trace:hew-test",
42
+ schedulerPolicy: args.schedule === "random" ? "chaos" : "round_robin",
43
+ replay: {
44
+ seed: args.seed.toString(),
45
+ step_budget: args.budget,
46
+ virtual_clock: { epoch_ms: 0, tick_ms: 1, current_ms: 0 },
47
+ inputs: [],
48
+ },
49
+ });
50
+ if (tracePath) fs.writeFileSync(tracePath, traceToDownloadJson(trace));
51
+ const final = trace.final_state;
52
+ const rejection = final.sandbox_rejections[0];
53
+ const fault = final.runtime_failures[0];
54
+ const hewFault = classifyHewFault(trace);
55
+ const passed = trace.result === "ok" && final.exit_code === 0;
56
+ const outcome = passed ? "passed" : rejection ? "refused" : fault ? "fault" : "exit";
57
+ const message = rejection?.message ?? fault?.message ?? (passed ? null : `VM result: ${trace.result}`);
58
+ const faultKind = hewFault?.kind ?? fault?.kind ?? null;
59
+ const report = {
60
+ version: 1,
61
+ coverage: final.coverage ?? {},
62
+ outcome,
63
+ status: passed ? 0 : 1,
64
+ fault_kind: faultKind,
65
+ fault_code: hewFault?.code ?? null,
66
+ message,
67
+ site_offset: null,
68
+ assertion: fault?.assertion ?? null,
69
+ schedule: args.schedule,
70
+ seed: args.seed.toString(),
71
+ steps: final.step_count,
72
+ virtual_time_ms: final.virtual_clock.current_ms,
73
+ };
74
+ if (reportPath) fs.writeFileSync(reportPath, JSON.stringify(report));
75
+ process.stdout.write(final.stdout.join(""));
76
+ process.stderr.write(final.stderr.join(""));
77
+ if (message) process.stderr.write(`${message}\n`);
78
+ process.exitCode = report.status;
79
+ } catch (error) {
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ if (reportPath) fs.writeFileSync(reportPath, JSON.stringify({ version: 1, outcome: "launch", status: 1, message }));
82
+ process.stderr.write(`hew test VM: ${message}\n`);
83
+ process.exitCode = 1;
84
+ }
@@ -57,6 +57,15 @@ try {
57
57
  });
58
58
  process.stdout.write(trace.final_state.stdout.join(""));
59
59
  process.stderr.write(trace.final_state.stderr.join(""));
60
+ if (trace.final_state.exit_code === null &&
61
+ trace.final_state.stderr.length === 0) {
62
+ process.stderr.write(`parity-runner: ${JSON.stringify({
63
+ status: trace.result,
64
+ diagnostics: trace.final_state.diagnostics,
65
+ rejections: trace.final_state.sandbox_rejections,
66
+ failures: trace.final_state.runtime_failures,
67
+ })}\n`);
68
+ }
60
69
  process.exitCode = trace.final_state.exit_code ?? 1;
61
70
  }
62
71
  catch (error) {
@@ -20,10 +20,11 @@ interface CompileOutput {
20
20
  diagnostics: Diagnostic[];
21
21
  bytecode: PackageV1 | null;
22
22
  }
23
- type SandboxCompiler = (source: string, profile?: string) => string | CompileOutput;
23
+ type SandboxCompiler = (source: string, profile?: string, files?: Map<string, string | Uint8Array>) => string | CompileOutput;
24
24
  declare global {
25
25
  var __hewSandboxCompileToSandboxBytecode: SandboxCompiler | undefined;
26
+ var __hewSandboxExecuteCodec: ((request: string) => string) | undefined;
26
27
  var compileToSandboxBytecode: SandboxCompiler | undefined;
27
28
  }
28
- export declare function runProgram(source: string, stdin: string): RunProgramResult;
29
+ export declare function runProgram(source: string, stdin: string, files?: Map<string, string | Uint8Array>): RunProgramResult;
29
30
  export {};
@@ -1,7 +1,7 @@
1
1
  import { runBytecode } from "./interpreter.js";
2
2
  const SANDBOX_PROFILE = "sandbox-vm-export";
3
- export function runProgram(source, stdin) {
4
- const compileOutput = compileSource(source);
3
+ export function runProgram(source, stdin, files) {
4
+ const compileOutput = compileSource(source, files);
5
5
  if (hasErrorDiagnostics(compileOutput.diagnostics) ||
6
6
  compileOutput.bytecode === null) {
7
7
  return {
@@ -17,6 +17,7 @@ export function runProgram(source, stdin) {
17
17
  const bytecode = compileOutput.bytecode;
18
18
  const trace = runBytecode(bytecode, {
19
19
  fixtureId: "page-run",
20
+ codecBridge: globalThis.__hewSandboxExecuteCodec,
20
21
  traceId: "trace:page-run",
21
22
  replay: { inputs: [{ kind: "stdin", data: stdin }] },
22
23
  });
@@ -30,13 +31,13 @@ export function runProgram(source, stdin) {
30
31
  diagnostics: [...compileOutput.diagnostics, ...runtimeDiagnostics(trace)],
31
32
  };
32
33
  }
33
- function compileSource(source) {
34
+ function compileSource(source, files) {
34
35
  const compiler = globalThis.__hewSandboxCompileToSandboxBytecode ??
35
36
  globalThis.compileToSandboxBytecode;
36
37
  if (!compiler) {
37
38
  throw new Error("compile_to_sandbox_bytecode WASM bridge is not initialized");
38
39
  }
39
- const output = compiler(source, SANDBOX_PROFILE);
40
+ const output = compiler(source, SANDBOX_PROFILE, files);
40
41
  const parsed = typeof output === "string" ? JSON.parse(output) : output;
41
42
  if (!isCompileOutput(parsed)) {
42
43
  throw new Error("compile_to_sandbox_bytecode returned an invalid payload");
@@ -21,6 +21,7 @@ export declare class TraceBuilder {
21
21
  private readonly traceId;
22
22
  readonly replay: ReplayConfig;
23
23
  private readonly sandboxVersion;
24
+ readonly coverage: Record<string, number>;
24
25
  readonly stdout: string[];
25
26
  readonly stderr: string[];
26
27
  readonly runtimeFailures: RuntimeFailure[];
@@ -5,6 +5,7 @@ export class TraceBuilder {
5
5
  traceId;
6
6
  replay;
7
7
  sandboxVersion;
8
+ coverage = {};
8
9
  stdout = [];
9
10
  stderr = [];
10
11
  runtimeFailures = [];
@@ -174,6 +175,9 @@ export class TraceBuilder {
174
175
  replay: this.replay,
175
176
  events: this.events,
176
177
  final_state: {
178
+ ...(Object.keys(this.coverage).length > 0
179
+ ? { coverage: this.coverage }
180
+ : {}),
177
181
  status: this.status,
178
182
  exit_code: this.status === "ok" ? this.exitCode : null,
179
183
  step_count: this.stepCount,
@@ -91,6 +91,7 @@ export interface SandboxTrace {
91
91
  replay: ReplayConfig;
92
92
  events: TraceEvent[];
93
93
  final_state: {
94
+ coverage?: Record<string, number>;
94
95
  status: RuntimeStatus;
95
96
  exit_code: number | null;
96
97
  step_count: number;
@@ -116,6 +117,8 @@ export interface SandboxTrace {
116
117
  };
117
118
  }
118
119
  export interface RunOptions {
120
+ codecBridge?: (request: string) => string;
121
+ propertyInput?: number[];
119
122
  fixtureId?: string;
120
123
  traceId?: string;
121
124
  replay?: Partial<ReplayConfig>;
@@ -0,0 +1,9 @@
1
+ import { type VmValue } from "../values.js";
2
+ import { type PackageV1, type TermV1 } from "./package.js";
3
+ import { type ValueProgram } from "./value-operations.js";
4
+ export type CodecBridge = (request: string) => string;
5
+ type CodecTerm = Extract<TermV1, {
6
+ op: "wire.codec";
7
+ }>;
8
+ export declare function executeCodec(pkg: PackageV1, term: CodecTerm, input: VmValue, bridge: CodecBridge | undefined, propertyInput: number[]): ValueProgram;
9
+ export {};
@@ -0,0 +1,266 @@
1
+ import { UNIT } from "../values.js";
2
+ import { runtimeTag } from "./package.js";
3
+ import { collectionOperation } from "./value-operations.js";
4
+ class DecodeFailure extends Error {
5
+ }
6
+ class Duplicate extends Error {
7
+ detail;
8
+ constructor(detail) {
9
+ super("duplicate decoded key");
10
+ this.detail = detail;
11
+ }
12
+ }
13
+ function array(value) {
14
+ if (!Array.isArray(value))
15
+ throw new Error("codec bridge returned a non-positional value");
16
+ return value;
17
+ }
18
+ function fields(value) {
19
+ if (value.kind === "record")
20
+ return value.fields;
21
+ if (value.kind === "vector")
22
+ return value.items;
23
+ throw new Error(`codec aggregate received ${value.kind}`);
24
+ }
25
+ function toBridge(pkg, shape, id, value) {
26
+ const node = shape.nodes[id];
27
+ if (node === "Unit")
28
+ return null;
29
+ if (node === "Bool" && value.kind === "bool")
30
+ return value.value;
31
+ if ((node === "Char" || node === "String") && value.kind === "string")
32
+ return value.value;
33
+ if (node === "Bytes")
34
+ return fields(value).map((byte) => {
35
+ if (byte.kind !== "i64")
36
+ throw new Error("invalid byte");
37
+ return Number(byte.value);
38
+ });
39
+ if (typeof node === "string")
40
+ throw new Error(`invalid codec scalar ${node}`);
41
+ if ("Float" in node) {
42
+ if (value.kind !== "f64")
43
+ throw new Error("codec float has wrong VM kind");
44
+ const number = value.value;
45
+ return Object.is(number, -0) ? "-0" : String(number);
46
+ }
47
+ if ("Signed" in node || "Unsigned" in node) {
48
+ if (value.kind !== "i64")
49
+ throw new Error("codec integer has wrong VM kind");
50
+ return ("Unsigned" in node ? BigInt.asUintN(64, value.value) : value.value).toString();
51
+ }
52
+ if ("Tuple" in node)
53
+ return fields(value).map((field, i) => toBridge(pkg, shape, node.Tuple[i], field));
54
+ if ("Record" in node)
55
+ return fields(value).map((field, i) => toBridge(pkg, shape, node.Record[i][1], field));
56
+ if ("Array" in node || "Sequence" in node) {
57
+ const child = "Array" in node ? node.Array.element : node.Sequence;
58
+ return fields(value).map((item) => toBridge(pkg, shape, child, item));
59
+ }
60
+ if ("Option" in node) {
61
+ if (value.kind !== "enum")
62
+ throw new Error("codec Option is not a variant");
63
+ return value.tag === runtimeTag(pkg.variants[value.shape], "OptionNone")
64
+ ? null
65
+ : [toBridge(pkg, shape, node.Option, value.payload[0])];
66
+ }
67
+ if ("Variant" in node) {
68
+ if (value.kind !== "enum")
69
+ throw new Error("codec enum is not a variant");
70
+ const child = node.Variant[value.tag][1];
71
+ if (child === null)
72
+ return [value.tag, null];
73
+ const payload = shape.grouped_payloads[child]
74
+ ? { kind: "record", typeId: "", fields: value.payload }
75
+ : value.payload[0];
76
+ return [value.tag, toBridge(pkg, shape, child, payload)];
77
+ }
78
+ if (value.kind !== "map")
79
+ throw new Error("codec collection is not a map");
80
+ if ("Set" in node)
81
+ return [...value.entries.values()].map((entry) => [
82
+ toBridge(pkg, shape, node.Set, entry.key),
83
+ null,
84
+ ]);
85
+ return [...value.entries.values()].map((entry) => [
86
+ toBridge(pkg, shape, node.Map.key, entry.key),
87
+ toBridge(pkg, shape, node.Map.value, entry.value),
88
+ ]);
89
+ }
90
+ function* fromBridge(pkg, shape, id, value, context) {
91
+ if (value !== null &&
92
+ typeof value === "object" &&
93
+ !Array.isArray(value) &&
94
+ value.decode_error)
95
+ throw new DecodeFailure();
96
+ const node = shape.nodes[id];
97
+ const name = shape.names[id] ?? "";
98
+ const aggregate = shape.aggregates[id] ?? undefined;
99
+ const variant = shape.variants[id];
100
+ if (node === "Unit")
101
+ return UNIT;
102
+ if (node === "Bool")
103
+ return { kind: "bool", value: Boolean(value) };
104
+ if (node === "Char" || node === "String")
105
+ return { kind: "string", value: String(value) };
106
+ if (node === "Bytes")
107
+ return {
108
+ kind: "vector",
109
+ elementType: "u8",
110
+ items: array(value).map((byte) => ({
111
+ kind: "i64",
112
+ value: BigInt(String(byte)),
113
+ })),
114
+ };
115
+ if (typeof node === "string")
116
+ throw new Error(`invalid codec scalar ${node}`);
117
+ if ("Float" in node) {
118
+ const number = value === "inf" ? Infinity : value === "-inf" ? -Infinity : Number(value);
119
+ return { kind: "f64", value: number };
120
+ }
121
+ if ("Signed" in node || "Unsigned" in node)
122
+ return { kind: "i64", value: BigInt(String(value)) };
123
+ if ("Option" in node) {
124
+ if (variant === null || variant === undefined)
125
+ throw new Error("codec Option lacks its shape");
126
+ return {
127
+ kind: "enum",
128
+ typeId: name,
129
+ shape: variant,
130
+ tag: runtimeTag(pkg.variants[variant], value === null ? "OptionNone" : "OptionSome"),
131
+ payload: value === null
132
+ ? []
133
+ : [
134
+ yield* fromBridge(pkg, shape, node.Option, array(value)[0], context),
135
+ ],
136
+ };
137
+ }
138
+ if ("Variant" in node) {
139
+ if (variant === null || variant === undefined)
140
+ throw new Error("codec enum lacks its shape");
141
+ const pair = array(value), tag = Number(pair[0]), child = node.Variant[tag][1];
142
+ let payload = [];
143
+ if (child !== null) {
144
+ const decoded = yield* fromBridge(pkg, shape, child, pair[1], context);
145
+ payload = shape.grouped_payloads[child] ? fields(decoded) : [decoded];
146
+ }
147
+ return { kind: "enum", typeId: name, shape: variant, tag, payload };
148
+ }
149
+ if ("Map" in node || "Set" in node) {
150
+ const family = "Map" in node ? "Map" : "Set";
151
+ const map = yield* collectionOperation(pkg, family, "New", [], [], null);
152
+ for (const item of array(value)) {
153
+ const itemFields = array(item);
154
+ const pair = "Map" in node ? itemFields : [itemFields[0], null];
155
+ const key = yield* fromBridge(pkg, shape, "Map" in node ? node.Map.key : node.Set, pair[0], context);
156
+ const member = "Map" in node
157
+ ? yield* fromBridge(pkg, shape, node.Map.value, pair[1], context)
158
+ : UNIT;
159
+ if (map.kind !== "map")
160
+ throw new Error("codec collection has no map storage");
161
+ const size = map.entries.size;
162
+ yield* collectionOperation(pkg, family, "Insert", [map, key, member], context.callbacks[id] ?? [], null);
163
+ if (!context.propertyInput && map.entries.size === size)
164
+ throw new Duplicate(itemFields["Map" in node ? 2 : 1]);
165
+ }
166
+ return map;
167
+ }
168
+ const values = [];
169
+ for (const [i, item] of array(value).entries()) {
170
+ const child = "Tuple" in node
171
+ ? node.Tuple[i]
172
+ : "Record" in node
173
+ ? node.Record[i][1]
174
+ : "Array" in node
175
+ ? node.Array.element
176
+ : node.Sequence;
177
+ values.push(yield* fromBridge(pkg, shape, child, item, context));
178
+ }
179
+ return "Array" in node || "Sequence" in node
180
+ ? { kind: "vector", elementType: "", items: values }
181
+ : { kind: "record", typeId: name, shape: aggregate, fields: values };
182
+ }
183
+ export function* executeCodec(pkg, term, input, bridge, propertyInput) {
184
+ if (!bridge)
185
+ throw new Error("typed codecs require the hew-wasm executeCodec bridge");
186
+ const encoded = pkg.codecs?.[term.codec];
187
+ if (encoded === undefined)
188
+ throw new Error("codec shape is absent");
189
+ const shape = JSON.parse(encoded);
190
+ const request = {
191
+ shape: encoded,
192
+ format: term.format,
193
+ direction: term.direction,
194
+ error_root: term.decoded?.error_root,
195
+ ...(term.direction === "encode"
196
+ ? { value: toBridge(pkg, shape, shape.root, input) }
197
+ : term.direction === "test_input"
198
+ ? {
199
+ input: propertyInput,
200
+ ordinal: input.kind === "i64" ? Number(input.value) : -1,
201
+ }
202
+ : {
203
+ input: input.kind === "string"
204
+ ? [...new TextEncoder().encode(input.value)]
205
+ : fields(input).map((byte) => byte.kind === "i64" ? Number(byte.value) : -1),
206
+ }),
207
+ };
208
+ let result = JSON.parse(bridge(JSON.stringify(request)));
209
+ if (result.failure !== undefined)
210
+ throw new Error(result.failure);
211
+ if (term.direction === "encode") {
212
+ const bytes = array(result.value).map(Number);
213
+ return term.format === 0 || term.format === 4
214
+ ? {
215
+ kind: "vector",
216
+ elementType: "u8",
217
+ items: bytes.map((byte) => ({ kind: "i64", value: BigInt(byte) })),
218
+ }
219
+ : {
220
+ kind: "string",
221
+ value: new TextDecoder("utf-8", {
222
+ fatal: true,
223
+ ignoreBOM: true,
224
+ }).decode(new Uint8Array(bytes)),
225
+ };
226
+ }
227
+ const root = result.ok ? shape.root : term.decoded.error_root;
228
+ const context = {
229
+ callbacks: term.callbacks,
230
+ propertyInput: term.direction === "test_input",
231
+ };
232
+ let decoded;
233
+ try {
234
+ decoded = yield* fromBridge(pkg, shape, root, result.value, context);
235
+ }
236
+ catch (error) {
237
+ if (!term.decoded)
238
+ throw error;
239
+ if (error instanceof DecodeFailure && result.error !== undefined) {
240
+ result = { ok: false, value: result.error };
241
+ }
242
+ else if (error instanceof Duplicate) {
243
+ result = JSON.parse(bridge(JSON.stringify({
244
+ ...request,
245
+ direction: "error",
246
+ input: [],
247
+ duplicate: error.detail,
248
+ })));
249
+ }
250
+ else {
251
+ throw error;
252
+ }
253
+ if (result.failure !== undefined)
254
+ throw new Error(result.failure);
255
+ decoded = yield* fromBridge(pkg, shape, term.decoded.error_root, result.value, context);
256
+ }
257
+ if (!term.decoded)
258
+ return decoded;
259
+ return {
260
+ kind: "enum",
261
+ typeId: pkg.variants[term.decoded.shape].name,
262
+ shape: term.decoded.shape,
263
+ tag: result.ok ? term.decoded.ok : term.decoded.error,
264
+ payload: [decoded],
265
+ };
266
+ }