@hew-lang/sandbox-vm 0.6.1 → 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.
@@ -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" | "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";
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;
@@ -110,6 +110,7 @@ export interface VtableShape {
110
110
  }>;
111
111
  }
112
112
  export interface FunctionV1 {
113
+ actor_construction?: boolean;
113
114
  id: number;
114
115
  name: string;
115
116
  params: ValueDef[];
@@ -369,6 +370,25 @@ export type OpV1 = (OpBase & {
369
370
  dst: number;
370
371
  scope: number;
371
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[];
372
392
  });
373
393
  export type OpName = OpV1["op"];
374
394
  interface TermBase {
@@ -452,12 +472,17 @@ export type TermV1 = (TermBase & {
452
472
  callbacks: Record<string, number[]>;
453
473
  }) | (TermBase & CallShape & {
454
474
  op: "runtime.call";
475
+ shared_payload_type?: string;
455
476
  family: number;
456
477
  structural?: number | null;
457
478
  callbacks?: number[];
458
479
  releases_contents?: boolean;
459
480
  keeps_inputs_on_failure?: boolean;
460
481
  result_member_shapes?: Array<number | null>;
482
+ variant_result_shapes?: {
483
+ error: number;
484
+ error_len: number;
485
+ };
461
486
  result_shape: number | null;
462
487
  }) | (TermBase & CallShape & {
463
488
  op: "extern.call";
@@ -520,7 +545,7 @@ export type TermV1 = (TermBase & {
520
545
  inputs: BoundaryOperand[];
521
546
  result: CallResult;
522
547
  resumes: Edge[];
523
- cancel: Edge;
548
+ cancel: Edge | null;
524
549
  unwind: Edge | null;
525
550
  });
526
551
  export type TermName = TermV1["op"];
@@ -535,6 +560,8 @@ export interface ActorProtocol {
535
560
  sealed: boolean;
536
561
  }
537
562
  export type ActorOperation = {
563
+ op: "identity";
564
+ } | {
538
565
  op: "spawn" | "self_handle" | "stop" | "terminate" | "await_stopped" | "await_restarted";
539
566
  actor: number;
540
567
  } | {
@@ -548,6 +575,12 @@ export type ActorOperation = {
548
575
  op: "stream_start";
549
576
  actor: number;
550
577
  message: number;
578
+ } | {
579
+ op: "timer_schedule";
580
+ actor: number;
581
+ message: number;
582
+ resource: number;
583
+ policy: "reject" | "drop_newest";
551
584
  } | {
552
585
  op: "local_observation";
553
586
  kind: string;
@@ -558,10 +591,10 @@ export type ActorOperation = {
558
591
  op: "supervisor_child" | "supervisor_pool_view";
559
592
  supervisor: number;
560
593
  child: number;
561
- owner_is_role: boolean;
562
594
  };
563
595
  export interface ActorShape {
564
596
  id: number;
597
+ name: string;
565
598
  state_fields: Array<{
566
599
  mutable: boolean;
567
600
  deferred: boolean;
@@ -571,6 +604,7 @@ export interface ActorShape {
571
604
  stop: number[];
572
605
  crash?: number;
573
606
  crash_info?: number;
607
+ crash_kind?: number;
574
608
  crash_action?: number;
575
609
  exit?: number;
576
610
  down?: number;
@@ -609,7 +643,8 @@ export interface SupervisorShape {
609
643
  };
610
644
  restart: string;
611
645
  stop_ns?: number | null;
612
- pool_count?: number;
646
+ stop_config?: number;
647
+ is_pool?: boolean;
613
648
  spawn: number;
614
649
  }>;
615
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, compareUtf8, 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,7 +25,7 @@ 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
  }
@@ -36,13 +37,14 @@ export function resolveRuntimeShim(entry) {
36
37
  throw new Error("StructuralFormat requires its resumable executor");
37
38
  };
38
39
  case "FileRead":
39
- return entry.detail === "LastError"
40
- ? () => ({ kind: "string", value: "" })
41
- : undefined;
40
+ return entry.detail === "LastError" ? () => str("") : undefined;
42
41
  case "Print":
43
42
  return printShim(entry.detail);
44
43
  case "RcNew":
45
44
  case "RcClone":
45
+ case "RcDowngrade":
46
+ case "WeakCloneRc":
47
+ case "WeakUpgradeRc":
46
48
  case "RcGet":
47
49
  case "RcIsUnique":
48
50
  case "RcSet":
@@ -110,6 +112,7 @@ export const SUPPORTED_SUSPEND_KINDS = new Set([
110
112
  "GeneratorNext",
111
113
  "Yield",
112
114
  "Sleep",
115
+ "Safepoint",
113
116
  "Ask",
114
117
  "Await",
115
118
  "Join",
@@ -144,19 +147,15 @@ const UNIT_FAMILY_SHIMS = {
144
147
  StringToBytes: (_host, args) => ({
145
148
  kind: "vector",
146
149
  elementType: "u8",
147
- items: [...new TextEncoder().encode(text(args, 0))].map((byte) => int(BigInt(byte))),
148
- }),
149
- StringToUppercase: (_host, args) => ({
150
- kind: "string",
151
- value: text(args, 0).toUpperCase(),
152
- }),
153
- StringToLowercase: (_host, args) => ({
154
- kind: "string",
155
- value: text(args, 0).toLowerCase(),
150
+ items: [...indexed(args, 0).encoded()].map((byte) => int(BigInt(byte))),
156
151
  }),
152
+ StringToUppercase: (_host, args) => str(text(args, 0).toUpperCase()),
153
+ StringToLowercase: (_host, args) => str(text(args, 0).toLowerCase()),
157
154
  StreamPairSink: (host, args) => host.pipes.extract(arg(args, 0), "sink"),
158
155
  StreamPairStream: (host, args) => host.pipes.extract(arg(args, 0), "stream"),
159
- 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)),
160
159
  SinkClose: (host, args) => {
161
160
  host.closePipe(arg(args, 0));
162
161
  return UNIT;
@@ -166,29 +165,92 @@ const UNIT_FAMILY_SHIMS = {
166
165
  return UNIT;
167
166
  },
168
167
  StringConcat: (_host, args) => str(`${text(args, 0)}${text(args, 1)}`),
169
- StringClone: (_host, args) => str(text(args, 0)),
168
+ StringClone: (_host, args) => cloneValue(arg(args, 0)),
170
169
  StringEquals: (_host, args) => bool(text(args, 0) === text(args, 1)),
171
170
  StringCompare: (_host, args) => int(BigInt(compareUtf8(text(args, 0), text(args, 1)))),
172
- StringContains: (_host, args) => bool(text(args, 0).includes(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),
173
174
  StringTrim: (_host, args) => str(text(args, 0).replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "")),
174
- // Native string length counts codepoints, not bytes.
175
- 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)),
176
177
  StringIndex: (_host, args) => {
177
- const chars = [...text(args, 0)];
178
+ const value = indexed(args, 0);
178
179
  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]);
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) };
183
221
  },
184
- StringIsEmpty: (_host, args) => bool(text(args, 0).length === 0),
185
222
  StringReplace: (_host, args) => str(text(args, 0).split(text(args, 1)).join(text(args, 2))),
186
- // Native slicing is by codepoint and clamps rather than trapping.
187
- StringSlice: (_host, args) => {
188
- const chars = [...text(args, 0)];
189
- const start = Math.min(Math.max(index(args, 1), 0), chars.length);
190
- const end = Math.min(Math.max(index(args, 2), start), chars.length);
191
- 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;
192
254
  },
193
255
  I64ToString: (_host, args) => str(renderStdout(arg(args, 0))),
194
256
  U64ToString: (_host, args) => str(renderStdout(arg(args, 0))),
@@ -197,8 +259,41 @@ const UNIT_FAMILY_SHIMS = {
197
259
  U8ToString: (_host, args) => str(renderStdout(arg(args, 0))),
198
260
  BoolToString: (_host, args) => str(renderStdout(arg(args, 0))),
199
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,
200
293
  CharToString: (_host, args) => str(renderStdout(arg(args, 0))),
201
- BytesDecodeUtf8: (_host, args) => str(decodeUtf8(arg(args, 0))),
294
+ BytesDecodeUtf8: () => {
295
+ throw new Error("UTF-8 decode requires its checked result carrier");
296
+ },
202
297
  // TextDecoder replaces each maximal invalid subsequence with U+FFFD, as
203
298
  // native lossy decoding does.
204
299
  BytesDecodeUtf8Lossy: (_host, args) => str(decodeUtf8(arg(args, 0))),
@@ -326,6 +421,12 @@ const VECTOR_SHIMS = {
326
421
  Index: (_host, args) => cloneValue(sequenceElement(args)),
327
422
  IndexBorrow: (_host, args) => sequenceElement(args),
328
423
  Len: (_host, args) => int(BigInt(vec(args, 0).items.length)),
424
+ Join: (_host, args) => {
425
+ const items = vec(args, 0).items;
426
+ return str(items
427
+ .map((_, at) => indexed(items, at).value)
428
+ .join(indexed(args, 1).value));
429
+ },
329
430
  Slice: (_host, args) => {
330
431
  const items = vec(args, 0).items;
331
432
  const start = index(args, 1);
@@ -665,6 +766,24 @@ function bigMax(left, right) {
665
766
  }
666
767
  // ── externs ─────────────────────────────────────────────────────────────────
667
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
+ },
668
787
  hew_msg_envelope_release: (host, args) => {
669
788
  host.releaseValue(arg(args, 0));
670
789
  return UNIT;
@@ -675,7 +794,7 @@ const EXTERN_SHIMS = {
675
794
  host.pipes.freePair(arg(args, 0));
676
795
  return UNIT;
677
796
  },
678
- hew_stream_last_error: () => ({ kind: "string", value: "" }),
797
+ hew_stream_last_error: () => str(""),
679
798
  hew_regex_new: (host, args) => compileRegex(host, args),
680
799
  hew_regex_clone: (_host, args) => cloneValue(arg(args, 0)),
681
800
  // Releasing a handle the VM traces by reference is nothing to do.
@@ -721,6 +840,47 @@ const EXTERN_SHIMS = {
721
840
  return int(high <= low ? low : low + host.prng.randbelow(high - low));
722
841
  },
723
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
+ }
724
884
  /// `bytes` is a vector of byte values, so decoding walks its items.
725
885
  function decodeUtf8(value) {
726
886
  if (value.kind === "string") {
@@ -750,6 +910,12 @@ function detailName(detail) {
750
910
  function arg(args, at) {
751
911
  return args[at] ?? UNIT;
752
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
+ }
753
919
  function text(args, at) {
754
920
  const value = arg(args, at);
755
921
  return value.kind === "string" ? value.value : renderStdout(value);
@@ -779,7 +945,7 @@ function comparable(value) {
779
945
  return `${value.kind}:${renderStdout(value)}`;
780
946
  }
781
947
  function str(value) {
782
- return { kind: "string", value };
948
+ return stringValue(value);
783
949
  }
784
950
  function bool(value) {
785
951
  return { kind: "bool", value };
@@ -787,3 +953,29 @@ function bool(value) {
787
953
  function int(value) {
788
954
  return { kind: "i64", value };
789
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
+ }