@hew-lang/sandbox-vm 0.6.0 → 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.
@@ -59,6 +59,7 @@ var SandboxExitCode;
59
59
  SandboxExitCode[SandboxExitCode["ExhaustivenessFallthrough"] = 208] = "ExhaustivenessFallthrough";
60
60
  })(SandboxExitCode || (SandboxExitCode = {}));
61
61
  const TRAP_EXIT_CODES = {
62
+ actor_refused: SandboxExitCode.InternalError,
62
63
  integer_overflow: SandboxExitCode.IntegerOverflow,
63
64
  shift_out_of_range: SandboxExitCode.ShiftOutOfRange,
64
65
  divide_by_zero: SandboxExitCode.DivideByZero,
@@ -2,7 +2,7 @@ export type JsonValue = null | boolean | number | string | JsonValue[] | {
2
2
  [key: string]: JsonValue;
3
3
  };
4
4
  export type SandboxBytecodePackage = import("./v1/package.js").PackageV1;
5
- export type TrapKind = "integer_overflow" | "shift_out_of_range" | "divide_by_zero" | "invalid_local" | "invalid_block" | "invalid_call" | "invalid_enum_tag" | "invalid_record_field" | "vector_bounds" | "string_bounds" | "regex_compile" | "capability_missing" | "budget_exhausted" | "panic" | "unsupported_instruction" | "internal_error";
5
+ export type TrapKind = "actor_refused" | "integer_overflow" | "shift_out_of_range" | "divide_by_zero" | "invalid_local" | "invalid_block" | "invalid_call" | "invalid_enum_tag" | "invalid_record_field" | "vector_bounds" | "string_bounds" | "regex_compile" | "capability_missing" | "budget_exhausted" | "panic" | "unsupported_instruction" | "internal_error";
6
6
  export type RuntimeStatus = "ok" | "compile_error" | "sandbox_rejected" | "runtime_failure" | "budget_exhausted" | "panic" | "trap";
7
7
  export interface RuntimeFailure {
8
8
  kind: "panic" | "trap" | "budget_exhausted" | "unsupported" | "internal_error";
@@ -98,6 +98,7 @@ class ExecutorV1 {
98
98
  generators = new Map();
99
99
  tasks = new Map();
100
100
  completedTasks = 0;
101
+ actorIdentity = 0n;
101
102
  faultDebts = new Set();
102
103
  nextFaultDebt = 0;
103
104
  closingResources = new WeakSet();
@@ -550,7 +551,8 @@ class ExecutorV1 {
550
551
  const shape = this.closureAt(op.closure);
551
552
  this.define(act, op.dst, {
552
553
  kind: "closure",
553
- body: shape.body,
554
+ entries: shape.entries,
555
+ specialization: null,
554
556
  environment: {
555
557
  kind: "record",
556
558
  typeId: "",
@@ -559,12 +561,23 @@ class ExecutorV1 {
559
561
  });
560
562
  return;
561
563
  }
562
- case "callable.coerce":
563
- // A coercion only weakens the callable's proved capabilities; SIR
564
- // refuses one that changes an argument, a result or a capture shape,
565
- // so the value itself is unchanged.
566
- this.define(act, op.dst, this.read(act, op.source));
564
+ case "callable.coerce": {
565
+ const callable = this.read(act, op.source);
566
+ if (op.specialization === null) {
567
+ this.define(act, op.dst, callable);
568
+ }
569
+ else {
570
+ if (callable.kind !== "closure" ||
571
+ !callable.entries.some((entry) => entry.specialization === op.specialization)) {
572
+ throw new Error("callable specialization has no checked invocation entry");
573
+ }
574
+ this.define(act, op.dst, {
575
+ ...callable,
576
+ specialization: op.specialization,
577
+ });
578
+ }
567
579
  return;
580
+ }
568
581
  case "dyn.make":
569
582
  this.define(act, op.dst, {
570
583
  kind: "dyn",
@@ -595,6 +608,9 @@ class ExecutorV1 {
595
608
  const duration = this.read(act, op.duration);
596
609
  if (duration.kind !== "i64")
597
610
  throw new Error("scope duration is not a duration");
611
+ group.expiresAt =
612
+ this.trace.virtualTimeMs +
613
+ Math.max(0, Number(duration.value) / 1_000_000);
598
614
  group.cancelTimer = this.scheduler.after(duration.value, () => {
599
615
  group.deadline = {
600
616
  kind: "panic",
@@ -636,6 +652,7 @@ class ExecutorV1 {
636
652
  const context = {
637
653
  id,
638
654
  actor: act.context.actor,
655
+ requestDeadline: this.requestDeadline(act),
639
656
  returned: (value) => this.closeValueAsync(value, null, (fault) => this.finishGenerator(generator, fault)),
640
657
  failed: (fault) => this.finishGenerator(generator, fault),
641
658
  };
@@ -791,6 +808,7 @@ class ExecutorV1 {
791
808
  const context = {
792
809
  id: task.id,
793
810
  actor: act.context.actor,
811
+ requestDeadline: this.requestDeadline(act),
794
812
  returned: (value) => settle(value, null),
795
813
  failed: (fault) => settle(UNIT, fault),
796
814
  };
@@ -920,7 +938,7 @@ class ExecutorV1 {
920
938
  // `self`. A plain function value carries none.
921
939
  const callee = this.boundary(act, term.callee);
922
940
  const args = term.args.map((operand) => this.boundary(act, operand));
923
- this.current = this.activate(this.functionAt(calleeFunction(callee)), callee.kind === "closure" ? [callee.environment, ...args] : args, act, term.result, term.normal, term.unwind);
941
+ this.current = this.activate(this.functionAt(calleeFunction(callee, term.specialization)), callee.kind === "closure" ? [callee.environment, ...args] : args, act, term.result, term.normal, term.unwind);
924
942
  return;
925
943
  }
926
944
  case "actor.call":
@@ -928,6 +946,38 @@ class ExecutorV1 {
928
946
  return;
929
947
  case "runtime.call": {
930
948
  const entry = this.pkg.runtime_families[term.family];
949
+ if (entry?.family === "ActorCaller") {
950
+ const caller = act.context.caller ?? { kind: "Task" };
951
+ const payload = [];
952
+ if (caller.kind === "Actor") {
953
+ const shape = this.pkg.variants[term.result_shape];
954
+ const tag = runtimeTag(shape, "CallerActor");
955
+ payload.push({
956
+ kind: "record",
957
+ typeId: shape.cases[tag].fields[0],
958
+ fields: [{ kind: "i64", value: caller.identity }],
959
+ });
960
+ }
961
+ this.completeShim(act, term, this.variant(term.result_shape, caller.kind === "Actor"
962
+ ? "CallerActor"
963
+ : caller.kind === "Timer"
964
+ ? "CallerTimer"
965
+ : "CallerTask", payload));
966
+ return;
967
+ }
968
+ if (entry?.family === "ActorRefusalLog") {
969
+ const [message, handler, reason] = term.args.map((argument) => this.boundary(act, argument));
970
+ this.trace.snapshot("actor.delivery_refused", {
971
+ actor_id: act.context.actor?.id ?? null,
972
+ handler: toJsonValue(handler),
973
+ message: toJsonValue(message),
974
+ reason: toJsonValue(reason),
975
+ });
976
+ if (!act.context.reply)
977
+ this.trace.writeStderr(`WARN actor.delivery_refused actor=${act.context.actor?.identity ?? 0n} handler=${asString(handler)} reason=${JSON.stringify(asString(reason))}\n`, null);
978
+ this.completeShim(act, term, UNIT);
979
+ return;
980
+ }
931
981
  if (entry &&
932
982
  (entry.family === "Map" ||
933
983
  entry.family === "Set" ||
@@ -1866,6 +1916,9 @@ class ExecutorV1 {
1866
1916
  actorCall(act, term) {
1867
1917
  const args = term.args.map((arg) => this.boundary(act, arg));
1868
1918
  const operation = term.operation;
1919
+ const caller = act.context.actor
1920
+ ? { kind: "Actor", identity: act.context.actor.identity }
1921
+ : { kind: "Task" };
1869
1922
  switch (operation.op) {
1870
1923
  case "spawn": {
1871
1924
  const layout = this.pkg.actors?.find((actor) => actor.id === operation.actor);
@@ -1873,6 +1926,7 @@ class ExecutorV1 {
1873
1926
  throw new Error(`actor layout ${operation.actor} is missing`);
1874
1927
  let supplied = 0;
1875
1928
  const actor = {
1929
+ identity: ++this.actorIdentity,
1876
1930
  id: this.ids.actor(),
1877
1931
  layout,
1878
1932
  state: {
@@ -1956,6 +2010,7 @@ class ExecutorV1 {
1956
2010
  if (!handler)
1957
2011
  throw new Error("stream request has no producer handler");
1958
2012
  actor.mailbox.push({
2013
+ caller,
1959
2014
  handler,
1960
2015
  payload: fields,
1961
2016
  reply: false,
@@ -2076,6 +2131,7 @@ class ExecutorV1 {
2076
2131
  const superseded = actor.mailbox[queued];
2077
2132
  actor.mailbox[queued] = {
2078
2133
  ...superseded,
2134
+ caller,
2079
2135
  payload: payload.fields,
2080
2136
  };
2081
2137
  discardAtReceiver({ kind: "record", typeId: "", fields: superseded.payload }, accepted(false));
@@ -2095,6 +2151,7 @@ class ExecutorV1 {
2095
2151
  if (oldest >= 0) {
2096
2152
  const [evicted] = actor.mailbox.splice(oldest, 1);
2097
2153
  actor.mailbox.push({
2154
+ caller,
2098
2155
  handler,
2099
2156
  payload: payload.fields,
2100
2157
  reply: false,
@@ -2124,6 +2181,7 @@ class ExecutorV1 {
2124
2181
  return;
2125
2182
  }
2126
2183
  actor.mailbox.push({
2184
+ caller,
2127
2185
  handler,
2128
2186
  payload: payload.fields,
2129
2187
  reply: false,
@@ -2312,15 +2370,32 @@ class ExecutorV1 {
2312
2370
  throw new Error(`actor operation ${operation.op} has no executor`);
2313
2371
  }
2314
2372
  }
2373
+ requestDeadline(act) {
2374
+ let deadline = act.context.requestDeadline;
2375
+ for (let frame = act; frame; frame = frame.caller) {
2376
+ for (const group of frame.scopes.values()) {
2377
+ if (group.expiresAt !== undefined)
2378
+ deadline = Math.min(deadline ?? Infinity, group.expiresAt);
2379
+ }
2380
+ }
2381
+ return deadline;
2382
+ }
2315
2383
  ask(protocol, inputs, complete, shapes) {
2316
2384
  let settled = false;
2317
2385
  let admitted = false;
2318
2386
  let closing = false;
2319
2387
  const owner = this.current.context.actor;
2388
+ const deadline = this.requestDeadline(this.current);
2389
+ let cancelTimer;
2390
+ let message;
2391
+ const caller = owner
2392
+ ? { kind: "Actor", identity: owner.identity }
2393
+ : { kind: "Task" };
2320
2394
  let receiver = null;
2321
2395
  const finish = (value, error, drained) => {
2322
2396
  const alreadySettled = settled;
2323
2397
  settled = true;
2398
+ cancelTimer?.();
2324
2399
  if (alreadySettled || complete(value, error) === false) {
2325
2400
  // The finishing receiver still owns a reply that its caller no
2326
2401
  // longer accepts. Its next turn must wait for this release.
@@ -2364,12 +2439,17 @@ class ExecutorV1 {
2364
2439
  if (settled)
2365
2440
  return;
2366
2441
  settled = true;
2442
+ cancelTimer?.();
2367
2443
  complete(null, reason, fault);
2368
2444
  });
2369
2445
  };
2370
2446
  const attempt = () => {
2371
2447
  if (settled || closing)
2372
2448
  return;
2449
+ if (deadline !== undefined && deadline <= this.trace.virtualTimeMs) {
2450
+ reject("TimedOut");
2451
+ return;
2452
+ }
2373
2453
  const role = "id" in target ? this.roles.get(target.id) : undefined;
2374
2454
  if (role && this.rolePending(role)) {
2375
2455
  role.waiting.push(attempt);
@@ -2422,23 +2502,50 @@ class ExecutorV1 {
2422
2502
  }
2423
2503
  admitted = true;
2424
2504
  receiver = actor;
2425
- actor.mailbox.push({
2505
+ message = {
2506
+ caller,
2426
2507
  handler,
2427
2508
  payload,
2428
2509
  complete: finish,
2429
2510
  reply: true,
2430
2511
  obligated: true,
2431
- });
2512
+ requestDeadline: deadline,
2513
+ };
2514
+ actor.mailbox.push(message);
2432
2515
  this.trace.snapshot("actor.ask", {
2433
2516
  actor_id: actor.id,
2434
2517
  handler: handler.name,
2435
2518
  });
2436
2519
  this.dispatchActor(actor);
2437
2520
  };
2521
+ const withdrawQueued = () => {
2522
+ if (!message || !receiver)
2523
+ return;
2524
+ const index = receiver.mailbox.indexOf(message);
2525
+ if (index < 0)
2526
+ return;
2527
+ receiver.mailbox.splice(index, 1);
2528
+ admitted = false;
2529
+ for (const admit of receiver.admission.splice(0))
2530
+ admit();
2531
+ };
2532
+ if (deadline !== undefined) {
2533
+ cancelTimer = this.scheduler.after(BigInt(Math.max(0, Math.round((deadline - this.trace.virtualTimeMs) * 1_000_000))), () => {
2534
+ if (settled || closing)
2535
+ return;
2536
+ withdrawQueued();
2537
+ if (admitted)
2538
+ finish(null, "TimedOut");
2539
+ else
2540
+ reject("TimedOut");
2541
+ });
2542
+ }
2438
2543
  attempt();
2439
2544
  return (done) => {
2440
2545
  const wasSettled = settled;
2441
2546
  settled = true;
2547
+ cancelTimer?.();
2548
+ withdrawQueued();
2442
2549
  if (admitted || (wasSettled && !closing))
2443
2550
  done(null);
2444
2551
  else
@@ -2489,6 +2596,18 @@ class ExecutorV1 {
2489
2596
  }
2490
2597
  for (const admit of actor.admission.splice(0))
2491
2598
  admit();
2599
+ if (message.requestDeadline !== undefined &&
2600
+ message.requestDeadline <= this.trace.virtualTimeMs) {
2601
+ this.closeValueAsync({ kind: "record", typeId: "", fields: message.payload }, null, (fault) => {
2602
+ message.complete(null, "TimedOut");
2603
+ actor.busy = false;
2604
+ if (fault)
2605
+ this.crashActor(actor, fault);
2606
+ else
2607
+ this.dispatchActor(actor);
2608
+ }, actor);
2609
+ return;
2610
+ }
2492
2611
  this.trace.snapshot("actor.receive", {
2493
2612
  actor_id: actor.id,
2494
2613
  handler: message.handler.name,
@@ -2496,6 +2615,9 @@ class ExecutorV1 {
2496
2615
  const frame = this.activate(this.functionAt(message.handler.callable), [actor.state, ...message.payload], null, null, undefined, null, {
2497
2616
  id: actor.id,
2498
2617
  actor,
2618
+ caller: message.caller,
2619
+ requestDeadline: message.requestDeadline,
2620
+ reply: message.reply,
2499
2621
  returned: (value) => {
2500
2622
  actor.active = null;
2501
2623
  const drained = (fault) => {
@@ -2533,7 +2655,14 @@ class ExecutorV1 {
2533
2655
  failed: (fault) => {
2534
2656
  actor.active = null;
2535
2657
  actor.busy = false;
2536
- if (fault.kind === "panic" && fault.cancelled) {
2658
+ if (fault.kind === "trap" &&
2659
+ fault.trap === "actor_refused" &&
2660
+ !fault.secondary?.length) {
2661
+ actor.crashing = null;
2662
+ message.complete(null, "Refused");
2663
+ this.dispatchActor(actor);
2664
+ }
2665
+ else if (fault.kind === "panic" && fault.cancelled) {
2537
2666
  message.complete(null, "Dead");
2538
2667
  this.dispatchActor(actor);
2539
2668
  }
@@ -2975,8 +3104,10 @@ function combineFaults(primary, secondary) {
2975
3104
  // Reports own an immutable ordered view of original diagnostics. Faults can
2976
3105
  // be shared by a parked unwind, actor completion and multiple observers;
2977
3106
  // combining those views must neither duplicate nor mutate their graph.
2978
- const retained = primary.kind === "panic" && primary.cancelled
2979
- ? diagnostics.filter((fault) => !(fault.kind === "panic" && fault.cancelled))
3107
+ const control = (fault) => (fault.kind === "panic" && fault.cancelled) ||
3108
+ (fault.kind === "trap" && fault.trap === "actor_refused");
3109
+ const retained = control(primary)
3110
+ ? diagnostics.filter((fault) => !control(fault))
2980
3111
  : diagnostics;
2981
3112
  const [first, ...rest] = retained.length ? retained : diagnostics;
2982
3113
  return { ...first, primary: first, secondary: rest };
@@ -2994,6 +3125,7 @@ function faultInfo(fault) {
2994
3125
  divide_by_zero: [202, "DivideByZero"],
2995
3126
  shift_out_of_range: [204, "ShiftOutOfRange"],
2996
3127
  vector_bounds: [205, "IndexOutOfBounds"],
3128
+ actor_refused: [-4, "Refused"],
2997
3129
  }[fault.trap];
2998
3130
  }
2999
3131
  function faultReport(fault, secondary = false) {
@@ -3063,6 +3195,8 @@ function actorErrorRole(reason) {
3063
3195
  return "ActorErrorFailed";
3064
3196
  case "Trapped":
3065
3197
  return "ActorErrorTrapped";
3198
+ case "Refused":
3199
+ return "ActorErrorRefused";
3066
3200
  case "Dead":
3067
3201
  return "ActorErrorDead";
3068
3202
  case "TimedOut":
@@ -3105,12 +3239,17 @@ function sendErrorRole(reason) {
3105
3239
  throw new Error(`unknown send error reason ${reason}`);
3106
3240
  }
3107
3241
  }
3108
- function calleeFunction(value) {
3242
+ function calleeFunction(value, specialization = null) {
3109
3243
  switch (value.kind) {
3110
3244
  case "function":
3111
3245
  return Number(value.id);
3112
- case "closure":
3113
- return value.body;
3246
+ case "closure": {
3247
+ const selected = specialization ?? value.specialization;
3248
+ const entry = value.entries.find((candidate) => candidate.specialization === selected);
3249
+ if (!entry)
3250
+ throw new Error("callable has no selected concrete invocation entry");
3251
+ return entry.body;
3252
+ }
3114
3253
  default:
3115
3254
  throw new TypeError(`indirect.call expected a callable value, got ${value.kind}`);
3116
3255
  }
@@ -3187,6 +3326,8 @@ function trapMessage(trap) {
3187
3326
  return "shift amount out of range";
3188
3327
  case "vector_bounds":
3189
3328
  return "index out of bounds";
3329
+ case "actor_refused":
3330
+ return "delivery refused by interceptor";
3190
3331
  }
3191
3332
  }
3192
3333
  /// The operand type the package records. `isize` and `usize` are 64-bit in the
@@ -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" | "SendErrorBackpressure" | "SendErrorDead" | "SendErrorWriteTimedOut" | "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;
@@ -93,7 +93,10 @@ export interface ValueComponentCapabilities {
93
93
  }
94
94
  export interface ClosureShape {
95
95
  id: number;
96
- body: number;
96
+ entries: Array<{
97
+ body: number;
98
+ specialization: number | null;
99
+ }>;
97
100
  fields: number;
98
101
  }
99
102
  export interface VtableShape {
@@ -326,6 +329,7 @@ export type OpV1 = (OpBase & {
326
329
  op: "callable.coerce";
327
330
  dst: number;
328
331
  source: Operand;
332
+ specialization: number | null;
329
333
  }) | (OpBase & {
330
334
  op: "dyn.make";
331
335
  dst: number;
@@ -426,6 +430,7 @@ export type TermV1 = (TermBase & {
426
430
  }) | (TermBase & CallShape & {
427
431
  op: "indirect.call";
428
432
  callee: BoundaryOperand;
433
+ specialization: number | null;
429
434
  }) | (TermBase & CallShape & {
430
435
  op: "dyn.call";
431
436
  receiver: BoundaryOperand;
@@ -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 [
@@ -162,6 +168,7 @@ const UNIT_FAMILY_SHIMS = {
162
168
  StringConcat: (_host, args) => str(`${text(args, 0)}${text(args, 1)}`),
163
169
  StringClone: (_host, args) => str(text(args, 0)),
164
170
  StringEquals: (_host, args) => bool(text(args, 0) === text(args, 1)),
171
+ StringCompare: (_host, args) => int(BigInt(compareUtf8(text(args, 0), text(args, 1)))),
165
172
  StringContains: (_host, args) => bool(text(args, 0).includes(text(args, 1))),
166
173
  StringTrim: (_host, args) => str(text(args, 0).replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "")),
167
174
  // Native string length counts codepoints, not bytes.
@@ -316,14 +323,8 @@ const VECTOR_SHIMS = {
316
323
  ? host.enumValue(shape, "OptionSome", [cloneValue(items[at])])
317
324
  : host.enumValue(shape, "OptionNone", []);
318
325
  },
319
- Index: (_host, args) => {
320
- const items = vec(args, 0).items;
321
- const at = index(args, 1);
322
- if (at < 0 || at >= items.length) {
323
- throw new ShimFault("vector_bounds", `vector index ${at} out of bounds for length ${items.length}`);
324
- }
325
- return cloneValue(items[at]);
326
- },
326
+ Index: (_host, args) => cloneValue(sequenceElement(args)),
327
+ IndexBorrow: (_host, args) => sequenceElement(args),
327
328
  Len: (_host, args) => int(BigInt(vec(args, 0).items.length)),
328
329
  Slice: (_host, args) => {
329
330
  const items = vec(args, 0).items;
@@ -347,6 +348,14 @@ const VECTOR_SHIMS = {
347
348
  };
348
349
  },
349
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
+ }
350
359
  function byteVector(items) {
351
360
  return { kind: "vector", elementType: "u8", items: items.map(cloneValue) };
352
361
  }
@@ -1,4 +1,4 @@
1
- import { UNIT, cloneValue } from "../values.js";
1
+ import { UNIT, cloneValue, compareUtf8 } from "../values.js";
2
2
  import { runtimeTag, } from "./package.js";
3
3
  import { ShimFault } from "./shims.js";
4
4
  const OFFSET = 0xcbf29ce484222325n;
@@ -32,16 +32,6 @@ function orderingResult(capability, comparison) {
32
32
  return boolean(comparison >= 0);
33
33
  }
34
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
35
  function scalarOrdering(left, right, ty, capability) {
46
36
  if (left.kind === "f64") {
47
37
  if (right?.kind !== "f64" ||
@@ -113,7 +103,7 @@ export function* selectedValue(pkg, id, args) {
113
103
  return boolean(false);
114
104
  if (equality)
115
105
  return boolean(left.value === right.value);
116
- return orderingResult(ordering, compareBytes(new TextEncoder().encode(left.value), new TextEncoder().encode(right.value)));
106
+ return orderingResult(ordering, compareUtf8(left.value, right.value));
117
107
  }
118
108
  if (plan.ty === "bytes") {
119
109
  if (left.kind !== "vector")
@@ -102,10 +102,15 @@ export type VmValue = {
102
102
  * owned by the closure, so `cloneValue` clones it. */
103
103
  | {
104
104
  kind: "closure";
105
- body: number;
105
+ entries: ReadonlyArray<{
106
+ body: number;
107
+ specialization: number | null;
108
+ }>;
109
+ specialization: number | null;
106
110
  environment: VmValue;
107
111
  };
108
112
  export declare const UNIT: VmValue;
113
+ export declare function compareUtf8(a: string, b: string): number;
109
114
  export declare function cloneValue(value: VmValue): VmValue;
110
115
  export declare function valueFromLiteral(value: JsonValue): VmValue;
111
116
  export declare function renderF64(value: number): string;
@@ -1,4 +1,17 @@
1
1
  export const UNIT = { kind: "unit" };
2
+ const UTF8_ENCODER = new TextEncoder();
3
+ export function compareUtf8(a, b) {
4
+ const left = UTF8_ENCODER.encode(a);
5
+ const right = UTF8_ENCODER.encode(b);
6
+ const length = Math.min(left.length, right.length);
7
+ for (let i = 0; i < length; i++) {
8
+ if (left[i] < right[i])
9
+ return -1;
10
+ if (left[i] > right[i])
11
+ return 1;
12
+ }
13
+ return left.length < right.length ? -1 : left.length > right.length ? 1 : 0;
14
+ }
2
15
  const I64_LITERAL_DECIMAL = /^(?:0|-?[1-9]\d*)$/;
3
16
  export function cloneValue(value) {
4
17
  switch (value.kind) {
@@ -61,7 +74,8 @@ export function cloneValue(value) {
61
74
  case "closure":
62
75
  return {
63
76
  kind: "closure",
64
- body: value.body,
77
+ entries: value.entries,
78
+ specialization: value.specialization,
65
79
  environment: cloneValue(value.environment),
66
80
  };
67
81
  case "map":
@@ -226,7 +240,7 @@ export function renderStdout(value) {
226
240
  case "function":
227
241
  return value.id;
228
242
  case "closure":
229
- return `closure:${value.body}`;
243
+ return `closure:${value.entries.find((entry) => entry.specialization === value.specialization)?.body ?? "generic"}`;
230
244
  }
231
245
  }
232
246
  export function toJsonValue(value) {
@@ -274,7 +288,7 @@ export function toJsonValue(value) {
274
288
  case "function":
275
289
  return value.id;
276
290
  case "closure":
277
- return `closure:${value.body}`;
291
+ return `closure:${value.entries.find((entry) => entry.specialization === value.specialization)?.body ?? "generic"}`;
278
292
  }
279
293
  }
280
294
  /// A map renders as its entries in insertion order, each a `[key, value]` pair.
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.1",
4
4
  "hewSource": {
5
- "commit": "c4b08fc6215201308794f6093aa67eb173eda1fd",
6
- "tag": "v0.6.0",
5
+ "commit": "6a446f773391f45d2c0ce0a799240b8b9afef54e",
6
+ "tag": "v0.6.1",
7
7
  "dirty": false
8
8
  },
9
9
  "description": "Deterministic TypeScript interpreter for the Hew educational sandbox",
@@ -21,7 +21,7 @@
21
21
  "cli/"
22
22
  ],
23
23
  "dependencies": {
24
- "@hew-lang/wasm": "0.6.0"
24
+ "@hew-lang/wasm": "0.6.1"
25
25
  },
26
26
  "keywords": [
27
27
  "hew",