@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 +6 -0
- package/cli/test-runner.mjs +84 -0
- package/dist/interpreter/parity-runner.js +9 -0
- package/dist/interpreter/run-program.d.ts +3 -2
- package/dist/interpreter/run-program.js +5 -4
- package/dist/interpreter/trace.d.ts +1 -0
- package/dist/interpreter/trace.js +4 -0
- package/dist/interpreter/types.d.ts +3 -0
- package/dist/interpreter/v1/codecs.d.ts +9 -0
- package/dist/interpreter/v1/codecs.js +266 -0
- package/dist/interpreter/v1/exec.js +238 -34
- package/dist/interpreter/v1/package.d.ts +38 -10
- package/dist/interpreter/v1/shims.js +273 -9
- package/dist/interpreter/v1/validate.js +33 -4
- package/dist/interpreter/v1/value-operations.js +103 -13
- package/dist/interpreter/worker.js +22 -4
- package/package.json +8 -4
|
@@ -85,6 +85,12 @@ export function resolveRuntimeShim(entry) {
|
|
|
85
85
|
: undefined;
|
|
86
86
|
case "MathIntrinsic":
|
|
87
87
|
return MATH_SHIMS[detailName(entry.detail)];
|
|
88
|
+
case "FloatMethod":
|
|
89
|
+
return FLOAT_METHOD_SHIMS[detailName(entry.detail)];
|
|
90
|
+
case "IntMethod":
|
|
91
|
+
return intMethodShim(entry.detail);
|
|
92
|
+
case "IntArith":
|
|
93
|
+
return intArithShim(entry.detail);
|
|
88
94
|
default:
|
|
89
95
|
return UNIT_FAMILY_SHIMS[entry.family];
|
|
90
96
|
}
|
|
@@ -105,10 +111,10 @@ export const SUPPORTED_SUSPEND_KINDS = new Set([
|
|
|
105
111
|
"StreamSend",
|
|
106
112
|
"StreamNext",
|
|
107
113
|
]);
|
|
108
|
-
/// The
|
|
109
|
-
/// the replay stdin; every file and socket operation stays native.
|
|
114
|
+
/// The VM serves replay standard input and in-memory sink closure.
|
|
110
115
|
export const SUPPORTED_NATIVE_IO = new Set([
|
|
111
116
|
"StdinReadLine",
|
|
117
|
+
"SinkFinish",
|
|
112
118
|
]);
|
|
113
119
|
// ── families ────────────────────────────────────────────────────────────────
|
|
114
120
|
/// `Print { kind, newline }`. The rendering follows the value's own
|
|
@@ -149,10 +155,6 @@ const UNIT_FAMILY_SHIMS = {
|
|
|
149
155
|
host.closePipe(arg(args, 0));
|
|
150
156
|
return UNIT;
|
|
151
157
|
},
|
|
152
|
-
SinkFinish: (host, args) => {
|
|
153
|
-
host.pipes.close(arg(args, 0));
|
|
154
|
-
return UNIT;
|
|
155
|
-
},
|
|
156
158
|
StreamClose: (host, args) => {
|
|
157
159
|
host.closePipe(arg(args, 0));
|
|
158
160
|
return UNIT;
|
|
@@ -161,8 +163,18 @@ const UNIT_FAMILY_SHIMS = {
|
|
|
161
163
|
StringClone: (_host, args) => str(text(args, 0)),
|
|
162
164
|
StringEquals: (_host, args) => bool(text(args, 0) === text(args, 1)),
|
|
163
165
|
StringContains: (_host, args) => bool(text(args, 0).includes(text(args, 1))),
|
|
166
|
+
StringTrim: (_host, args) => str(text(args, 0).replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "")),
|
|
164
167
|
// Native string length counts codepoints, not bytes.
|
|
165
168
|
StringLen: (_host, args) => int(BigInt([...text(args, 0)].length)),
|
|
169
|
+
StringIndex: (_host, args) => {
|
|
170
|
+
const chars = [...text(args, 0)];
|
|
171
|
+
const at = index(args, 1);
|
|
172
|
+
if (at < 0 || at >= chars.length) {
|
|
173
|
+
throw new ShimFault("vector_bounds", `string index ${at} out of bounds for length ${chars.length}`);
|
|
174
|
+
}
|
|
175
|
+
return str(chars[at]);
|
|
176
|
+
},
|
|
177
|
+
StringIsEmpty: (_host, args) => bool(text(args, 0).length === 0),
|
|
166
178
|
StringReplace: (_host, args) => str(text(args, 0).split(text(args, 1)).join(text(args, 2))),
|
|
167
179
|
// Native slicing is by codepoint and clamps rather than trapping.
|
|
168
180
|
StringSlice: (_host, args) => {
|
|
@@ -183,8 +195,63 @@ const UNIT_FAMILY_SHIMS = {
|
|
|
183
195
|
// TextDecoder replaces each maximal invalid subsequence with U+FFFD, as
|
|
184
196
|
// native lossy decoding does.
|
|
185
197
|
BytesDecodeUtf8Lossy: (_host, args) => str(decodeUtf8(arg(args, 0))),
|
|
198
|
+
BytesNew: () => ({ kind: "vector", elementType: "u8", items: [] }),
|
|
186
199
|
BytesIsEmpty: (_host, args) => bool(vec(args, 0).items.length === 0),
|
|
187
200
|
BytesLen: (_host, args) => int(BigInt(vec(args, 0).items.length)),
|
|
201
|
+
BytesContains: (_host, args) => {
|
|
202
|
+
const byte = integer(args, 1);
|
|
203
|
+
return bool(vec(args, 0).items.some((item) => integer([item], 0) === byte));
|
|
204
|
+
},
|
|
205
|
+
BytesIndex: (_host, args) => {
|
|
206
|
+
const items = vec(args, 0).items;
|
|
207
|
+
const at = index(args, 1);
|
|
208
|
+
if (at < 0 || at >= items.length) {
|
|
209
|
+
throw new ShimFault("vector_bounds", `bytes index ${at} out of bounds for length ${items.length}`);
|
|
210
|
+
}
|
|
211
|
+
return cloneValue(items[at]);
|
|
212
|
+
},
|
|
213
|
+
// A native bytes range shares the buffer; a VM value is never aliased, so
|
|
214
|
+
// a copy is observably the same.
|
|
215
|
+
BytesSlice: (_host, args) => {
|
|
216
|
+
const items = vec(args, 0).items;
|
|
217
|
+
const start = index(args, 1);
|
|
218
|
+
const end = index(args, 2);
|
|
219
|
+
checkSliceBounds(start, end, items.length);
|
|
220
|
+
return byteVector(items.slice(start, end));
|
|
221
|
+
},
|
|
222
|
+
BytesSliceFrom: (_host, args) => {
|
|
223
|
+
const items = vec(args, 0).items;
|
|
224
|
+
const start = index(args, 1);
|
|
225
|
+
checkSliceBounds(start, items.length, items.length);
|
|
226
|
+
return byteVector(items.slice(start));
|
|
227
|
+
},
|
|
228
|
+
BytesConcat: (_host, args) => byteVector([...vec(args, 0).items, ...vec(args, 1).items]),
|
|
229
|
+
BytesStartsWith: (_host, args) => bool(runAt(vec(args, 0).items, vec(args, 1).items, 0)),
|
|
230
|
+
BytesEndsWith: (_host, args) => {
|
|
231
|
+
const items = vec(args, 0).items;
|
|
232
|
+
const run = vec(args, 1).items;
|
|
233
|
+
return bool(runAt(items, run, items.length - run.length));
|
|
234
|
+
},
|
|
235
|
+
// A mutating call receives its receiver by `move` and returns it.
|
|
236
|
+
BytesPush: (_host, args) => {
|
|
237
|
+
const target = vec(args, 0);
|
|
238
|
+
target.items.push(cloneValue(arg(args, 1)));
|
|
239
|
+
return target;
|
|
240
|
+
},
|
|
241
|
+
BytesSet: (_host, args) => {
|
|
242
|
+
const target = vec(args, 0);
|
|
243
|
+
const at = index(args, 1);
|
|
244
|
+
if (at < 0 || at >= target.items.length) {
|
|
245
|
+
throw new ShimFault("vector_bounds", `bytes index ${at} out of bounds for length ${target.items.length}`);
|
|
246
|
+
}
|
|
247
|
+
target.items[at] = cloneValue(arg(args, 2));
|
|
248
|
+
return target;
|
|
249
|
+
},
|
|
250
|
+
BytesClear: (_host, args) => {
|
|
251
|
+
const target = vec(args, 0);
|
|
252
|
+
target.items.length = 0;
|
|
253
|
+
return target;
|
|
254
|
+
},
|
|
188
255
|
BytesGet: (host, args, shape) => {
|
|
189
256
|
if (!shape) {
|
|
190
257
|
throw new TypeError("BytesGet names no result shape to build its Option against");
|
|
@@ -230,6 +297,15 @@ const VECTOR_SHIMS = {
|
|
|
230
297
|
target.items.push(arg(args, 1));
|
|
231
298
|
return target;
|
|
232
299
|
},
|
|
300
|
+
Remove: (_host, args) => {
|
|
301
|
+
const target = vec(args, 0);
|
|
302
|
+
const at = index(args, 1);
|
|
303
|
+
if (at < 0 || at >= target.items.length) {
|
|
304
|
+
throw new ShimFault("vector_bounds", `vector index ${at} out of bounds for length ${target.items.length}`);
|
|
305
|
+
}
|
|
306
|
+
const [item] = target.items.splice(at, 1);
|
|
307
|
+
return { kind: "record", typeId: "", fields: [target, item] };
|
|
308
|
+
},
|
|
233
309
|
Get: (host, args, shape) => {
|
|
234
310
|
if (!shape) {
|
|
235
311
|
throw new TypeError("Vector::Get names no result shape to build its Option against");
|
|
@@ -251,8 +327,9 @@ const VECTOR_SHIMS = {
|
|
|
251
327
|
Len: (_host, args) => int(BigInt(vec(args, 0).items.length)),
|
|
252
328
|
Slice: (_host, args) => {
|
|
253
329
|
const items = vec(args, 0).items;
|
|
254
|
-
const start =
|
|
255
|
-
const end =
|
|
330
|
+
const start = index(args, 1);
|
|
331
|
+
const end = index(args, 2);
|
|
332
|
+
checkSliceBounds(start, end, items.length);
|
|
256
333
|
return {
|
|
257
334
|
kind: "vector",
|
|
258
335
|
elementType: "",
|
|
@@ -261,7 +338,8 @@ const VECTOR_SHIMS = {
|
|
|
261
338
|
},
|
|
262
339
|
SliceFrom: (_host, args) => {
|
|
263
340
|
const items = vec(args, 0).items;
|
|
264
|
-
const start =
|
|
341
|
+
const start = index(args, 1);
|
|
342
|
+
checkSliceBounds(start, items.length, items.length);
|
|
265
343
|
return {
|
|
266
344
|
kind: "vector",
|
|
267
345
|
elementType: "",
|
|
@@ -269,6 +347,22 @@ const VECTOR_SHIMS = {
|
|
|
269
347
|
};
|
|
270
348
|
},
|
|
271
349
|
};
|
|
350
|
+
function byteVector(items) {
|
|
351
|
+
return { kind: "vector", elementType: "u8", items: items.map(cloneValue) };
|
|
352
|
+
}
|
|
353
|
+
/// Whether `run` occurs in `items` starting at `at`.
|
|
354
|
+
function runAt(items, run, at) {
|
|
355
|
+
return (at >= 0 &&
|
|
356
|
+
at + run.length <= items.length &&
|
|
357
|
+
run.every((byte, offset) => integer([items[at + offset]], 0) === integer([byte], 0)));
|
|
358
|
+
}
|
|
359
|
+
/// A slice outside `0 <= start <= end <= length` faults like native's
|
|
360
|
+
/// `IndexOutOfBounds` rather than clamping to the valid range.
|
|
361
|
+
function checkSliceBounds(start, end, length) {
|
|
362
|
+
if (start < 0 || end < start || end > length) {
|
|
363
|
+
throw new ShimFault("vector_bounds", `slice ${start}..${end} out of bounds for length ${length}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
272
366
|
/// `std.math`. Native emits the LLVM intrinsic for each of these, so the
|
|
273
367
|
/// mapping is to the matching IEEE operation rather than to whatever JS spells
|
|
274
368
|
/// similarly: `Round` is half away from zero, `MinF64`/`MaxF64` follow
|
|
@@ -379,6 +473,176 @@ function fma(a, b, c) {
|
|
|
379
473
|
const sumTail = product - (sum - bVirtual) + (c - bVirtual);
|
|
380
474
|
return sum + (sumTail + tail);
|
|
381
475
|
}
|
|
476
|
+
/// `f64` classification and bit methods. Native reads the IEEE bits
|
|
477
|
+
/// directly, so `IsSignNegative` tests the sign bit (true for `-0.0` and a
|
|
478
|
+
/// negative NaN) rather than comparing against zero.
|
|
479
|
+
const FLOAT_METHOD_SHIMS = {
|
|
480
|
+
ToBits: (_host, args) => int(toBits(real(args, 0))),
|
|
481
|
+
IsNan: (_host, args) => bool(Number.isNaN(real(args, 0))),
|
|
482
|
+
IsFinite: (_host, args) => bool(Number.isFinite(real(args, 0))),
|
|
483
|
+
IsInfinite: (_host, args) => {
|
|
484
|
+
const value = real(args, 0);
|
|
485
|
+
return bool(value === Infinity || value === -Infinity);
|
|
486
|
+
},
|
|
487
|
+
IsSignNegative: (_host, args) => bool(toBits(real(args, 0)) >> 63n === 1n),
|
|
488
|
+
};
|
|
489
|
+
/// The receiver width an `IntMethod`/`IntArith` detail names. The VM is a
|
|
490
|
+
/// 64-bit target, so the pointer-sized widths are 64 bits wide.
|
|
491
|
+
const INT_WIDTHS = {
|
|
492
|
+
I8: { bits: 8, signed: true },
|
|
493
|
+
I16: { bits: 16, signed: true },
|
|
494
|
+
I32: { bits: 32, signed: true },
|
|
495
|
+
I64: { bits: 64, signed: true },
|
|
496
|
+
Isize: { bits: 64, signed: true },
|
|
497
|
+
U8: { bits: 8, signed: false },
|
|
498
|
+
U16: { bits: 16, signed: false },
|
|
499
|
+
U32: { bits: 32, signed: false },
|
|
500
|
+
U64: { bits: 64, signed: false },
|
|
501
|
+
Usize: { bits: 64, signed: false },
|
|
502
|
+
};
|
|
503
|
+
/// `[op, width]`, as `RuntimeCallFamily` serializes a two-field variant.
|
|
504
|
+
function intDetail(detail) {
|
|
505
|
+
if (!Array.isArray(detail) || detail.length !== 2) {
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
const [op, width] = detail;
|
|
509
|
+
const shape = typeof width === "string" ? INT_WIDTHS[width] : undefined;
|
|
510
|
+
return typeof op === "string" && shape ? [op, shape] : undefined;
|
|
511
|
+
}
|
|
512
|
+
function wrapTo(width, value) {
|
|
513
|
+
return width.signed
|
|
514
|
+
? BigInt.asIntN(width.bits, value)
|
|
515
|
+
: BigInt.asUintN(width.bits, value);
|
|
516
|
+
}
|
|
517
|
+
function intBounds(width) {
|
|
518
|
+
const bits = BigInt(width.bits);
|
|
519
|
+
return width.signed
|
|
520
|
+
? [-(1n << (bits - 1n)), (1n << (bits - 1n)) - 1n]
|
|
521
|
+
: [0n, (1n << bits) - 1n];
|
|
522
|
+
}
|
|
523
|
+
function popcount(value) {
|
|
524
|
+
let count = 0n;
|
|
525
|
+
for (let rest = value; rest !== 0n; rest >>= 1n) {
|
|
526
|
+
count += rest & 1n;
|
|
527
|
+
}
|
|
528
|
+
return count;
|
|
529
|
+
}
|
|
530
|
+
/// Integer bit methods. Native emits `ctpop`/`ctlz`/`cttz`/`bswap`/
|
|
531
|
+
/// `bitreverse`/`fshl`/`fshr` at the receiver's width, so every operation
|
|
532
|
+
/// works on the receiver's unsigned bit pattern and the rotate amount is taken
|
|
533
|
+
/// modulo the width.
|
|
534
|
+
function intMethodShim(detail) {
|
|
535
|
+
const parsed = intDetail(detail);
|
|
536
|
+
if (!parsed) {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
const [op, width] = parsed;
|
|
540
|
+
const bits = BigInt(width.bits);
|
|
541
|
+
const unsigned = (args) => BigInt.asUintN(width.bits, integer(args, 0));
|
|
542
|
+
const result = (pattern) => int(wrapTo(width, pattern));
|
|
543
|
+
const rotate = (args, left) => {
|
|
544
|
+
const pattern = unsigned(args);
|
|
545
|
+
const amount = BigInt.asUintN(32, integer(args, 1)) % bits;
|
|
546
|
+
const shift = left ? amount : (bits - amount) % bits;
|
|
547
|
+
return result((pattern << shift) | (pattern >> ((bits - shift) % bits)));
|
|
548
|
+
};
|
|
549
|
+
switch (op) {
|
|
550
|
+
case "CountOnes":
|
|
551
|
+
return (_host, args) => int(popcount(unsigned(args)));
|
|
552
|
+
case "CountZeros":
|
|
553
|
+
return (_host, args) => int(bits - popcount(unsigned(args)));
|
|
554
|
+
case "LeadingZeros":
|
|
555
|
+
return (_host, args) => {
|
|
556
|
+
const pattern = unsigned(args);
|
|
557
|
+
return int(pattern === 0n ? bits : bits - BigInt(pattern.toString(2).length));
|
|
558
|
+
};
|
|
559
|
+
case "TrailingZeros":
|
|
560
|
+
return (_host, args) => {
|
|
561
|
+
let pattern = unsigned(args);
|
|
562
|
+
if (pattern === 0n) {
|
|
563
|
+
return int(bits);
|
|
564
|
+
}
|
|
565
|
+
let count = 0n;
|
|
566
|
+
while ((pattern & 1n) === 0n) {
|
|
567
|
+
pattern >>= 1n;
|
|
568
|
+
count += 1n;
|
|
569
|
+
}
|
|
570
|
+
return int(count);
|
|
571
|
+
};
|
|
572
|
+
case "SwapBytes":
|
|
573
|
+
return (_host, args) => {
|
|
574
|
+
let pattern = unsigned(args);
|
|
575
|
+
let swapped = 0n;
|
|
576
|
+
for (let byte = 0; byte < width.bits / 8; byte += 1) {
|
|
577
|
+
swapped = (swapped << 8n) | (pattern & 0xffn);
|
|
578
|
+
pattern >>= 8n;
|
|
579
|
+
}
|
|
580
|
+
return result(swapped);
|
|
581
|
+
};
|
|
582
|
+
case "ReverseBits":
|
|
583
|
+
return (_host, args) => {
|
|
584
|
+
let pattern = unsigned(args);
|
|
585
|
+
let reversed = 0n;
|
|
586
|
+
for (let bit = 0; bit < width.bits; bit += 1) {
|
|
587
|
+
reversed = (reversed << 1n) | (pattern & 1n);
|
|
588
|
+
pattern >>= 1n;
|
|
589
|
+
}
|
|
590
|
+
return result(reversed);
|
|
591
|
+
};
|
|
592
|
+
case "RotateLeft":
|
|
593
|
+
return (_host, args) => rotate(args, true);
|
|
594
|
+
case "RotateRight":
|
|
595
|
+
return (_host, args) => rotate(args, false);
|
|
596
|
+
default:
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/// Non-trapping integer arithmetic at the receiver's width: wrapping keeps
|
|
601
|
+
/// the low bits, saturating clamps to the width's bounds and checked answers
|
|
602
|
+
/// `None` where native's overflow flag is set.
|
|
603
|
+
function intArithShim(detail) {
|
|
604
|
+
const parsed = intDetail(detail);
|
|
605
|
+
if (!parsed) {
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
const [kind, width] = parsed;
|
|
609
|
+
const [min, max] = intBounds(width);
|
|
610
|
+
const operations = {
|
|
611
|
+
Add: (x, y) => x + y,
|
|
612
|
+
Sub: (x, y) => x - y,
|
|
613
|
+
Mul: (x, y) => x * y,
|
|
614
|
+
};
|
|
615
|
+
const mode = ["Wrapping", "Saturating", "Checked"].find((prefix) => kind.startsWith(prefix));
|
|
616
|
+
const apply = mode ? operations[kind.slice(mode.length)] : undefined;
|
|
617
|
+
if (!mode || !apply) {
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
const exact = (args) => apply(wrapTo(width, integer(args, 0)), wrapTo(width, integer(args, 1)));
|
|
621
|
+
switch (mode) {
|
|
622
|
+
case "Wrapping":
|
|
623
|
+
return (_host, args) => int(wrapTo(width, exact(args)));
|
|
624
|
+
case "Saturating":
|
|
625
|
+
return (_host, args) => {
|
|
626
|
+
const value = exact(args);
|
|
627
|
+
return int(value < min ? min : value > max ? max : value);
|
|
628
|
+
};
|
|
629
|
+
default:
|
|
630
|
+
return (host, args, shape) => {
|
|
631
|
+
if (!shape) {
|
|
632
|
+
throw new TypeError("checked arithmetic names no result shape to build its Option against");
|
|
633
|
+
}
|
|
634
|
+
const value = exact(args);
|
|
635
|
+
return value < min || value > max
|
|
636
|
+
? host.enumValue(shape, "OptionNone", [])
|
|
637
|
+
: host.enumValue(shape, "OptionSome", [int(value)]);
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function toBits(value) {
|
|
642
|
+
const view = new DataView(new ArrayBuffer(8));
|
|
643
|
+
view.setFloat64(0, value);
|
|
644
|
+
return view.getBigUint64(0);
|
|
645
|
+
}
|
|
382
646
|
function fromBits(bits) {
|
|
383
647
|
const view = new DataView(new ArrayBuffer(8));
|
|
384
648
|
view.setBigUint64(0, BigInt.asUintN(64, bits));
|
|
@@ -13,6 +13,7 @@ const UNSUPPORTED = "sandbox.capability.unsupported";
|
|
|
13
13
|
const NATIVE_FAMILIES = new Set(["AsyncIo", "FileRead", "Tcp"]);
|
|
14
14
|
const NATIVE_SUSPENSIONS = new Set(["NativeIo", "RemoteAsk", "Read", "Accept"]);
|
|
15
15
|
const OPS = new Set([
|
|
16
|
+
"coverage.hit",
|
|
16
17
|
"const.int",
|
|
17
18
|
"const.bool",
|
|
18
19
|
"const.float",
|
|
@@ -63,6 +64,7 @@ const OPS = new Set([
|
|
|
63
64
|
"task_scope.enter",
|
|
64
65
|
"task_scope.close",
|
|
65
66
|
"task.spawn",
|
|
67
|
+
"task.race",
|
|
66
68
|
]);
|
|
67
69
|
const TERMS = new Set([
|
|
68
70
|
"goto",
|
|
@@ -77,6 +79,7 @@ const TERMS = new Set([
|
|
|
77
79
|
"runtime.call",
|
|
78
80
|
"extern.call",
|
|
79
81
|
"value.call",
|
|
82
|
+
"wire.codec",
|
|
80
83
|
"panic",
|
|
81
84
|
"checked_raise",
|
|
82
85
|
"trap",
|
|
@@ -151,11 +154,34 @@ export function admitPackage(pkg) {
|
|
|
151
154
|
for (const block of function_.blocks) {
|
|
152
155
|
if (block.ops.some((op) => op.op === "actor.ingress_adapter"))
|
|
153
156
|
return unavailable("actor.ingress_adapter");
|
|
154
|
-
if (
|
|
155
|
-
|
|
157
|
+
if (block.term.op === "wire.codec" &&
|
|
158
|
+
(!Number.isSafeInteger(block.term.codec) ||
|
|
159
|
+
block.term.codec < 0 ||
|
|
160
|
+
typeof pkg.codecs?.[block.term.codec] !== "string" ||
|
|
161
|
+
![0, 1, 2, 3, 4].includes(block.term.format) ||
|
|
162
|
+
!["encode", "decode", "test_input"].includes(block.term.direction)))
|
|
163
|
+
return {
|
|
164
|
+
category: "invalid_package",
|
|
165
|
+
code: "sandbox.package.invalid_codec",
|
|
166
|
+
capability: "wire.codec",
|
|
167
|
+
message: "codec call lacks its checked shape or format",
|
|
168
|
+
span: null,
|
|
169
|
+
};
|
|
156
170
|
if (block.term.op === "actor.call" &&
|
|
157
171
|
!ACTOR_OPS.has(block.term.operation.op))
|
|
158
172
|
return unavailable(`actor.${block.term.operation.op}`);
|
|
173
|
+
if (block.ops.some((op) => op.op === "coverage.hit" &&
|
|
174
|
+
(!Number.isSafeInteger(op.counter) ||
|
|
175
|
+
op.counter < 0 ||
|
|
176
|
+
op.counter > 0xffff_ffff))) {
|
|
177
|
+
return {
|
|
178
|
+
category: "invalid_package",
|
|
179
|
+
code: "sandbox.package.invalid_counter",
|
|
180
|
+
capability: "coverage.hit",
|
|
181
|
+
message: "coverage counter must be a u32",
|
|
182
|
+
span: null,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
159
185
|
const unknown = block.ops.find((op) => !OPS.has(op.op))?.op ??
|
|
160
186
|
(!TERMS.has(block.term.op) ? block.term.op : null);
|
|
161
187
|
if (unknown)
|
|
@@ -208,11 +234,14 @@ export function admitPackage(pkg) {
|
|
|
208
234
|
}
|
|
209
235
|
for (const entry of pkg.externs) {
|
|
210
236
|
if (!resolveExternShim(entry.symbol)) {
|
|
237
|
+
const native = entry.native_capability;
|
|
211
238
|
return {
|
|
212
|
-
category: "not_implemented",
|
|
239
|
+
category: native === undefined ? "not_implemented" : "native_only",
|
|
213
240
|
code: UNSUPPORTED,
|
|
214
241
|
capability: entry.symbol,
|
|
215
|
-
message:
|
|
242
|
+
message: native === undefined
|
|
243
|
+
? "This external-library operation is not available in the browser runtime yet."
|
|
244
|
+
: `${native} require native execution.`,
|
|
216
245
|
span: null,
|
|
217
246
|
};
|
|
218
247
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UNIT, cloneValue } from "../values.js";
|
|
2
|
-
import { runtimeTag } from "./package.js";
|
|
2
|
+
import { runtimeTag, } from "./package.js";
|
|
3
3
|
import { ShimFault } from "./shims.js";
|
|
4
4
|
const OFFSET = 0xcbf29ce484222325n;
|
|
5
5
|
const PRIME = 0x100000001b3n;
|
|
@@ -20,6 +20,48 @@ function bytesHash(bytes) {
|
|
|
20
20
|
state = mix(state, BigInt(byte));
|
|
21
21
|
return state;
|
|
22
22
|
}
|
|
23
|
+
function orderingResult(capability, comparison) {
|
|
24
|
+
switch (capability) {
|
|
25
|
+
case "Less":
|
|
26
|
+
return boolean(comparison < 0);
|
|
27
|
+
case "LessEqual":
|
|
28
|
+
return boolean(comparison <= 0);
|
|
29
|
+
case "Greater":
|
|
30
|
+
return boolean(comparison > 0);
|
|
31
|
+
case "GreaterEqual":
|
|
32
|
+
return boolean(comparison >= 0);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function compareBytes(left, right) {
|
|
36
|
+
const length = Math.min(left.length, right.length);
|
|
37
|
+
for (let i = 0; i < length; i++) {
|
|
38
|
+
if (left[i] < right[i])
|
|
39
|
+
return -1;
|
|
40
|
+
if (left[i] > right[i])
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
return left.length < right.length ? -1 : left.length > right.length ? 1 : 0;
|
|
44
|
+
}
|
|
45
|
+
function scalarOrdering(left, right, ty, capability) {
|
|
46
|
+
if (left.kind === "f64") {
|
|
47
|
+
if (right?.kind !== "f64" ||
|
|
48
|
+
Number.isNaN(left.value) ||
|
|
49
|
+
Number.isNaN(right.value))
|
|
50
|
+
return boolean(false);
|
|
51
|
+
return orderingResult(capability, left.value < right.value ? -1 : left.value > right.value ? 1 : 0);
|
|
52
|
+
}
|
|
53
|
+
if (!right || right.kind !== left.kind)
|
|
54
|
+
return boolean(false);
|
|
55
|
+
const signed = /^(?:i(?:8|16|32|64)|isize|duration)$/.test(ty);
|
|
56
|
+
const width = Number(ty.match(/^[iu](8|16|32|64)$/)?.[1] ?? 64);
|
|
57
|
+
const lhs = left.kind === "i64" && signed
|
|
58
|
+
? BigInt.asIntN(width, left.value)
|
|
59
|
+
: bits(left, ty);
|
|
60
|
+
const rhs = right.kind === "i64" && signed
|
|
61
|
+
? BigInt.asIntN(width, right.value)
|
|
62
|
+
: bits(right, ty);
|
|
63
|
+
return orderingResult(capability, lhs < rhs ? -1 : lhs > rhs ? 1 : 0);
|
|
64
|
+
}
|
|
23
65
|
function bits(value, ty) {
|
|
24
66
|
switch (value.kind) {
|
|
25
67
|
case "unit":
|
|
@@ -55,22 +97,35 @@ export function* selectedValue(pkg, id, args) {
|
|
|
55
97
|
if (!left)
|
|
56
98
|
throw new Error("selected value operation has no receiver");
|
|
57
99
|
const hash = plan.capability === "Hash";
|
|
100
|
+
const equality = plan.capability === "Eq";
|
|
101
|
+
const ordering = plan.capability === "Less" ||
|
|
102
|
+
plan.capability === "LessEqual" ||
|
|
103
|
+
plan.capability === "Greater" ||
|
|
104
|
+
plan.capability === "GreaterEqual"
|
|
105
|
+
? plan.capability
|
|
106
|
+
: undefined;
|
|
58
107
|
if (plan.ty === "string") {
|
|
59
108
|
if (left.kind !== "string")
|
|
60
109
|
throw new Error("string capability has no string");
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
if (hash)
|
|
111
|
+
return integer(bytesHash(new TextEncoder().encode(left.value)));
|
|
112
|
+
if (right?.kind !== "string")
|
|
113
|
+
return boolean(false);
|
|
114
|
+
if (equality)
|
|
115
|
+
return boolean(left.value === right.value);
|
|
116
|
+
return orderingResult(ordering, compareBytes(new TextEncoder().encode(left.value), new TextEncoder().encode(right.value)));
|
|
64
117
|
}
|
|
65
118
|
if (plan.ty === "bytes") {
|
|
66
119
|
if (left.kind !== "vector")
|
|
67
120
|
throw new Error("bytes capability has no bytes");
|
|
68
121
|
const bytes = left.items.map((value) => Number(bits(value, "u8")));
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
122
|
+
if (hash)
|
|
123
|
+
return integer(bytesHash(bytes));
|
|
124
|
+
if (equality)
|
|
125
|
+
return boolean(right?.kind === "vector" &&
|
|
72
126
|
bytes.length === right.items.length &&
|
|
73
127
|
bytes.every((value, i) => BigInt(value) === bits(right.items[i], "u8")));
|
|
128
|
+
throw new Error("bytes ordering is outside the checked capability domain");
|
|
74
129
|
}
|
|
75
130
|
const components = plan.components ?? [];
|
|
76
131
|
let fields;
|
|
@@ -88,7 +143,11 @@ export function* selectedValue(pkg, id, args) {
|
|
|
88
143
|
else if (left.kind === "enum") {
|
|
89
144
|
if (hash)
|
|
90
145
|
throw new Error("derived enum Hash is outside the checked capability domain");
|
|
91
|
-
if (right?.kind !== "enum"
|
|
146
|
+
if (right?.kind !== "enum")
|
|
147
|
+
return boolean(false);
|
|
148
|
+
if (ordering && left.tag !== right.tag)
|
|
149
|
+
return orderingResult(ordering, left.tag < right.tag ? -1 : 1);
|
|
150
|
+
if (left.tag !== right.tag)
|
|
92
151
|
return boolean(false);
|
|
93
152
|
fields = left.payload;
|
|
94
153
|
others = right.payload;
|
|
@@ -104,13 +163,40 @@ export function* selectedValue(pkg, id, args) {
|
|
|
104
163
|
selections = fields.map(() => components[0]);
|
|
105
164
|
}
|
|
106
165
|
else {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
166
|
+
if (hash)
|
|
167
|
+
return integer(bits(left, plan.ty));
|
|
168
|
+
if (equality)
|
|
169
|
+
return boolean(!!right && bits(left, plan.ty) === bits(right, plan.ty));
|
|
170
|
+
return scalarOrdering(left, right, plan.ty, ordering);
|
|
110
171
|
}
|
|
111
172
|
let state = OFFSET;
|
|
112
173
|
for (const [at, field] of fields.entries()) {
|
|
113
|
-
const
|
|
174
|
+
const selection = selections[at];
|
|
175
|
+
if (!selection)
|
|
176
|
+
throw new Error("derived value operation lacks a component selection");
|
|
177
|
+
if (ordering) {
|
|
178
|
+
const selected = selection.ordering;
|
|
179
|
+
if (!selected)
|
|
180
|
+
throw new Error("derived ordering lacks exact component predicates");
|
|
181
|
+
const pair = [field, others[at]];
|
|
182
|
+
const less = yield* selectedValue(pkg, selected.less, pair);
|
|
183
|
+
if (less.kind !== "bool")
|
|
184
|
+
throw new Error("Less returned a non-boolean value");
|
|
185
|
+
if (less.value)
|
|
186
|
+
return boolean(ordering === "Less" || ordering === "LessEqual");
|
|
187
|
+
const greater = yield* selectedValue(pkg, selected.greater, pair);
|
|
188
|
+
if (greater.kind !== "bool")
|
|
189
|
+
throw new Error("Greater returned a non-boolean value");
|
|
190
|
+
if (greater.value)
|
|
191
|
+
return boolean(ordering === "Greater" || ordering === "GreaterEqual");
|
|
192
|
+
const comparable = yield* selectedValue(pkg, selected.less_equal, pair);
|
|
193
|
+
if (comparable.kind !== "bool")
|
|
194
|
+
throw new Error("LessEqual returned a non-boolean value");
|
|
195
|
+
if (!comparable.value)
|
|
196
|
+
return boolean(false);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const result = yield* selectedValue(pkg, selection.selected, hash ? [field] : [field, others[at]]);
|
|
114
200
|
if (hash) {
|
|
115
201
|
if (result.kind !== "i64")
|
|
116
202
|
throw new Error("Hash returned a non-integer value");
|
|
@@ -123,7 +209,11 @@ export function* selectedValue(pkg, id, args) {
|
|
|
123
209
|
return boolean(false);
|
|
124
210
|
}
|
|
125
211
|
}
|
|
126
|
-
|
|
212
|
+
if (hash)
|
|
213
|
+
return integer(state);
|
|
214
|
+
if (equality)
|
|
215
|
+
return boolean(true);
|
|
216
|
+
return boolean(ordering === "LessEqual" || ordering === "GreaterEqual");
|
|
127
217
|
}
|
|
128
218
|
function ordered(entries) {
|
|
129
219
|
return new Map([...entries].sort(([a], [b]) => Number(a) - Number(b)));
|
|
@@ -12,14 +12,32 @@ worker.addEventListener("message", (event) => {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
const { bytecode, options } = event.data;
|
|
15
|
-
const trace = runBytecode(bytecode,
|
|
16
|
-
|
|
15
|
+
const trace = runBytecode(bytecode, {
|
|
16
|
+
...options,
|
|
17
|
+
codecBridge: globalThis.__hewSandboxExecuteCodec,
|
|
18
|
+
});
|
|
19
|
+
worker.postMessage({
|
|
20
|
+
id,
|
|
21
|
+
ok: true,
|
|
22
|
+
trace,
|
|
23
|
+
playground: buildPlaygroundState(trace),
|
|
24
|
+
});
|
|
17
25
|
}
|
|
18
26
|
catch (error) {
|
|
19
27
|
if (error instanceof BytecodeValidationError) {
|
|
20
|
-
worker.postMessage({
|
|
28
|
+
worker.postMessage({
|
|
29
|
+
id,
|
|
30
|
+
ok: false,
|
|
31
|
+
error: "bytecode_validation",
|
|
32
|
+
diagnostics: error.errors,
|
|
33
|
+
});
|
|
21
34
|
return;
|
|
22
35
|
}
|
|
23
|
-
worker.postMessage({
|
|
36
|
+
worker.postMessage({
|
|
37
|
+
id,
|
|
38
|
+
ok: false,
|
|
39
|
+
error: "interpreter_error",
|
|
40
|
+
message: String(error),
|
|
41
|
+
});
|
|
24
42
|
}
|
|
25
43
|
});
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hew-lang/sandbox-vm",
|
|
3
|
-
"version": "0.6.0
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"hewSource": {
|
|
5
|
-
"commit": "
|
|
6
|
-
"tag": "v0.6.0
|
|
5
|
+
"commit": "c4b08fc6215201308794f6093aa67eb173eda1fd",
|
|
6
|
+
"tag": "v0.6.0",
|
|
7
7
|
"dirty": false
|
|
8
8
|
},
|
|
9
9
|
"description": "Deterministic TypeScript interpreter for the Hew educational sandbox",
|
|
@@ -17,8 +17,12 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist/"
|
|
20
|
+
"dist/",
|
|
21
|
+
"cli/"
|
|
21
22
|
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@hew-lang/wasm": "0.6.0"
|
|
25
|
+
},
|
|
22
26
|
"keywords": [
|
|
23
27
|
"hew",
|
|
24
28
|
"sandbox",
|