@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.
@@ -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 `NativeIo` operations the VM serves. A standard input line comes from
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 = Math.min(Math.max(index(args, 1), 0), items.length);
255
- const end = Math.min(Math.max(index(args, 2), start), items.length);
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 = Math.min(Math.max(index(args, 1), 0), items.length);
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 (String(block.term.op) === "wire.codec")
155
- return unavailable("wire.codec");
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: "This external-library operation is not available in the browser runtime yet.",
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
- return hash
62
- ? integer(bytesHash(new TextEncoder().encode(left.value)))
63
- : boolean(right?.kind === "string" && left.value === right.value);
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
- return hash
70
- ? integer(bytesHash(bytes))
71
- : boolean(right?.kind === "vector" &&
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" || left.tag !== right.tag)
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
- return hash
108
- ? integer(bits(left, plan.ty))
109
- : boolean(!!right && bits(left, plan.ty) === bits(right, plan.ty));
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 result = yield* selectedValue(pkg, selections[at], hash ? [field] : [field, others[at]]);
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
- return hash ? integer(state) : boolean(true);
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, options);
16
- worker.postMessage({ id, ok: true, trace, playground: buildPlaygroundState(trace) });
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({ id, ok: false, error: "bytecode_validation", diagnostics: error.errors });
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({ id, ok: false, error: "interpreter_error", message: String(error) });
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-rc7",
3
+ "version": "0.6.0",
4
4
  "hewSource": {
5
- "commit": "f09d3403587d1cf1bc33d00444bc76ae96861c17",
6
- "tag": "v0.6.0-rc7",
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",