@hew-lang/sandbox-vm 0.6.0 → 0.6.2

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" | "SendErrorBackpressure" | "SendErrorDead" | "SendErrorWriteTimedOut" | "DeliveryAccepted" | "DeliveryDiscarded" | "LinkErrorNoContext";
37
+ export type RuntimeVariantRole = "CrashKindFault" | "CrashKindHeapExceeded" | "CrashKindPartitionDetected" | "CrashKindHandlerFailed" | "CrashKindTimerOverdue" | "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" | "SendErrorUnauthorized" | "SendErrorInvalidMessage" | "SendErrorTimedOut" | "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 {
@@ -107,6 +110,7 @@ export interface VtableShape {
107
110
  }>;
108
111
  }
109
112
  export interface FunctionV1 {
113
+ actor_construction?: boolean;
110
114
  id: number;
111
115
  name: string;
112
116
  params: ValueDef[];
@@ -326,6 +330,7 @@ export type OpV1 = (OpBase & {
326
330
  op: "callable.coerce";
327
331
  dst: number;
328
332
  source: Operand;
333
+ specialization: number | null;
329
334
  }) | (OpBase & {
330
335
  op: "dyn.make";
331
336
  dst: number;
@@ -365,6 +370,25 @@ export type OpV1 = (OpBase & {
365
370
  dst: number;
366
371
  scope: number;
367
372
  callable: Operand;
373
+ }) | (OpBase & {
374
+ op: "task.invoke";
375
+ dst: number;
376
+ scope: number;
377
+ callee: {
378
+ direct: number;
379
+ } | {
380
+ mutable: number;
381
+ receiver: BoundaryOperand;
382
+ staged: boolean;
383
+ } | {
384
+ dyn: number;
385
+ method: string;
386
+ receiver: BoundaryOperand;
387
+ } | {
388
+ receiver: BoundaryOperand;
389
+ specialization: number | null;
390
+ };
391
+ args: BoundaryOperand[];
368
392
  });
369
393
  export type OpName = OpV1["op"];
370
394
  interface TermBase {
@@ -426,6 +450,7 @@ export type TermV1 = (TermBase & {
426
450
  }) | (TermBase & CallShape & {
427
451
  op: "indirect.call";
428
452
  callee: BoundaryOperand;
453
+ specialization: number | null;
429
454
  }) | (TermBase & CallShape & {
430
455
  op: "dyn.call";
431
456
  receiver: BoundaryOperand;
@@ -447,12 +472,17 @@ export type TermV1 = (TermBase & {
447
472
  callbacks: Record<string, number[]>;
448
473
  }) | (TermBase & CallShape & {
449
474
  op: "runtime.call";
475
+ shared_payload_type?: string;
450
476
  family: number;
451
477
  structural?: number | null;
452
478
  callbacks?: number[];
453
479
  releases_contents?: boolean;
454
480
  keeps_inputs_on_failure?: boolean;
455
481
  result_member_shapes?: Array<number | null>;
482
+ variant_result_shapes?: {
483
+ error: number;
484
+ error_len: number;
485
+ };
456
486
  result_shape: number | null;
457
487
  }) | (TermBase & CallShape & {
458
488
  op: "extern.call";
@@ -515,7 +545,7 @@ export type TermV1 = (TermBase & {
515
545
  inputs: BoundaryOperand[];
516
546
  result: CallResult;
517
547
  resumes: Edge[];
518
- cancel: Edge;
548
+ cancel: Edge | null;
519
549
  unwind: Edge | null;
520
550
  });
521
551
  export type TermName = TermV1["op"];
@@ -530,6 +560,8 @@ export interface ActorProtocol {
530
560
  sealed: boolean;
531
561
  }
532
562
  export type ActorOperation = {
563
+ op: "identity";
564
+ } | {
533
565
  op: "spawn" | "self_handle" | "stop" | "terminate" | "await_stopped" | "await_restarted";
534
566
  actor: number;
535
567
  } | {
@@ -543,6 +575,12 @@ export type ActorOperation = {
543
575
  op: "stream_start";
544
576
  actor: number;
545
577
  message: number;
578
+ } | {
579
+ op: "timer_schedule";
580
+ actor: number;
581
+ message: number;
582
+ resource: number;
583
+ policy: "reject" | "drop_newest";
546
584
  } | {
547
585
  op: "local_observation";
548
586
  kind: string;
@@ -553,10 +591,10 @@ export type ActorOperation = {
553
591
  op: "supervisor_child" | "supervisor_pool_view";
554
592
  supervisor: number;
555
593
  child: number;
556
- owner_is_role: boolean;
557
594
  };
558
595
  export interface ActorShape {
559
596
  id: number;
597
+ name: string;
560
598
  state_fields: Array<{
561
599
  mutable: boolean;
562
600
  deferred: boolean;
@@ -566,6 +604,7 @@ export interface ActorShape {
566
604
  stop: number[];
567
605
  crash?: number;
568
606
  crash_info?: number;
607
+ crash_kind?: number;
569
608
  crash_action?: number;
570
609
  exit?: number;
571
610
  down?: number;
@@ -604,7 +643,8 @@ export interface SupervisorShape {
604
643
  };
605
644
  restart: string;
606
645
  stop_ns?: number | null;
607
- pool_count?: number;
646
+ stop_config?: number;
647
+ is_pool?: boolean;
608
648
  spawn: number;
609
649
  }>;
610
650
  }
@@ -12,6 +12,7 @@ export declare class Pipes {
12
12
  private half;
13
13
  private get;
14
14
  cloneSink(value: VmValue): VmValue;
15
+ isClosed(value: VmValue): boolean;
15
16
  close(value: VmValue, fault?: string | null): VmValue[];
16
17
  peerClosed(value: VmValue): boolean;
17
18
  readable(value: VmValue): boolean;
@@ -29,10 +29,10 @@ export class Pipes {
29
29
  throw new Error("pipe pair does not exist");
30
30
  return this.half(pipe, kind);
31
31
  }
32
- half(pipe, kind) {
32
+ half(pipe, kind, open = true) {
33
33
  const id = `pipe-half:${++this.next}`;
34
- this.halves.set(id, { pipe, kind, open: true });
35
- if (kind === "sink")
34
+ this.halves.set(id, { pipe, kind, open });
35
+ if (kind === "sink" && open)
36
36
  pipe.sinks += 1;
37
37
  return { kind, channelId: id };
38
38
  }
@@ -46,9 +46,12 @@ export class Pipes {
46
46
  }
47
47
  cloneSink(value) {
48
48
  const half = this.get(value);
49
- if (!half.open || half.kind !== "sink")
50
- throw new Error("cannot clone a closed sink");
51
- return this.half(half.pipe, "sink");
49
+ if (half.kind !== "sink")
50
+ throw new Error("cannot clone a stream as a sink");
51
+ return this.half(half.pipe, "sink", half.open);
52
+ }
53
+ isClosed(value) {
54
+ return !this.get(value).open;
52
55
  }
53
56
  close(value, fault = null) {
54
57
  const half = this.get(value);
@@ -11,5 +11,8 @@ export declare class FrameScheduler {
11
11
  constructor(trace: TraceBuilder, policy: "round_robin" | "chaos");
12
12
  enqueue(id: string, resume: () => void): void;
13
13
  after(nanos: bigint, wake: () => void): () => void;
14
+ hasRunnableWork(): boolean;
15
+ advanceSlice(): void;
16
+ private fireTimers;
14
17
  run(): void;
15
18
  }
@@ -42,19 +42,29 @@ export class FrameScheduler {
42
42
  this.timers.splice(index, 1);
43
43
  };
44
44
  }
45
+ hasRunnableWork() {
46
+ return this.ready.length > 0;
47
+ }
48
+ advanceSlice() {
49
+ this.trace.advanceVirtualClock(this.trace.replay.virtual_clock.tick_ms, null);
50
+ this.fireTimers();
51
+ }
52
+ fireTimers() {
53
+ for (let index = 0; index < this.timers.length;) {
54
+ if (this.timers[index].at <= this.trace.virtualTimeMs) {
55
+ this.timers.splice(index, 1)[0].wake();
56
+ }
57
+ else {
58
+ index += 1;
59
+ }
60
+ }
61
+ }
45
62
  run() {
46
63
  while (this.ready.length > 0 || this.timers.length > 0) {
47
64
  if (this.ready.length === 0) {
48
65
  const next = Math.min(...this.timers.map((timer) => timer.at));
49
66
  this.trace.advanceVirtualClock(next - this.trace.virtualTimeMs, null);
50
- for (let index = 0; index < this.timers.length;) {
51
- if (this.timers[index].at <= next) {
52
- this.timers.splice(index, 1)[0].wake();
53
- }
54
- else {
55
- index += 1;
56
- }
57
- }
67
+ this.fireTimers();
58
68
  }
59
69
  if (this.ready.length === 0)
60
70
  continue;
@@ -7,9 +7,13 @@ export declare class ShimFault extends Error {
7
7
  constructor(trap: TrapName, message: string);
8
8
  }
9
9
  export interface ShimHost {
10
+ readonly argv: readonly string[];
10
11
  pipes?: Pipes;
11
12
  newPipe?(capacity: number): VmValue;
12
13
  closePipe?(value: VmValue): void;
14
+ generatorStream?(generator: VmValue): VmValue;
15
+ encoderSink?(sink: VmValue, encoder: VmValue): VmValue;
16
+ cloneSink?(sink: VmValue): VmValue;
13
17
  releaseValue?(value: VmValue): void;
14
18
  writeStdout(text: string): void;
15
19
  writeStderr(text: string): void;
@@ -22,3 +26,9 @@ export declare function resolveRuntimeShim(entry: RuntimeFamilyEntry): RuntimeSh
22
26
  export declare function resolveExternShim(symbol: string): RuntimeShim | undefined;
23
27
  export declare const SUPPORTED_SUSPEND_KINDS: ReadonlySet<string>;
24
28
  export declare const SUPPORTED_NATIVE_IO: ReadonlySet<string>;
29
+ export declare function decodeUtf8Checked(value: VmValue): {
30
+ value: string;
31
+ } | {
32
+ validUpTo: number;
33
+ errorLen: number | null;
34
+ };
@@ -1,3 +1,4 @@
1
+ import { charProperty, charCase } from "../unicode.js";
1
2
  /// The VM's runtime-family and extern shim table.
2
3
  ///
3
4
  /// This table is the runtime-family admission authority: a package may name a
@@ -6,7 +7,7 @@
6
7
  /// reject list to keep in step with this one. `FileRead` is refused by its
7
8
  /// absence, and a `NativeIo` suspension by an operation missing from
8
9
  /// [`SUPPORTED_NATIVE_IO`].
9
- import { UNIT, cloneValue, renderStdout } from "../values.js";
10
+ import { UNIT, cloneValue, compareUtf8, stringValue, renderStdout, } from "../values.js";
10
11
  import { seededMt } from "./mt19937.js";
11
12
  /// A language-visible fault raised inside a shim. The executor routes it to the
12
13
  /// call's `unwind` edge, or ends the program when the call names none.
@@ -24,23 +25,26 @@ export class ShimFault extends Error {
24
25
  export function resolveRuntimeShim(entry) {
25
26
  switch (entry.family) {
26
27
  case "SupervisorPool":
27
- return ["Member", "Get"].includes(String(entry.detail))
28
+ return ["Len", "Member", "Get"].includes(String(entry.detail))
28
29
  ? () => {
29
30
  throw new Error("SupervisorPool requires its resumable executor");
30
31
  }
31
32
  : undefined;
33
+ case "ActorCaller":
34
+ case "ActorRefusalLog":
32
35
  case "StructuralFormat":
33
36
  return () => {
34
37
  throw new Error("StructuralFormat requires its resumable executor");
35
38
  };
36
39
  case "FileRead":
37
- return entry.detail === "LastError"
38
- ? () => ({ kind: "string", value: "" })
39
- : undefined;
40
+ return entry.detail === "LastError" ? () => str("") : undefined;
40
41
  case "Print":
41
42
  return printShim(entry.detail);
42
43
  case "RcNew":
43
44
  case "RcClone":
45
+ case "RcDowngrade":
46
+ case "WeakCloneRc":
47
+ case "WeakUpgradeRc":
44
48
  case "RcGet":
45
49
  case "RcIsUnique":
46
50
  case "RcSet":
@@ -60,7 +64,11 @@ export function resolveRuntimeShim(entry) {
60
64
  ? () => {
61
65
  throw new Error("array mutation requires its resumable executor");
62
66
  }
63
- : undefined;
67
+ : entry.detail === "IndexBorrow"
68
+ ? (_host, args) => sequenceElement(args)
69
+ : ["Len", "Index"].includes(String(entry.detail))
70
+ ? VECTOR_SHIMS[detailName(entry.detail)]
71
+ : undefined;
64
72
  case "Map":
65
73
  case "Set":
66
74
  return [
@@ -104,6 +112,7 @@ export const SUPPORTED_SUSPEND_KINDS = new Set([
104
112
  "GeneratorNext",
105
113
  "Yield",
106
114
  "Sleep",
115
+ "Safepoint",
107
116
  "Ask",
108
117
  "Await",
109
118
  "Join",
@@ -138,19 +147,15 @@ const UNIT_FAMILY_SHIMS = {
138
147
  StringToBytes: (_host, args) => ({
139
148
  kind: "vector",
140
149
  elementType: "u8",
141
- items: [...new TextEncoder().encode(text(args, 0))].map((byte) => int(BigInt(byte))),
142
- }),
143
- StringToUppercase: (_host, args) => ({
144
- kind: "string",
145
- value: text(args, 0).toUpperCase(),
146
- }),
147
- StringToLowercase: (_host, args) => ({
148
- kind: "string",
149
- value: text(args, 0).toLowerCase(),
150
+ items: [...indexed(args, 0).encoded()].map((byte) => int(BigInt(byte))),
150
151
  }),
152
+ StringToUppercase: (_host, args) => str(text(args, 0).toUpperCase()),
153
+ StringToLowercase: (_host, args) => str(text(args, 0).toLowerCase()),
151
154
  StreamPairSink: (host, args) => host.pipes.extract(arg(args, 0), "sink"),
152
155
  StreamPairStream: (host, args) => host.pipes.extract(arg(args, 0), "stream"),
153
- SinkClone: (host, args) => host.pipes.cloneSink(arg(args, 0)),
156
+ StreamFromGenerator: (host, args) => host.generatorStream(arg(args, 0)),
157
+ SinkFromEncoder: (host, args) => host.encoderSink(arg(args, 0), arg(args, 1)),
158
+ SinkClone: (host, args) => host.cloneSink(arg(args, 0)),
154
159
  SinkClose: (host, args) => {
155
160
  host.closePipe(arg(args, 0));
156
161
  return UNIT;
@@ -160,28 +165,92 @@ const UNIT_FAMILY_SHIMS = {
160
165
  return UNIT;
161
166
  },
162
167
  StringConcat: (_host, args) => str(`${text(args, 0)}${text(args, 1)}`),
163
- StringClone: (_host, args) => str(text(args, 0)),
168
+ StringClone: (_host, args) => cloneValue(arg(args, 0)),
164
169
  StringEquals: (_host, args) => bool(text(args, 0) === text(args, 1)),
165
- StringContains: (_host, args) => bool(text(args, 0).includes(text(args, 1))),
170
+ StringCompare: (_host, args) => int(BigInt(compareUtf8(text(args, 0), text(args, 1)))),
171
+ StringStartsWith: (_host, args) => bool(text(args, 0).startsWith(text(args, 1))),
172
+ StringEndsWith: (_host, args) => bool(text(args, 0).endsWith(text(args, 1))),
173
+ StringContains: (_host, args) => bool(indexed(args, 0).find(indexed(args, 1)) !== undefined),
166
174
  StringTrim: (_host, args) => str(text(args, 0).replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "")),
167
- // Native string length counts codepoints, not bytes.
168
- StringLen: (_host, args) => int(BigInt([...text(args, 0)].length)),
175
+ StringLen: (_host, args) => int(BigInt(indexed(args, 0).scalarLength)),
176
+ StringByteLen: (_host, args) => int(BigInt(indexed(args, 0).byteLength)),
169
177
  StringIndex: (_host, args) => {
170
- const chars = [...text(args, 0)];
178
+ const value = indexed(args, 0);
171
179
  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]);
180
+ const scalar = value.at(at);
181
+ if (scalar === undefined)
182
+ throw new ShimFault("vector_bounds", `string index ${at} out of bounds for length ${value.scalarLength}`);
183
+ return str(scalar);
184
+ },
185
+ StringGet: (host, args, shape) => {
186
+ if (!shape)
187
+ throw new TypeError("string get requires its Option shape");
188
+ const scalar = indexed(args, 0).at(index(args, 1));
189
+ return scalar === undefined
190
+ ? host.enumValue(shape, "OptionNone", [])
191
+ : host.enumValue(shape, "OptionSome", [str(scalar)]);
192
+ },
193
+ StringFind: (host, args, shape) => {
194
+ if (!shape)
195
+ throw new TypeError("string find requires its Option shape");
196
+ const found = indexed(args, 0).find(indexed(args, 1));
197
+ return found === undefined
198
+ ? host.enumValue(shape, "OptionNone", [])
199
+ : host.enumValue(shape, "OptionSome", [int(BigInt(found))]);
200
+ },
201
+ StringFindFrom: (host, args, shape) => {
202
+ if (!shape)
203
+ throw new TypeError("string find_from requires its Option shape");
204
+ const found = indexed(args, 0).find(indexed(args, 1), index(args, 2));
205
+ return found === undefined
206
+ ? host.enumValue(shape, "OptionNone", [])
207
+ : host.enumValue(shape, "OptionSome", [int(BigInt(found))]);
208
+ },
209
+ StringAppend: (_host, args) => indexed(args, 0).append(indexed(args, 1)),
210
+ StringPush: (_host, args) => indexed(args, 0).append(indexed(args, 1)),
211
+ StringClear: (_host, args) => {
212
+ indexed(args, 0).release();
213
+ return str("");
214
+ },
215
+ StringIsEmpty: (_host, args) => bool(indexed(args, 0).scalarLength === 0),
216
+ StringSplit: (_host, args) => {
217
+ const value = indexed(args, 0).value;
218
+ const separator = indexed(args, 1).value;
219
+ const parts = separator === "" ? Array.from(value) : value.split(separator);
220
+ return { kind: "vector", elementType: "string", items: parts.map(str) };
176
221
  },
177
- StringIsEmpty: (_host, args) => bool(text(args, 0).length === 0),
178
222
  StringReplace: (_host, args) => str(text(args, 0).split(text(args, 1)).join(text(args, 2))),
179
- // Native slicing is by codepoint and clamps rather than trapping.
180
- StringSlice: (_host, args) => {
181
- const chars = [...text(args, 0)];
182
- const start = Math.min(Math.max(index(args, 1), 0), chars.length);
183
- const end = Math.min(Math.max(index(args, 2), start), chars.length);
184
- return str(chars.slice(start, end).join(""));
223
+ StringRepeat: (_host, args) => str(text(args, 0).repeat(Math.max(0, index(args, 1)))),
224
+ StringGetRange: (host, args, shape) => {
225
+ if (!shape)
226
+ throw new TypeError("string range get requires its Option shape");
227
+ const value = stringRange(args);
228
+ return value === undefined
229
+ ? host.enumValue(shape, "OptionNone", [])
230
+ : host.enumValue(shape, "OptionSome", [value]);
231
+ },
232
+ StringSliceRange: (_host, args) => {
233
+ const value = stringRange(args);
234
+ if (value === undefined)
235
+ throw new ShimFault("vector_bounds", "string range out of bounds");
236
+ return value;
237
+ },
238
+ StringSliceCodepoints: (_host, args) => {
239
+ const value = indexed(args, 0);
240
+ const start = index(args, 1);
241
+ const end = index(args, 2);
242
+ const sliced = value.slice(start, end);
243
+ if (!sliced)
244
+ throw new ShimFault("vector_bounds", `string slice ${start}..${end} out of bounds for length ${value.scalarLength}`);
245
+ return sliced;
246
+ },
247
+ StringSliceCodepointsFrom: (_host, args) => {
248
+ const value = indexed(args, 0);
249
+ const start = index(args, 1);
250
+ const sliced = value.slice(start, value.scalarLength);
251
+ if (!sliced)
252
+ throw new ShimFault("vector_bounds", `string slice ${start}.. out of bounds for length ${value.scalarLength}`);
253
+ return sliced;
185
254
  },
186
255
  I64ToString: (_host, args) => str(renderStdout(arg(args, 0))),
187
256
  U64ToString: (_host, args) => str(renderStdout(arg(args, 0))),
@@ -190,8 +259,41 @@ const UNIT_FAMILY_SHIMS = {
190
259
  U8ToString: (_host, args) => str(renderStdout(arg(args, 0))),
191
260
  BoolToString: (_host, args) => str(renderStdout(arg(args, 0))),
192
261
  F64ToString: (_host, args) => str(renderStdout(arg(args, 0))),
262
+ CharIsDigit: (_host, args) => bool(charProperty(text(args, 0), 0)),
263
+ CharIsLetter: (_host, args) => bool(charProperty(text(args, 0), 1)),
264
+ CharIsAlphanumeric: (_host, args) => bool(charProperty(text(args, 0), 2)),
265
+ CharIsWhitespace: (_host, args) => bool(charProperty(text(args, 0), 3)),
266
+ CharIsUpper: (_host, args) => bool(charProperty(text(args, 0), 4)),
267
+ CharIsLower: (_host, args) => bool(charProperty(text(args, 0), 5)),
268
+ CharIsPunctuation: (_host, args) => bool(charProperty(text(args, 0), 6)),
269
+ CharIsControl: (_host, args) => bool(charProperty(text(args, 0), 7)),
270
+ CharIsAscii: (_host, args) => bool(charProperty(text(args, 0), 8)),
271
+ CharLenUtf8: (_host, args) => int(BigInt(indexed(args, 0).byteLength)),
272
+ CharToUpper: (_host, args) => str(charCase(text(args, 0), "upper")),
273
+ CharToLower: (_host, args) => str(charCase(text(args, 0), "lower")),
274
+ CharToTitle: (_host, args) => str(charCase(text(args, 0), "title")),
275
+ CharToDigit: (host, args, shape) => {
276
+ if (!shape)
277
+ throw new TypeError("char to_digit requires its Option shape");
278
+ const radix = index(args, 1);
279
+ const scalar = text(args, 0).codePointAt(0);
280
+ const digit = scalar >= 48 && scalar <= 57
281
+ ? scalar - 48
282
+ : scalar >= 65 && scalar <= 90
283
+ ? scalar - 65 + 10
284
+ : scalar >= 97 && scalar <= 122
285
+ ? scalar - 97 + 10
286
+ : -1;
287
+ return radix < 2 || radix > 36 || digit < 0 || digit >= radix
288
+ ? host.enumValue(shape, "OptionNone", [])
289
+ : host.enumValue(shape, "OptionSome", [int(BigInt(digit))]);
290
+ },
291
+ CharFromI64: checkedChar,
292
+ CharFromU64: checkedChar,
193
293
  CharToString: (_host, args) => str(renderStdout(arg(args, 0))),
194
- BytesDecodeUtf8: (_host, args) => str(decodeUtf8(arg(args, 0))),
294
+ BytesDecodeUtf8: () => {
295
+ throw new Error("UTF-8 decode requires its checked result carrier");
296
+ },
195
297
  // TextDecoder replaces each maximal invalid subsequence with U+FFFD, as
196
298
  // native lossy decoding does.
197
299
  BytesDecodeUtf8Lossy: (_host, args) => str(decodeUtf8(arg(args, 0))),
@@ -316,15 +418,15 @@ const VECTOR_SHIMS = {
316
418
  ? host.enumValue(shape, "OptionSome", [cloneValue(items[at])])
317
419
  : host.enumValue(shape, "OptionNone", []);
318
420
  },
319
- Index: (_host, args) => {
421
+ Index: (_host, args) => cloneValue(sequenceElement(args)),
422
+ IndexBorrow: (_host, args) => sequenceElement(args),
423
+ Len: (_host, args) => int(BigInt(vec(args, 0).items.length)),
424
+ Join: (_host, args) => {
320
425
  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]);
426
+ return str(items
427
+ .map((_, at) => indexed(items, at).value)
428
+ .join(indexed(args, 1).value));
326
429
  },
327
- Len: (_host, args) => int(BigInt(vec(args, 0).items.length)),
328
430
  Slice: (_host, args) => {
329
431
  const items = vec(args, 0).items;
330
432
  const start = index(args, 1);
@@ -347,6 +449,14 @@ const VECTOR_SHIMS = {
347
449
  };
348
450
  },
349
451
  };
452
+ function sequenceElement(args) {
453
+ const items = vec(args, 0).items;
454
+ const at = index(args, 1);
455
+ if (at < 0 || at >= items.length) {
456
+ throw new ShimFault("vector_bounds", `vector index ${at} out of bounds for length ${items.length}`);
457
+ }
458
+ return items[at];
459
+ }
350
460
  function byteVector(items) {
351
461
  return { kind: "vector", elementType: "u8", items: items.map(cloneValue) };
352
462
  }
@@ -656,6 +766,24 @@ function bigMax(left, right) {
656
766
  }
657
767
  // ── externs ─────────────────────────────────────────────────────────────────
658
768
  const EXTERN_SHIMS = {
769
+ hew_args_count: (host) => int(BigInt(host.argv.length)),
770
+ // A native null managed string is canonical empty. Each result owns fresh
771
+ // string storage; releasing it cannot change the host's supplied arguments.
772
+ hew_args_get: (host, args) => str(host.argv[index(args, 0)] ?? ""),
773
+ hew_actor_timer_cancel: (_host, args) => {
774
+ const timer = arg(args, 0);
775
+ if (timer.kind !== "timer" || timer.closed)
776
+ throw new Error("invalid timer handle");
777
+ timer.cancel();
778
+ return UNIT;
779
+ },
780
+ hew_actor_timer_release: (_host, args) => {
781
+ const timer = arg(args, 0);
782
+ if (timer.kind !== "timer" || timer.closed)
783
+ throw new Error("invalid timer handle");
784
+ timer.closed = true;
785
+ return UNIT;
786
+ },
659
787
  hew_msg_envelope_release: (host, args) => {
660
788
  host.releaseValue(arg(args, 0));
661
789
  return UNIT;
@@ -666,7 +794,7 @@ const EXTERN_SHIMS = {
666
794
  host.pipes.freePair(arg(args, 0));
667
795
  return UNIT;
668
796
  },
669
- hew_stream_last_error: () => ({ kind: "string", value: "" }),
797
+ hew_stream_last_error: () => str(""),
670
798
  hew_regex_new: (host, args) => compileRegex(host, args),
671
799
  hew_regex_clone: (_host, args) => cloneValue(arg(args, 0)),
672
800
  // Releasing a handle the VM traces by reference is nothing to do.
@@ -712,6 +840,47 @@ const EXTERN_SHIMS = {
712
840
  return int(high <= low ? low : low + host.prng.randbelow(high - low));
713
841
  },
714
842
  };
843
+ export function decodeUtf8Checked(value) {
844
+ if (value.kind !== "vector")
845
+ throw new TypeError("UTF-8 input must be bytes");
846
+ const bytes = Uint8Array.from(value.items.map((item) => Number(integer([item], 0))));
847
+ let at = 0;
848
+ while (at < bytes.length) {
849
+ const first = bytes[at];
850
+ if (first < 128) {
851
+ at++;
852
+ continue;
853
+ }
854
+ const width = first >= 194 && first <= 223
855
+ ? 2
856
+ : first >= 224 && first <= 239
857
+ ? 3
858
+ : first >= 240 && first <= 244
859
+ ? 4
860
+ : 0;
861
+ if (width === 0)
862
+ return { validUpTo: at, errorLen: 1 };
863
+ for (let offset = 1; offset < width; offset++) {
864
+ if (at + offset >= bytes.length)
865
+ return { validUpTo: at, errorLen: null };
866
+ const next = bytes[at + offset];
867
+ const lower = offset === 1 && first === 224
868
+ ? 160
869
+ : offset === 1 && first === 240
870
+ ? 144
871
+ : 128;
872
+ const upper = offset === 1 && first === 237
873
+ ? 159
874
+ : offset === 1 && first === 244
875
+ ? 143
876
+ : 191;
877
+ if (next < lower || next > upper)
878
+ return { validUpTo: at, errorLen: offset };
879
+ }
880
+ at += width;
881
+ }
882
+ return { value: new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes) };
883
+ }
715
884
  /// `bytes` is a vector of byte values, so decoding walks its items.
716
885
  function decodeUtf8(value) {
717
886
  if (value.kind === "string") {
@@ -741,6 +910,12 @@ function detailName(detail) {
741
910
  function arg(args, at) {
742
911
  return args[at] ?? UNIT;
743
912
  }
913
+ function indexed(args, at) {
914
+ const value = arg(args, at);
915
+ if (value.kind !== "string")
916
+ throw new TypeError(`runtime call expected a string operand, got ${value.kind}`);
917
+ return value;
918
+ }
744
919
  function text(args, at) {
745
920
  const value = arg(args, at);
746
921
  return value.kind === "string" ? value.value : renderStdout(value);
@@ -770,7 +945,7 @@ function comparable(value) {
770
945
  return `${value.kind}:${renderStdout(value)}`;
771
946
  }
772
947
  function str(value) {
773
- return { kind: "string", value };
948
+ return stringValue(value);
774
949
  }
775
950
  function bool(value) {
776
951
  return { kind: "bool", value };
@@ -778,3 +953,29 @@ function bool(value) {
778
953
  function int(value) {
779
954
  return { kind: "i64", value };
780
955
  }
956
+ function checkedChar(host, args, shape) {
957
+ if (!shape)
958
+ throw new TypeError("try_to_char requires its Option shape");
959
+ const value = integer(args, 0);
960
+ return value < 0n ||
961
+ value > 0x10ffffn ||
962
+ (value >= 0xd800n && value <= 0xdfffn)
963
+ ? host.enumValue(shape, "OptionNone", [])
964
+ : host.enumValue(shape, "OptionSome", [
965
+ str(String.fromCodePoint(Number(value))),
966
+ ]);
967
+ }
968
+ function stringRange(args) {
969
+ const value = indexed(args, 0);
970
+ const inclusive = arg(args, 3);
971
+ const openEnd = arg(args, 4);
972
+ if (inclusive.kind !== "bool" || openEnd.kind !== "bool")
973
+ throw new TypeError("string range flags must be bool");
974
+ const start = integer(args, 1);
975
+ const end = openEnd.value
976
+ ? BigInt(value.scalarLength)
977
+ : integer(args, 2) + (inclusive.value ? 1n : 0n);
978
+ if (start < 0n || end < start || end > BigInt(value.scalarLength))
979
+ return undefined;
980
+ return value.slice(Number(start), Number(end));
981
+ }