@hew-lang/sandbox-vm 0.6.0-rc7 → 0.6.1

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.
@@ -1,6 +1,6 @@
1
1
  export type Own = "none" | "owned" | "guaranteed";
2
2
  export type BoundaryDecision = "borrow" | "borrow_mut" | "copy" | "move" | "snapshot";
3
- export type TrapName = "integer_overflow" | "divide_by_zero" | "shift_out_of_range" | "vector_bounds";
3
+ export type TrapName = "integer_overflow" | "divide_by_zero" | "shift_out_of_range" | "vector_bounds" | "actor_refused";
4
4
  export interface SpanV1 {
5
5
  start: number;
6
6
  end: number;
@@ -34,7 +34,7 @@ export interface VariantShape {
34
34
  cases: VariantCase[];
35
35
  runtime_tags?: Partial<Record<RuntimeVariantRole, number>>;
36
36
  }
37
- export type RuntimeVariantRole = "OptionSome" | "OptionNone" | "ResultOk" | "ResultErr" | "ActorErrorRejected" | "ActorErrorFailed" | "ActorErrorTrapped" | "ActorErrorDead" | "ActorErrorTimedOut" | "ActorErrorNodeNotRunning" | "ActorErrorRoutingFailed" | "ActorErrorEncodeFailed" | "ActorErrorConnectionDropped" | "ActorErrorPartition" | "SendErrorFull" | "SendErrorClosed" | "SendErrorPartition" | "SendErrorStaleRef" | "SendErrorLocalShutdown" | "SendErrorCancelled" | "SendErrorVersionMismatch" | "SendErrorUnauthorized" | "SendErrorBackpressure" | "SendErrorDead" | "DeliveryAccepted" | "DeliveryDiscarded" | "LinkErrorNoContext";
37
+ export type RuntimeVariantRole = "OptionSome" | "OptionNone" | "ResultOk" | "ResultErr" | "ActorErrorRejected" | "ActorErrorFailed" | "ActorErrorTrapped" | "ActorErrorRefused" | "CallerActor" | "CallerTask" | "CallerTimer" | "CallerNode" | "ActorErrorDead" | "ActorErrorTimedOut" | "ActorErrorNodeNotRunning" | "ActorErrorRoutingFailed" | "ActorErrorEncodeFailed" | "ActorErrorConnectionDropped" | "ActorErrorPartition" | "SendErrorFull" | "SendErrorClosed" | "SendErrorPartition" | "SendErrorStaleRef" | "SendErrorLocalShutdown" | "SendErrorCancelled" | "SendErrorVersionMismatch" | "SendErrorBackpressure" | "SendErrorDead" | "SendErrorWriteTimedOut" | "DeliveryAccepted" | "DeliveryDiscarded" | "LinkErrorNoContext";
38
38
  export declare function runtimeTag(shape: VariantShape, role: RuntimeVariantRole): number;
39
39
  export interface RuntimeFamilyEntry {
40
40
  id: number;
@@ -44,6 +44,7 @@ export interface RuntimeFamilyEntry {
44
44
  export interface ExternEntry {
45
45
  id: number;
46
46
  symbol: string;
47
+ native_capability?: string;
47
48
  }
48
49
  export type PlaceDecl = {
49
50
  id: number;
@@ -75,15 +76,27 @@ export type PlaceDecl = {
75
76
  };
76
77
  export interface ValueCapability {
77
78
  id: number;
78
- capability: "Eq" | "Hash";
79
+ capability: "Eq" | "Hash" | "Less" | "LessEqual" | "Greater" | "GreaterEqual";
79
80
  ty: string;
80
81
  callable?: number;
81
- components?: number[];
82
- variants?: number[][];
82
+ components?: ValueComponentCapabilities[];
83
+ variants?: ValueComponentCapabilities[][];
84
+ }
85
+ export interface ValueComponentCapabilities {
86
+ selected: number;
87
+ ordering?: {
88
+ less: number;
89
+ less_equal: number;
90
+ greater: number;
91
+ greater_equal: number;
92
+ };
83
93
  }
84
94
  export interface ClosureShape {
85
95
  id: number;
86
- body: number;
96
+ entries: Array<{
97
+ body: number;
98
+ specialization: number | null;
99
+ }>;
87
100
  fields: number;
88
101
  }
89
102
  export interface VtableShape {
@@ -93,6 +106,7 @@ export interface VtableShape {
93
106
  method: string;
94
107
  callee: number;
95
108
  receiver: BoundaryDecision;
109
+ receiver_handback?: boolean;
96
110
  }>;
97
111
  }
98
112
  export interface FunctionV1 {
@@ -115,6 +129,9 @@ interface OpBase {
115
129
  span?: SpanV1 | null;
116
130
  }
117
131
  export type OpV1 = (OpBase & {
132
+ op: "coverage.hit";
133
+ counter: number;
134
+ }) | (OpBase & {
118
135
  op: "const.int";
119
136
  dst: number;
120
137
  value: string;
@@ -312,6 +329,7 @@ export type OpV1 = (OpBase & {
312
329
  op: "callable.coerce";
313
330
  dst: number;
314
331
  source: Operand;
332
+ specialization: number | null;
315
333
  }) | (OpBase & {
316
334
  op: "dyn.make";
317
335
  dst: number;
@@ -341,6 +359,11 @@ export type OpV1 = (OpBase & {
341
359
  }) | (OpBase & {
342
360
  op: "task_scope.close";
343
361
  scope: number;
362
+ }) | (OpBase & {
363
+ op: "task.race";
364
+ dst: number;
365
+ scope: number;
366
+ members: Operand[];
344
367
  }) | (OpBase & {
345
368
  op: "task.spawn";
346
369
  dst: number;
@@ -407,6 +430,7 @@ export type TermV1 = (TermBase & {
407
430
  }) | (TermBase & CallShape & {
408
431
  op: "indirect.call";
409
432
  callee: BoundaryOperand;
433
+ specialization: number | null;
410
434
  }) | (TermBase & CallShape & {
411
435
  op: "dyn.call";
412
436
  receiver: BoundaryOperand;
@@ -414,12 +438,25 @@ export type TermV1 = (TermBase & {
414
438
  }) | (TermBase & CallShape & {
415
439
  op: "value.call";
416
440
  plan: number;
441
+ }) | (TermBase & CallShape & {
442
+ op: "wire.codec";
443
+ codec: number;
444
+ format: number;
445
+ direction: "encode" | "decode" | "test_input";
446
+ decoded: {
447
+ shape: number;
448
+ ok: number;
449
+ error: number;
450
+ error_root: number;
451
+ } | null;
452
+ callbacks: Record<string, number[]>;
417
453
  }) | (TermBase & CallShape & {
418
454
  op: "runtime.call";
419
455
  family: number;
420
456
  structural?: number | null;
421
457
  callbacks?: number[];
422
458
  releases_contents?: boolean;
459
+ keeps_inputs_on_failure?: boolean;
423
460
  result_member_shapes?: Array<number | null>;
424
461
  result_shape: number | null;
425
462
  }) | (TermBase & CallShape & {
@@ -436,10 +473,6 @@ export type TermV1 = (TermBase & {
436
473
  };
437
474
  result_shape: number | null;
438
475
  error_shape: number | null;
439
- }) | (TermBase & CallShape & {
440
- op: "wire.codec";
441
- direction: string;
442
- plan: number;
443
476
  }) | (TermBase & {
444
477
  op: "panic";
445
478
  message: BoundaryOperand;
@@ -498,7 +531,7 @@ export interface EntryPlan {
498
531
  export interface ActorProtocol {
499
532
  actor: number;
500
533
  message: number;
501
- policy: "reject" | "wait" | "drop_newest" | "replace_latest";
534
+ policy: "reject" | "wait" | "drop_newest";
502
535
  sealed: boolean;
503
536
  }
504
537
  export type ActorOperation = {
@@ -558,7 +591,6 @@ export interface ActorShape {
558
591
  keys: Array<{
559
592
  message: number;
560
593
  param: number;
561
- kind: string;
562
594
  }>;
563
595
  };
564
596
  max_heap_bytes?: number;
@@ -582,6 +614,7 @@ export interface SupervisorShape {
582
614
  }>;
583
615
  }
584
616
  export interface PackageV1 {
617
+ codecs?: string[];
585
618
  resources?: Array<{
586
619
  kind: string;
587
620
  ty?: string;
@@ -6,7 +6,7 @@
6
6
  /// reject list to keep in step with this one. `FileRead` is refused by its
7
7
  /// absence, and a `NativeIo` suspension by an operation missing from
8
8
  /// [`SUPPORTED_NATIVE_IO`].
9
- import { UNIT, cloneValue, renderStdout } from "../values.js";
9
+ import { UNIT, cloneValue, compareUtf8, renderStdout, } from "../values.js";
10
10
  import { seededMt } from "./mt19937.js";
11
11
  /// A language-visible fault raised inside a shim. The executor routes it to the
12
12
  /// call's `unwind` edge, or ends the program when the call names none.
@@ -29,6 +29,8 @@ export function resolveRuntimeShim(entry) {
29
29
  throw new Error("SupervisorPool requires its resumable executor");
30
30
  }
31
31
  : undefined;
32
+ case "ActorCaller":
33
+ case "ActorRefusalLog":
32
34
  case "StructuralFormat":
33
35
  return () => {
34
36
  throw new Error("StructuralFormat requires its resumable executor");
@@ -60,7 +62,11 @@ export function resolveRuntimeShim(entry) {
60
62
  ? () => {
61
63
  throw new Error("array mutation requires its resumable executor");
62
64
  }
63
- : undefined;
65
+ : entry.detail === "IndexBorrow"
66
+ ? (_host, args) => sequenceElement(args)
67
+ : ["Len", "Index"].includes(String(entry.detail))
68
+ ? VECTOR_SHIMS[detailName(entry.detail)]
69
+ : undefined;
64
70
  case "Map":
65
71
  case "Set":
66
72
  return [
@@ -85,6 +91,12 @@ export function resolveRuntimeShim(entry) {
85
91
  : undefined;
86
92
  case "MathIntrinsic":
87
93
  return MATH_SHIMS[detailName(entry.detail)];
94
+ case "FloatMethod":
95
+ return FLOAT_METHOD_SHIMS[detailName(entry.detail)];
96
+ case "IntMethod":
97
+ return intMethodShim(entry.detail);
98
+ case "IntArith":
99
+ return intArithShim(entry.detail);
88
100
  default:
89
101
  return UNIT_FAMILY_SHIMS[entry.family];
90
102
  }
@@ -105,10 +117,10 @@ export const SUPPORTED_SUSPEND_KINDS = new Set([
105
117
  "StreamSend",
106
118
  "StreamNext",
107
119
  ]);
108
- /// The `NativeIo` operations the VM serves. A standard input line comes from
109
- /// the replay stdin; every file and socket operation stays native.
120
+ /// The VM serves replay standard input and in-memory sink closure.
110
121
  export const SUPPORTED_NATIVE_IO = new Set([
111
122
  "StdinReadLine",
123
+ "SinkFinish",
112
124
  ]);
113
125
  // ── families ────────────────────────────────────────────────────────────────
114
126
  /// `Print { kind, newline }`. The rendering follows the value's own
@@ -149,10 +161,6 @@ const UNIT_FAMILY_SHIMS = {
149
161
  host.closePipe(arg(args, 0));
150
162
  return UNIT;
151
163
  },
152
- SinkFinish: (host, args) => {
153
- host.pipes.close(arg(args, 0));
154
- return UNIT;
155
- },
156
164
  StreamClose: (host, args) => {
157
165
  host.closePipe(arg(args, 0));
158
166
  return UNIT;
@@ -160,9 +168,20 @@ const UNIT_FAMILY_SHIMS = {
160
168
  StringConcat: (_host, args) => str(`${text(args, 0)}${text(args, 1)}`),
161
169
  StringClone: (_host, args) => str(text(args, 0)),
162
170
  StringEquals: (_host, args) => bool(text(args, 0) === text(args, 1)),
171
+ StringCompare: (_host, args) => int(BigInt(compareUtf8(text(args, 0), text(args, 1)))),
163
172
  StringContains: (_host, args) => bool(text(args, 0).includes(text(args, 1))),
173
+ StringTrim: (_host, args) => str(text(args, 0).replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "")),
164
174
  // Native string length counts codepoints, not bytes.
165
175
  StringLen: (_host, args) => int(BigInt([...text(args, 0)].length)),
176
+ StringIndex: (_host, args) => {
177
+ const chars = [...text(args, 0)];
178
+ const at = index(args, 1);
179
+ if (at < 0 || at >= chars.length) {
180
+ throw new ShimFault("vector_bounds", `string index ${at} out of bounds for length ${chars.length}`);
181
+ }
182
+ return str(chars[at]);
183
+ },
184
+ StringIsEmpty: (_host, args) => bool(text(args, 0).length === 0),
166
185
  StringReplace: (_host, args) => str(text(args, 0).split(text(args, 1)).join(text(args, 2))),
167
186
  // Native slicing is by codepoint and clamps rather than trapping.
168
187
  StringSlice: (_host, args) => {
@@ -183,8 +202,63 @@ const UNIT_FAMILY_SHIMS = {
183
202
  // TextDecoder replaces each maximal invalid subsequence with U+FFFD, as
184
203
  // native lossy decoding does.
185
204
  BytesDecodeUtf8Lossy: (_host, args) => str(decodeUtf8(arg(args, 0))),
205
+ BytesNew: () => ({ kind: "vector", elementType: "u8", items: [] }),
186
206
  BytesIsEmpty: (_host, args) => bool(vec(args, 0).items.length === 0),
187
207
  BytesLen: (_host, args) => int(BigInt(vec(args, 0).items.length)),
208
+ BytesContains: (_host, args) => {
209
+ const byte = integer(args, 1);
210
+ return bool(vec(args, 0).items.some((item) => integer([item], 0) === byte));
211
+ },
212
+ BytesIndex: (_host, args) => {
213
+ const items = vec(args, 0).items;
214
+ const at = index(args, 1);
215
+ if (at < 0 || at >= items.length) {
216
+ throw new ShimFault("vector_bounds", `bytes index ${at} out of bounds for length ${items.length}`);
217
+ }
218
+ return cloneValue(items[at]);
219
+ },
220
+ // A native bytes range shares the buffer; a VM value is never aliased, so
221
+ // a copy is observably the same.
222
+ BytesSlice: (_host, args) => {
223
+ const items = vec(args, 0).items;
224
+ const start = index(args, 1);
225
+ const end = index(args, 2);
226
+ checkSliceBounds(start, end, items.length);
227
+ return byteVector(items.slice(start, end));
228
+ },
229
+ BytesSliceFrom: (_host, args) => {
230
+ const items = vec(args, 0).items;
231
+ const start = index(args, 1);
232
+ checkSliceBounds(start, items.length, items.length);
233
+ return byteVector(items.slice(start));
234
+ },
235
+ BytesConcat: (_host, args) => byteVector([...vec(args, 0).items, ...vec(args, 1).items]),
236
+ BytesStartsWith: (_host, args) => bool(runAt(vec(args, 0).items, vec(args, 1).items, 0)),
237
+ BytesEndsWith: (_host, args) => {
238
+ const items = vec(args, 0).items;
239
+ const run = vec(args, 1).items;
240
+ return bool(runAt(items, run, items.length - run.length));
241
+ },
242
+ // A mutating call receives its receiver by `move` and returns it.
243
+ BytesPush: (_host, args) => {
244
+ const target = vec(args, 0);
245
+ target.items.push(cloneValue(arg(args, 1)));
246
+ return target;
247
+ },
248
+ BytesSet: (_host, args) => {
249
+ const target = vec(args, 0);
250
+ const at = index(args, 1);
251
+ if (at < 0 || at >= target.items.length) {
252
+ throw new ShimFault("vector_bounds", `bytes index ${at} out of bounds for length ${target.items.length}`);
253
+ }
254
+ target.items[at] = cloneValue(arg(args, 2));
255
+ return target;
256
+ },
257
+ BytesClear: (_host, args) => {
258
+ const target = vec(args, 0);
259
+ target.items.length = 0;
260
+ return target;
261
+ },
188
262
  BytesGet: (host, args, shape) => {
189
263
  if (!shape) {
190
264
  throw new TypeError("BytesGet names no result shape to build its Option against");
@@ -230,6 +304,15 @@ const VECTOR_SHIMS = {
230
304
  target.items.push(arg(args, 1));
231
305
  return target;
232
306
  },
307
+ Remove: (_host, args) => {
308
+ const target = vec(args, 0);
309
+ const at = index(args, 1);
310
+ if (at < 0 || at >= target.items.length) {
311
+ throw new ShimFault("vector_bounds", `vector index ${at} out of bounds for length ${target.items.length}`);
312
+ }
313
+ const [item] = target.items.splice(at, 1);
314
+ return { kind: "record", typeId: "", fields: [target, item] };
315
+ },
233
316
  Get: (host, args, shape) => {
234
317
  if (!shape) {
235
318
  throw new TypeError("Vector::Get names no result shape to build its Option against");
@@ -240,19 +323,14 @@ const VECTOR_SHIMS = {
240
323
  ? host.enumValue(shape, "OptionSome", [cloneValue(items[at])])
241
324
  : host.enumValue(shape, "OptionNone", []);
242
325
  },
243
- Index: (_host, args) => {
244
- const items = vec(args, 0).items;
245
- const at = index(args, 1);
246
- if (at < 0 || at >= items.length) {
247
- throw new ShimFault("vector_bounds", `vector index ${at} out of bounds for length ${items.length}`);
248
- }
249
- return cloneValue(items[at]);
250
- },
326
+ Index: (_host, args) => cloneValue(sequenceElement(args)),
327
+ IndexBorrow: (_host, args) => sequenceElement(args),
251
328
  Len: (_host, args) => int(BigInt(vec(args, 0).items.length)),
252
329
  Slice: (_host, args) => {
253
330
  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);
331
+ const start = index(args, 1);
332
+ const end = index(args, 2);
333
+ checkSliceBounds(start, end, items.length);
256
334
  return {
257
335
  kind: "vector",
258
336
  elementType: "",
@@ -261,7 +339,8 @@ const VECTOR_SHIMS = {
261
339
  },
262
340
  SliceFrom: (_host, args) => {
263
341
  const items = vec(args, 0).items;
264
- const start = Math.min(Math.max(index(args, 1), 0), items.length);
342
+ const start = index(args, 1);
343
+ checkSliceBounds(start, items.length, items.length);
265
344
  return {
266
345
  kind: "vector",
267
346
  elementType: "",
@@ -269,6 +348,30 @@ const VECTOR_SHIMS = {
269
348
  };
270
349
  },
271
350
  };
351
+ function sequenceElement(args) {
352
+ const items = vec(args, 0).items;
353
+ const at = index(args, 1);
354
+ if (at < 0 || at >= items.length) {
355
+ throw new ShimFault("vector_bounds", `vector index ${at} out of bounds for length ${items.length}`);
356
+ }
357
+ return items[at];
358
+ }
359
+ function byteVector(items) {
360
+ return { kind: "vector", elementType: "u8", items: items.map(cloneValue) };
361
+ }
362
+ /// Whether `run` occurs in `items` starting at `at`.
363
+ function runAt(items, run, at) {
364
+ return (at >= 0 &&
365
+ at + run.length <= items.length &&
366
+ run.every((byte, offset) => integer([items[at + offset]], 0) === integer([byte], 0)));
367
+ }
368
+ /// A slice outside `0 <= start <= end <= length` faults like native's
369
+ /// `IndexOutOfBounds` rather than clamping to the valid range.
370
+ function checkSliceBounds(start, end, length) {
371
+ if (start < 0 || end < start || end > length) {
372
+ throw new ShimFault("vector_bounds", `slice ${start}..${end} out of bounds for length ${length}`);
373
+ }
374
+ }
272
375
  /// `std.math`. Native emits the LLVM intrinsic for each of these, so the
273
376
  /// mapping is to the matching IEEE operation rather than to whatever JS spells
274
377
  /// similarly: `Round` is half away from zero, `MinF64`/`MaxF64` follow
@@ -379,6 +482,176 @@ function fma(a, b, c) {
379
482
  const sumTail = product - (sum - bVirtual) + (c - bVirtual);
380
483
  return sum + (sumTail + tail);
381
484
  }
485
+ /// `f64` classification and bit methods. Native reads the IEEE bits
486
+ /// directly, so `IsSignNegative` tests the sign bit (true for `-0.0` and a
487
+ /// negative NaN) rather than comparing against zero.
488
+ const FLOAT_METHOD_SHIMS = {
489
+ ToBits: (_host, args) => int(toBits(real(args, 0))),
490
+ IsNan: (_host, args) => bool(Number.isNaN(real(args, 0))),
491
+ IsFinite: (_host, args) => bool(Number.isFinite(real(args, 0))),
492
+ IsInfinite: (_host, args) => {
493
+ const value = real(args, 0);
494
+ return bool(value === Infinity || value === -Infinity);
495
+ },
496
+ IsSignNegative: (_host, args) => bool(toBits(real(args, 0)) >> 63n === 1n),
497
+ };
498
+ /// The receiver width an `IntMethod`/`IntArith` detail names. The VM is a
499
+ /// 64-bit target, so the pointer-sized widths are 64 bits wide.
500
+ const INT_WIDTHS = {
501
+ I8: { bits: 8, signed: true },
502
+ I16: { bits: 16, signed: true },
503
+ I32: { bits: 32, signed: true },
504
+ I64: { bits: 64, signed: true },
505
+ Isize: { bits: 64, signed: true },
506
+ U8: { bits: 8, signed: false },
507
+ U16: { bits: 16, signed: false },
508
+ U32: { bits: 32, signed: false },
509
+ U64: { bits: 64, signed: false },
510
+ Usize: { bits: 64, signed: false },
511
+ };
512
+ /// `[op, width]`, as `RuntimeCallFamily` serializes a two-field variant.
513
+ function intDetail(detail) {
514
+ if (!Array.isArray(detail) || detail.length !== 2) {
515
+ return undefined;
516
+ }
517
+ const [op, width] = detail;
518
+ const shape = typeof width === "string" ? INT_WIDTHS[width] : undefined;
519
+ return typeof op === "string" && shape ? [op, shape] : undefined;
520
+ }
521
+ function wrapTo(width, value) {
522
+ return width.signed
523
+ ? BigInt.asIntN(width.bits, value)
524
+ : BigInt.asUintN(width.bits, value);
525
+ }
526
+ function intBounds(width) {
527
+ const bits = BigInt(width.bits);
528
+ return width.signed
529
+ ? [-(1n << (bits - 1n)), (1n << (bits - 1n)) - 1n]
530
+ : [0n, (1n << bits) - 1n];
531
+ }
532
+ function popcount(value) {
533
+ let count = 0n;
534
+ for (let rest = value; rest !== 0n; rest >>= 1n) {
535
+ count += rest & 1n;
536
+ }
537
+ return count;
538
+ }
539
+ /// Integer bit methods. Native emits `ctpop`/`ctlz`/`cttz`/`bswap`/
540
+ /// `bitreverse`/`fshl`/`fshr` at the receiver's width, so every operation
541
+ /// works on the receiver's unsigned bit pattern and the rotate amount is taken
542
+ /// modulo the width.
543
+ function intMethodShim(detail) {
544
+ const parsed = intDetail(detail);
545
+ if (!parsed) {
546
+ return undefined;
547
+ }
548
+ const [op, width] = parsed;
549
+ const bits = BigInt(width.bits);
550
+ const unsigned = (args) => BigInt.asUintN(width.bits, integer(args, 0));
551
+ const result = (pattern) => int(wrapTo(width, pattern));
552
+ const rotate = (args, left) => {
553
+ const pattern = unsigned(args);
554
+ const amount = BigInt.asUintN(32, integer(args, 1)) % bits;
555
+ const shift = left ? amount : (bits - amount) % bits;
556
+ return result((pattern << shift) | (pattern >> ((bits - shift) % bits)));
557
+ };
558
+ switch (op) {
559
+ case "CountOnes":
560
+ return (_host, args) => int(popcount(unsigned(args)));
561
+ case "CountZeros":
562
+ return (_host, args) => int(bits - popcount(unsigned(args)));
563
+ case "LeadingZeros":
564
+ return (_host, args) => {
565
+ const pattern = unsigned(args);
566
+ return int(pattern === 0n ? bits : bits - BigInt(pattern.toString(2).length));
567
+ };
568
+ case "TrailingZeros":
569
+ return (_host, args) => {
570
+ let pattern = unsigned(args);
571
+ if (pattern === 0n) {
572
+ return int(bits);
573
+ }
574
+ let count = 0n;
575
+ while ((pattern & 1n) === 0n) {
576
+ pattern >>= 1n;
577
+ count += 1n;
578
+ }
579
+ return int(count);
580
+ };
581
+ case "SwapBytes":
582
+ return (_host, args) => {
583
+ let pattern = unsigned(args);
584
+ let swapped = 0n;
585
+ for (let byte = 0; byte < width.bits / 8; byte += 1) {
586
+ swapped = (swapped << 8n) | (pattern & 0xffn);
587
+ pattern >>= 8n;
588
+ }
589
+ return result(swapped);
590
+ };
591
+ case "ReverseBits":
592
+ return (_host, args) => {
593
+ let pattern = unsigned(args);
594
+ let reversed = 0n;
595
+ for (let bit = 0; bit < width.bits; bit += 1) {
596
+ reversed = (reversed << 1n) | (pattern & 1n);
597
+ pattern >>= 1n;
598
+ }
599
+ return result(reversed);
600
+ };
601
+ case "RotateLeft":
602
+ return (_host, args) => rotate(args, true);
603
+ case "RotateRight":
604
+ return (_host, args) => rotate(args, false);
605
+ default:
606
+ return undefined;
607
+ }
608
+ }
609
+ /// Non-trapping integer arithmetic at the receiver's width: wrapping keeps
610
+ /// the low bits, saturating clamps to the width's bounds and checked answers
611
+ /// `None` where native's overflow flag is set.
612
+ function intArithShim(detail) {
613
+ const parsed = intDetail(detail);
614
+ if (!parsed) {
615
+ return undefined;
616
+ }
617
+ const [kind, width] = parsed;
618
+ const [min, max] = intBounds(width);
619
+ const operations = {
620
+ Add: (x, y) => x + y,
621
+ Sub: (x, y) => x - y,
622
+ Mul: (x, y) => x * y,
623
+ };
624
+ const mode = ["Wrapping", "Saturating", "Checked"].find((prefix) => kind.startsWith(prefix));
625
+ const apply = mode ? operations[kind.slice(mode.length)] : undefined;
626
+ if (!mode || !apply) {
627
+ return undefined;
628
+ }
629
+ const exact = (args) => apply(wrapTo(width, integer(args, 0)), wrapTo(width, integer(args, 1)));
630
+ switch (mode) {
631
+ case "Wrapping":
632
+ return (_host, args) => int(wrapTo(width, exact(args)));
633
+ case "Saturating":
634
+ return (_host, args) => {
635
+ const value = exact(args);
636
+ return int(value < min ? min : value > max ? max : value);
637
+ };
638
+ default:
639
+ return (host, args, shape) => {
640
+ if (!shape) {
641
+ throw new TypeError("checked arithmetic names no result shape to build its Option against");
642
+ }
643
+ const value = exact(args);
644
+ return value < min || value > max
645
+ ? host.enumValue(shape, "OptionNone", [])
646
+ : host.enumValue(shape, "OptionSome", [int(value)]);
647
+ };
648
+ }
649
+ }
650
+ function toBits(value) {
651
+ const view = new DataView(new ArrayBuffer(8));
652
+ view.setFloat64(0, value);
653
+ return view.getBigUint64(0);
654
+ }
382
655
  function fromBits(bits) {
383
656
  const view = new DataView(new ArrayBuffer(8));
384
657
  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
  }