@hew-lang/sandbox-vm 0.6.1 → 0.6.3

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.
Files changed (52) hide show
  1. package/README.md +48 -4
  2. package/cli/test-runner.mjs +4 -1
  3. package/dist/interpreter/interpreter.d.ts +1 -1
  4. package/dist/interpreter/interpreter.js +1 -1
  5. package/dist/interpreter/parity-runner.js +1 -1
  6. package/dist/interpreter/run-program.d.ts +3 -2
  7. package/dist/interpreter/run-program.js +5 -3
  8. package/dist/interpreter/schema-validator.d.ts +9 -4
  9. package/dist/interpreter/schema-validator.js +15 -49
  10. package/dist/interpreter/strings.d.ts +21 -0
  11. package/dist/interpreter/strings.js +191 -0
  12. package/dist/interpreter/types.d.ts +6 -0
  13. package/dist/interpreter/unicode-data.d.ts +4 -0
  14. package/dist/interpreter/unicode-data.js +1673 -0
  15. package/dist/interpreter/unicode.d.ts +3 -0
  16. package/dist/interpreter/unicode.js +35 -0
  17. package/dist/interpreter/v1/actor-identity.d.ts +3 -0
  18. package/dist/interpreter/v1/actor-identity.js +7 -0
  19. package/dist/interpreter/v1/actor-waits.d.ts +6 -0
  20. package/dist/interpreter/v1/actor-waits.js +33 -0
  21. package/dist/interpreter/v1/browser-directory.d.ts +107 -0
  22. package/dist/interpreter/v1/browser-directory.js +288 -0
  23. package/dist/interpreter/v1/calendar.d.ts +4 -0
  24. package/dist/interpreter/v1/calendar.js +64 -0
  25. package/dist/interpreter/v1/codecs.js +8 -11
  26. package/dist/interpreter/v1/exec.d.ts +2 -2
  27. package/dist/interpreter/v1/exec.js +1761 -489
  28. package/dist/interpreter/v1/instructions.generated.d.ts +1135 -0
  29. package/dist/interpreter/v1/instructions.generated.js +106 -0
  30. package/dist/interpreter/v1/package-graph.d.ts +2 -0
  31. package/dist/interpreter/v1/package-graph.js +729 -0
  32. package/dist/interpreter/v1/package-validator.generated.d.ts +3 -0
  33. package/dist/interpreter/v1/package-validator.generated.js +75902 -0
  34. package/dist/interpreter/v1/package.d.ts +5 -645
  35. package/dist/interpreter/v1/package.js +0 -12
  36. package/dist/interpreter/v1/pipes.d.ts +1 -0
  37. package/dist/interpreter/v1/pipes.js +9 -6
  38. package/dist/interpreter/v1/scheduler.d.ts +8 -1
  39. package/dist/interpreter/v1/scheduler.js +50 -11
  40. package/dist/interpreter/v1/shims.d.ts +260 -7
  41. package/dist/interpreter/v1/shims.js +647 -175
  42. package/dist/interpreter/v1/structural.d.ts +2 -28
  43. package/dist/interpreter/v1/timestamp.d.ts +7 -0
  44. package/dist/interpreter/v1/timestamp.js +54 -0
  45. package/dist/interpreter/v1/validate.d.ts +8 -2
  46. package/dist/interpreter/v1/validate.js +284 -200
  47. package/dist/interpreter/v1/value-operations.d.ts +11 -3
  48. package/dist/interpreter/v1/value-operations.js +70 -18
  49. package/dist/interpreter/values.d.ts +30 -8
  50. package/dist/interpreter/values.js +31 -5
  51. package/dist/interpreter/worker.js +4 -3
  52. package/package.json +4 -4
@@ -1,12 +1,9 @@
1
- /// The VM's runtime-family and extern shim table.
2
- ///
3
- /// This table is the runtime-family admission authority: a package may name a
4
- /// runtime family or an extern symbol exactly when there is a shim here for it.
5
- /// A family or symbol with no entry is rejected at load, so there is no second
6
- /// reject list to keep in step with this one. `FileRead` is refused by its
7
- /// absence, and a `NativeIo` suspension by an operation missing from
8
- /// [`SUPPORTED_NATIVE_IO`].
9
- import { UNIT, cloneValue, compareUtf8, renderStdout, } from "../values.js";
1
+ import { DIRECTORY_OPERATIONS, } from "./browser-directory.js";
2
+ import { executeCalendar } from "./calendar.js";
3
+ import { charProperty, charCase } from "../unicode.js";
4
+ /// Runtime implementations and their host execution registration. Admission and
5
+ /// execution consume the same ordinary, resumable or refused disposition.
6
+ import { UNIT, cloneValue, compareUtf8, stringValue, renderStdout, } from "../values.js";
10
7
  import { seededMt } from "./mt19937.js";
11
8
  /// A language-visible fault raised inside a shim. The executor routes it to the
12
9
  /// call's `unwind` edge, or ends the program when the call names none.
@@ -18,125 +15,252 @@ export class ShimFault extends Error {
18
15
  this.name = "ShimFault";
19
16
  }
20
17
  }
21
- /// Resolve a declared runtime family to its shim, or `undefined` when the VM
22
- /// has none. Both the load-time validator and the executor go through here, so
23
- /// admission and execution can never disagree about what is implemented.
24
- export function resolveRuntimeShim(entry) {
18
+ function ordinary(shim) {
19
+ return { kind: "ordinary", shim };
20
+ }
21
+ function refused(capability, native = false) {
22
+ return {
23
+ kind: "refused",
24
+ category: native ? "native_only" : "not_implemented",
25
+ capability,
26
+ };
27
+ }
28
+ function resumable(continuation) {
29
+ return { kind: "resumable", ...continuation };
30
+ }
31
+ /// A registered algorithm may still require a host-supplied Wasm bridge.
32
+ export function registrationRefusal(registration, bridges) {
33
+ if (registration.kind === "refused")
34
+ return registration;
35
+ if (registration.kind === "directory" &&
36
+ registration.operation.action === "root" &&
37
+ !bridges.directoryRoots?.[registration.operation.access])
38
+ return {
39
+ ...refused(`directoryRoots.${registration.operation.access}`),
40
+ message: `This operation requires a host-supplied OPFS ${registration.operation.access} directory handle.`,
41
+ };
42
+ return "bridge" in registration &&
43
+ registration.bridge &&
44
+ typeof bridges[registration.bridge] !== "function"
45
+ ? refused(registration.bridge)
46
+ : null;
47
+ }
48
+ export const CODEC_REGISTRATION = {
49
+ kind: "resumable",
50
+ bridge: "codecBridge",
51
+ };
52
+ /// Each typed runtime identity has exactly one host disposition. An added Rust
53
+ /// family or nested operation must acquire a disposition before TypeScript builds.
54
+ export function resolveRuntime(entry) {
25
55
  switch (entry.family) {
26
56
  case "SupervisorPool":
27
- return ["Member", "Get"].includes(String(entry.detail))
28
- ? () => {
29
- throw new Error("SupervisorPool requires its resumable executor");
30
- }
31
- : undefined;
32
- case "ActorCaller":
33
- case "ActorRefusalLog":
34
- case "StructuralFormat":
35
- return () => {
36
- throw new Error("StructuralFormat requires its resumable executor");
37
- };
38
- case "FileRead":
39
- return entry.detail === "LastError"
40
- ? () => ({ kind: "string", value: "" })
41
- : undefined;
57
+ return resumable({ handler: "supervisor_pool", operation: entry.detail });
42
58
  case "Print":
43
- return printShim(entry.detail);
44
- case "RcNew":
45
- case "RcClone":
46
- case "RcGet":
47
- case "RcIsUnique":
48
- case "RcSet":
49
- case "RcStrongCount":
50
- case "RcWeakCount":
51
- return () => {
52
- throw new Error("shared operation requires its resumable executor");
53
- };
54
- case "Vector":
55
- return ["Contains", "Set", "Clear"].includes(String(entry.detail))
56
- ? () => {
57
- throw new Error("vector Contains requires its selected Eq executor");
59
+ return ordinary(printShim(entry.detail));
60
+ case "FileRead":
61
+ switch (entry.detail) {
62
+ case "HasError":
63
+ case "LastErrorKind":
64
+ case "LastErrno":
65
+ case "LastError": {
66
+ const operation = entry.detail;
67
+ return ordinary((host) => host.fileError(operation));
58
68
  }
59
- : VECTOR_SHIMS[detailName(entry.detail)];
69
+ default:
70
+ return refused(`FileRead::${entry.detail}`, true);
71
+ }
72
+ case "AsyncIo":
73
+ case "Tcp":
74
+ return refused(`${entry.family}::${entry.detail}`, true);
75
+ case "Semaphore":
76
+ case "Service":
77
+ case "VecContainsScalar":
78
+ case "VecGet":
79
+ case "VecSliceRange":
80
+ return refused(`${entry.family}::${entry.detail}`);
81
+ case "Encoding":
82
+ case "VecScalar":
83
+ return refused(entry.family);
84
+ case "Vector":
85
+ switch (entry.detail) {
86
+ case "Contains":
87
+ case "Set":
88
+ case "Clear":
89
+ return resumable({
90
+ handler: "collection",
91
+ family: "Vector",
92
+ operation: entry.detail,
93
+ });
94
+ case "Pop":
95
+ case "GetBorrow":
96
+ case "TakeFirst":
97
+ case "TakeAll":
98
+ case "Append":
99
+ return refused(`Vector::${entry.detail}`);
100
+ default:
101
+ return ordinary(VECTOR_SHIMS[entry.detail]);
102
+ }
60
103
  case "Array":
61
- return entry.detail === "Set"
62
- ? () => {
63
- throw new Error("array mutation requires its resumable executor");
104
+ switch (entry.detail) {
105
+ case "Set":
106
+ return resumable({
107
+ handler: "collection",
108
+ family: "Vector",
109
+ operation: entry.detail,
110
+ });
111
+ case "Len":
112
+ case "Index":
113
+ case "IndexBorrow":
114
+ return ordinary(VECTOR_SHIMS[entry.detail]);
115
+ default: {
116
+ const exhaustive = entry;
117
+ throw new Error(`unknown array operation ${String(exhaustive)}`);
64
118
  }
65
- : entry.detail === "IndexBorrow"
66
- ? (_host, args) => sequenceElement(args)
67
- : ["Len", "Index"].includes(String(entry.detail))
68
- ? VECTOR_SHIMS[detailName(entry.detail)]
69
- : undefined;
119
+ }
70
120
  case "Map":
121
+ return resumable({
122
+ handler: "collection",
123
+ family: "Map",
124
+ operation: entry.detail,
125
+ });
71
126
  case "Set":
72
- return [
73
- "New",
74
- "Len",
75
- "Index",
76
- "Get",
77
- "GetBorrow",
78
- "ContainsKey",
79
- "Contains",
80
- "Insert",
81
- "Remove",
82
- "Clear",
83
- "Keys",
84
- "Values",
85
- "Entries",
86
- "Elements",
87
- ].includes(detailName(entry.detail))
88
- ? () => {
89
- throw new Error("collection operation requires its resumable executor");
90
- }
91
- : undefined;
127
+ return resumable({
128
+ handler: "collection",
129
+ family: "Set",
130
+ operation: entry.detail,
131
+ });
92
132
  case "MathIntrinsic":
93
- return MATH_SHIMS[detailName(entry.detail)];
133
+ return ordinary(MATH_SHIMS[entry.detail]);
94
134
  case "FloatMethod":
95
- return FLOAT_METHOD_SHIMS[detailName(entry.detail)];
135
+ return ordinary(FLOAT_METHOD_SHIMS[entry.detail]);
96
136
  case "IntMethod":
97
- return intMethodShim(entry.detail);
137
+ return ordinary(intMethodShim(entry.detail));
98
138
  case "IntArith":
99
- return intArithShim(entry.detail);
100
- default:
101
- return UNIT_FAMILY_SHIMS[entry.family];
139
+ return ordinary(intArithShim(entry.detail));
140
+ default: {
141
+ const registration = UNIT_FAMILIES[entry.family];
142
+ return typeof registration === "function"
143
+ ? ordinary(registration)
144
+ : registration;
145
+ }
146
+ }
147
+ }
148
+ export function resolveExtern(entry) {
149
+ if (Object.hasOwn(DIRECTORY_OPERATIONS, entry.symbol))
150
+ return {
151
+ kind: "directory",
152
+ operation: DIRECTORY_OPERATIONS[entry.symbol],
153
+ };
154
+ const registration = Object.hasOwn(EXTERN_SHIMS, entry.symbol)
155
+ ? EXTERN_SHIMS[entry.symbol]
156
+ : undefined;
157
+ if (typeof registration === "function")
158
+ return ordinary(registration);
159
+ return (registration ?? {
160
+ ...refused(entry.symbol, entry.native_capability != null),
161
+ description: entry.native_capability ?? undefined,
162
+ });
163
+ }
164
+ const SUSPENSION_ROUTES = {
165
+ GeneratorNext: "GeneratorNext",
166
+ Yield: "Yield",
167
+ Sleep: "Sleep",
168
+ Safepoint: "Safepoint",
169
+ Ask: "Ask",
170
+ Await: "Await",
171
+ Join: "Join",
172
+ Select: "Select",
173
+ StreamSend: "StreamSend",
174
+ StreamNext: "StreamNext",
175
+ NativeIo: "NativeIo",
176
+ Read: refused("Read", true),
177
+ Accept: refused("Accept", true),
178
+ SleepUntil: refused("SleepUntil"),
179
+ ServiceAcquire: refused("ServiceAcquire"),
180
+ SemaphoreAcquire: refused("SemaphoreAcquire"),
181
+ ActorSend: refused("ActorSend"),
182
+ RestartWait: refused("RestartWait"),
183
+ CallClosure: refused("CallClosure"),
184
+ Timeout: refused("Timeout"),
185
+ ScopeDeadline: refused("ScopeDeadline"),
186
+ Offload: refused("Offload"),
187
+ };
188
+ export function suspensionRefusal(kind) {
189
+ const route = SUSPENSION_ROUTES[kind];
190
+ return typeof route === "string" ? null : route;
191
+ }
192
+ function executableSuspension(term) {
193
+ return suspensionRefusal(term.kind) === null;
194
+ }
195
+ const NATIVE_IO_ROUTES = {
196
+ StdinReadLine: "StdinReadLine",
197
+ SinkFinish: "SinkFinish",
198
+ TcpRead: refused("NativeIo::TcpRead", true),
199
+ TcpWrite: refused("NativeIo::TcpWrite", true),
200
+ TcpAccept: refused("NativeIo::TcpAccept", true),
201
+ TcpConnect: refused("NativeIo::TcpConnect", true),
202
+ TcpConnectTimeout: refused("NativeIo::TcpConnectTimeout", true),
203
+ TlsConnect: refused("NativeIo::TlsConnect", true),
204
+ TlsRead: refused("NativeIo::TlsRead", true),
205
+ TlsWrite: refused("NativeIo::TlsWrite", true),
206
+ FileRead: "FileRead",
207
+ FileWrite: "FileWrite",
208
+ };
209
+ function nativeIoRefusal(operation) {
210
+ const route = NATIVE_IO_ROUTES[operation];
211
+ return typeof route === "string" ? null : route;
212
+ }
213
+ function executableNativeIo(term) {
214
+ return nativeIoRefusal(term.detail.operation) === null;
215
+ }
216
+ /// The executor handles exactly the resumable identities registered above.
217
+ export function resolveSuspension(term) {
218
+ if (!executableSuspension(term))
219
+ return suspensionRefusal(term.kind);
220
+ if (term.kind === "NativeIo") {
221
+ if (!executableNativeIo(term))
222
+ return nativeIoRefusal(term.detail.operation);
223
+ return { kind: "resumable", term };
102
224
  }
225
+ return { kind: "resumable", term };
103
226
  }
104
- export function resolveExternShim(symbol) {
105
- return EXTERN_SHIMS[symbol];
106
- }
107
- /// The suspend kinds a sequential package may carry. `NativeIo` is admitted
108
- /// per operation through [`SUPPORTED_NATIVE_IO`] instead.
109
- export const SUPPORTED_SUSPEND_KINDS = new Set([
110
- "GeneratorNext",
111
- "Yield",
112
- "Sleep",
113
- "Ask",
114
- "Await",
115
- "Join",
116
- "Select",
117
- "StreamSend",
118
- "StreamNext",
119
- ]);
120
- /// The VM serves replay standard input and in-memory sink closure.
121
- export const SUPPORTED_NATIVE_IO = new Set([
122
- "StdinReadLine",
123
- "SinkFinish",
124
- ]);
125
227
  // ── families ────────────────────────────────────────────────────────────────
126
228
  /// `Print { kind, newline }`. The rendering follows the value's own
127
229
  /// representation: casts normalize an integer into its target range, so the
128
230
  /// declared `kind` never changes how a value prints.
129
231
  function printShim(detail) {
130
- if (typeof detail !== "object" || detail === null) {
131
- return undefined;
132
- }
133
- const newline = detail.newline === true;
232
+ const newline = detail.newline;
134
233
  return (host, args) => {
135
234
  host.writeStdout(newline ? `${renderStdout(arg(args, 0))}\n` : renderStdout(arg(args, 0)));
136
235
  return UNIT;
137
236
  };
138
237
  }
139
- const UNIT_FAMILY_SHIMS = {
238
+ const UNIT_FAMILIES = {
239
+ TimestampNow: (host, _args, _variant, aggregate) => {
240
+ const millis = host.unixTimeMillis();
241
+ if (!Number.isFinite(millis))
242
+ throw new ShimFault("integer_overflow", "host wall clock is not finite");
243
+ const seconds = Math.floor(millis / 1000);
244
+ let nanos = Math.round((millis - seconds * 1000) * 1_000_000);
245
+ let whole = BigInt(seconds);
246
+ if (nanos === 1_000_000_000) {
247
+ whole += 1n;
248
+ nanos = 0;
249
+ }
250
+ if (whole < -(1n << 63n) ||
251
+ whole >= 1n << 63n ||
252
+ nanos < 0 ||
253
+ nanos >= 1_000_000_000)
254
+ throw new ShimFault("integer_overflow", "host wall clock exceeds timestamp range");
255
+ if (!aggregate || aggregate.builtin !== "Timestamp")
256
+ throw new TypeError("timestamp clock lacks its aggregate descriptor");
257
+ return {
258
+ kind: "record",
259
+ typeId: aggregate.name,
260
+ shape: aggregate.id,
261
+ fields: [int(whole), int(BigInt(nanos))],
262
+ };
263
+ },
140
264
  StderrWrite: (host, args) => {
141
265
  host.writeStderr(text(args, 0));
142
266
  return UNIT;
@@ -144,51 +268,104 @@ const UNIT_FAMILY_SHIMS = {
144
268
  StringToBytes: (_host, args) => ({
145
269
  kind: "vector",
146
270
  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(),
271
+ items: [...indexed(args, 0).encoded()].map((byte) => int(BigInt(byte))),
156
272
  }),
273
+ StringToUppercase: (_host, args) => str(text(args, 0).toUpperCase()),
274
+ StringToLowercase: (_host, args) => str(text(args, 0).toLowerCase()),
157
275
  StreamPairSink: (host, args) => host.pipes.extract(arg(args, 0), "sink"),
158
276
  StreamPairStream: (host, args) => host.pipes.extract(arg(args, 0), "stream"),
159
- SinkClone: (host, args) => host.pipes.cloneSink(arg(args, 0)),
160
- SinkClose: (host, args) => {
161
- host.closePipe(arg(args, 0));
162
- return UNIT;
163
- },
164
- StreamClose: (host, args) => {
165
- host.closePipe(arg(args, 0));
166
- return UNIT;
167
- },
277
+ StreamFromGenerator: (host, args) => host.generatorStream(arg(args, 0)),
278
+ SinkFromEncoder: (host, args) => host.encoderSink(arg(args, 0), arg(args, 1)),
279
+ SinkClone: (host, args) => host.cloneSink(arg(args, 0)),
280
+ SinkClose: resumable({ handler: "close" }),
281
+ StreamClose: resumable({ handler: "close" }),
168
282
  StringConcat: (_host, args) => str(`${text(args, 0)}${text(args, 1)}`),
169
- StringClone: (_host, args) => str(text(args, 0)),
283
+ StringClone: (_host, args) => cloneValue(arg(args, 0)),
170
284
  StringEquals: (_host, args) => bool(text(args, 0) === text(args, 1)),
171
285
  StringCompare: (_host, args) => int(BigInt(compareUtf8(text(args, 0), text(args, 1)))),
172
- StringContains: (_host, args) => bool(text(args, 0).includes(text(args, 1))),
286
+ StringStartsWith: (_host, args) => bool(text(args, 0).startsWith(text(args, 1))),
287
+ StringEndsWith: (_host, args) => bool(text(args, 0).endsWith(text(args, 1))),
288
+ StringContains: (_host, args) => bool(indexed(args, 0).find(indexed(args, 1)) !== undefined),
173
289
  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)),
290
+ StringLen: (_host, args) => int(BigInt(indexed(args, 0).scalarLength)),
291
+ StringByteLen: (_host, args) => int(BigInt(indexed(args, 0).byteLength)),
176
292
  StringIndex: (_host, args) => {
177
- const chars = [...text(args, 0)];
293
+ const value = indexed(args, 0);
178
294
  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]);
295
+ const scalar = value.at(at);
296
+ if (scalar === undefined)
297
+ throw new ShimFault("vector_bounds", `string index ${at} out of bounds for length ${value.scalarLength}`);
298
+ return str(scalar);
299
+ },
300
+ StringGet: (host, args, shape) => {
301
+ if (!shape)
302
+ throw new TypeError("string get requires its Option shape");
303
+ const scalar = indexed(args, 0).at(index(args, 1));
304
+ return scalar === undefined
305
+ ? host.enumValue(shape, "OptionNone", [])
306
+ : host.enumValue(shape, "OptionSome", [str(scalar)]);
307
+ },
308
+ StringFind: (host, args, shape) => {
309
+ if (!shape)
310
+ throw new TypeError("string find requires its Option shape");
311
+ const found = indexed(args, 0).find(indexed(args, 1));
312
+ return found === undefined
313
+ ? host.enumValue(shape, "OptionNone", [])
314
+ : host.enumValue(shape, "OptionSome", [int(BigInt(found))]);
315
+ },
316
+ StringFindFrom: (host, args, shape) => {
317
+ if (!shape)
318
+ throw new TypeError("string find_from requires its Option shape");
319
+ const found = indexed(args, 0).find(indexed(args, 1), index(args, 2));
320
+ return found === undefined
321
+ ? host.enumValue(shape, "OptionNone", [])
322
+ : host.enumValue(shape, "OptionSome", [int(BigInt(found))]);
323
+ },
324
+ StringAppend: (_host, args) => indexed(args, 0).append(indexed(args, 1)),
325
+ StringPush: (_host, args) => indexed(args, 0).append(indexed(args, 1)),
326
+ StringClear: (_host, args) => {
327
+ indexed(args, 0).release();
328
+ return str("");
329
+ },
330
+ StringIsEmpty: (_host, args) => bool(indexed(args, 0).scalarLength === 0),
331
+ StringSplit: (_host, args) => {
332
+ const value = indexed(args, 0).value;
333
+ const separator = indexed(args, 1).value;
334
+ const parts = separator === "" ? Array.from(value) : value.split(separator);
335
+ return { kind: "vector", elementType: "string", items: parts.map(str) };
183
336
  },
184
- StringIsEmpty: (_host, args) => bool(text(args, 0).length === 0),
185
337
  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(""));
338
+ StringRepeat: (_host, args) => str(text(args, 0).repeat(Math.max(0, index(args, 1)))),
339
+ StringGetRange: (host, args, shape) => {
340
+ if (!shape)
341
+ throw new TypeError("string range get requires its Option shape");
342
+ const value = stringRange(args);
343
+ return value === undefined
344
+ ? host.enumValue(shape, "OptionNone", [])
345
+ : host.enumValue(shape, "OptionSome", [value]);
346
+ },
347
+ StringSliceRange: (_host, args) => {
348
+ const value = stringRange(args);
349
+ if (value === undefined)
350
+ throw new ShimFault("vector_bounds", "string range out of bounds");
351
+ return value;
352
+ },
353
+ StringSliceCodepoints: (_host, args) => {
354
+ const value = indexed(args, 0);
355
+ const start = index(args, 1);
356
+ const end = index(args, 2);
357
+ const sliced = value.slice(start, end);
358
+ if (!sliced)
359
+ throw new ShimFault("vector_bounds", `string slice ${start}..${end} out of bounds for length ${value.scalarLength}`);
360
+ return sliced;
361
+ },
362
+ StringSliceCodepointsFrom: (_host, args) => {
363
+ const value = indexed(args, 0);
364
+ const start = index(args, 1);
365
+ const sliced = value.slice(start, value.scalarLength);
366
+ if (!sliced)
367
+ throw new ShimFault("vector_bounds", `string slice ${start}.. out of bounds for length ${value.scalarLength}`);
368
+ return sliced;
192
369
  },
193
370
  I64ToString: (_host, args) => str(renderStdout(arg(args, 0))),
194
371
  U64ToString: (_host, args) => str(renderStdout(arg(args, 0))),
@@ -197,8 +374,39 @@ const UNIT_FAMILY_SHIMS = {
197
374
  U8ToString: (_host, args) => str(renderStdout(arg(args, 0))),
198
375
  BoolToString: (_host, args) => str(renderStdout(arg(args, 0))),
199
376
  F64ToString: (_host, args) => str(renderStdout(arg(args, 0))),
377
+ CharIsDigit: (_host, args) => bool(charProperty(text(args, 0), 0)),
378
+ CharIsLetter: (_host, args) => bool(charProperty(text(args, 0), 1)),
379
+ CharIsAlphanumeric: (_host, args) => bool(charProperty(text(args, 0), 2)),
380
+ CharIsWhitespace: (_host, args) => bool(charProperty(text(args, 0), 3)),
381
+ CharIsUpper: (_host, args) => bool(charProperty(text(args, 0), 4)),
382
+ CharIsLower: (_host, args) => bool(charProperty(text(args, 0), 5)),
383
+ CharIsPunctuation: (_host, args) => bool(charProperty(text(args, 0), 6)),
384
+ CharIsControl: (_host, args) => bool(charProperty(text(args, 0), 7)),
385
+ CharIsAscii: (_host, args) => bool(charProperty(text(args, 0), 8)),
386
+ CharLenUtf8: (_host, args) => int(BigInt(indexed(args, 0).byteLength)),
387
+ CharToUpper: (_host, args) => str(charCase(text(args, 0), "upper")),
388
+ CharToLower: (_host, args) => str(charCase(text(args, 0), "lower")),
389
+ CharToTitle: (_host, args) => str(charCase(text(args, 0), "title")),
390
+ CharToDigit: (host, args, shape) => {
391
+ if (!shape)
392
+ throw new TypeError("char to_digit requires its Option shape");
393
+ const radix = index(args, 1);
394
+ const scalar = text(args, 0).codePointAt(0);
395
+ const digit = scalar >= 48 && scalar <= 57
396
+ ? scalar - 48
397
+ : scalar >= 65 && scalar <= 90
398
+ ? scalar - 65 + 10
399
+ : scalar >= 97 && scalar <= 122
400
+ ? scalar - 97 + 10
401
+ : -1;
402
+ return radix < 2 || radix > 36 || digit < 0 || digit >= radix
403
+ ? host.enumValue(shape, "OptionNone", [])
404
+ : host.enumValue(shape, "OptionSome", [int(BigInt(digit))]);
405
+ },
406
+ CharFromI64: checkedChar,
407
+ CharFromU64: checkedChar,
200
408
  CharToString: (_host, args) => str(renderStdout(arg(args, 0))),
201
- BytesDecodeUtf8: (_host, args) => str(decodeUtf8(arg(args, 0))),
409
+ BytesDecodeUtf8: resumable({ handler: "utf8_decode" }),
202
410
  // TextDecoder replaces each maximal invalid subsequence with U+FFFD, as
203
411
  // native lossy decoding does.
204
412
  BytesDecodeUtf8Lossy: (_host, args) => str(decodeUtf8(arg(args, 0))),
@@ -293,6 +501,165 @@ const UNIT_FAMILY_SHIMS = {
293
501
  ],
294
502
  };
295
503
  },
504
+ ActorDemonitor: refused("ActorDemonitor"),
505
+ ActorLink: refused("ActorLink"),
506
+ ActorMonitor: refused("ActorMonitor"),
507
+ ActorCaller: resumable({ handler: "actor_caller" }),
508
+ ActorRefusalLog: resumable({ handler: "actor_refusal_log" }),
509
+ ActorUnlink: refused("ActorUnlink"),
510
+ AutoMutexAlloc: refused("AutoMutexAlloc"),
511
+ AutoMutexFree: refused("AutoMutexFree"),
512
+ AutoMutexLock: refused("AutoMutexLock"),
513
+ AutoMutexUnlock: refused("AutoMutexUnlock"),
514
+ DequePopFront: refused("DequePopFront"),
515
+ DequePopBack: refused("DequePopBack"),
516
+ CancelTokenIsRequested: refused("CancelTokenIsRequested"),
517
+ CancelTokenRelease: refused("CancelTokenRelease"),
518
+ CancelTokenRetain: refused("CancelTokenRetain"),
519
+ ActorRequestRelease: refused("ActorRequestRelease"),
520
+ ActorCallFree: refused("ActorCallFree"),
521
+ ActorRequestTake: refused("ActorRequestTake"),
522
+ DurationAbs: refused("DurationAbs"),
523
+ DurationHours: refused("DurationHours"),
524
+ DurationIsZero: refused("DurationIsZero"),
525
+ DurationMicros: refused("DurationMicros"),
526
+ DurationMillis: refused("DurationMillis"),
527
+ DurationMins: refused("DurationMins"),
528
+ DurationNanos: refused("DurationNanos"),
529
+ DurationSecs: refused("DurationSecs"),
530
+ DynBoxAlloc: refused("DynBoxAlloc"),
531
+ DynBoxFree: refused("DynBoxFree"),
532
+ HashMapContainsKeyLayout: refused("HashMapContainsKeyLayout"),
533
+ HashMapClearLayout: refused("HashMapClearLayout"),
534
+ HashMapCloneLayout: refused("HashMapCloneLayout"),
535
+ HashMapEntriesLayout: refused("HashMapEntriesLayout"),
536
+ HashMapFreeLayout: refused("HashMapFreeLayout"),
537
+ HashMapGetLayout: refused("HashMapGetLayout"),
538
+ HashMapInsertLayout: refused("HashMapInsertLayout"),
539
+ HashMapKeysLayout: refused("HashMapKeysLayout"),
540
+ HashMapLenLayout: refused("HashMapLenLayout"),
541
+ HashMapNew: refused("HashMapNew"),
542
+ HashMapNewWithLayout: refused("HashMapNewWithLayout"),
543
+ HashMapRemoveLayout: refused("HashMapRemoveLayout"),
544
+ HashMapValuesLayout: refused("HashMapValuesLayout"),
545
+ HashSetContainsLayout: refused("HashSetContainsLayout"),
546
+ HashSetClearLayout: refused("HashSetClearLayout"),
547
+ HashSetCloneLayout: refused("HashSetCloneLayout"),
548
+ HashSetFreeLayout: refused("HashSetFreeLayout"),
549
+ HashSetInsertLayout: refused("HashSetInsertLayout"),
550
+ HashSetIsEmptyLayout: refused("HashSetIsEmptyLayout"),
551
+ HashSetLenLayout: refused("HashSetLenLayout"),
552
+ HashSetNew: refused("HashSetNew"),
553
+ HashSetNewWithLayout: refused("HashSetNewWithLayout"),
554
+ HashSetRemoveLayout: refused("HashSetRemoveLayout"),
555
+ HashSetToVecLayout: refused("HashSetToVecLayout"),
556
+ InstantDurationSince: refused("InstantDurationSince"),
557
+ InstantElapsed: refused("InstantElapsed"),
558
+ InstantNow: refused("InstantNow"),
559
+ NodeAllowPeer: refused("NodeAllowPeer"),
560
+ NodeConnect: refused("NodeConnect"),
561
+ NodeId: refused("NodeId"),
562
+ NodeIdDisplay: refused("NodeIdDisplay"),
563
+ LocationNodeId: refused("LocationNodeId"),
564
+ LocationIncarnation: refused("LocationIncarnation"),
565
+ LocationDisplay: refused("LocationDisplay"),
566
+ NodeIdentityKey: refused("NodeIdentityKey"),
567
+ NodeLoadKeys: refused("NodeLoadKeys"),
568
+ NodeLookup: refused("NodeLookup"),
569
+ NodeRegister: refused("NodeRegister"),
570
+ NodeSetTransport: refused("NodeSetTransport"),
571
+ NodeShutdown: refused("NodeShutdown"),
572
+ NodeStart: refused("NodeStart"),
573
+ MetricCounterRegister: refused("MetricCounterRegister"),
574
+ MetricCounterInc: refused("MetricCounterInc"),
575
+ MetricCounterAdd: refused("MetricCounterAdd"),
576
+ MetricGaugeRegister: refused("MetricGaugeRegister"),
577
+ MetricGaugeSet: refused("MetricGaugeSet"),
578
+ MetricGaugeInc: refused("MetricGaugeInc"),
579
+ MetricGaugeDec: refused("MetricGaugeDec"),
580
+ MetricGaugeAdd: refused("MetricGaugeAdd"),
581
+ MetricHistogramRegister: refused("MetricHistogramRegister"),
582
+ MetricHistogramRegisterSimple: refused("MetricHistogramRegisterSimple"),
583
+ MetricHistogramRecord: refused("MetricHistogramRecord"),
584
+ MetricVecRegister: refused("MetricVecRegister"),
585
+ MetricVecWith: refused("MetricVecWith"),
586
+ ObserveReadU64: refused("ObserveReadU64"),
587
+ ObserveScrape: refused("ObserveScrape"),
588
+ ObserveSeries: refused("ObserveSeries"),
589
+ ObserveBarrier: refused("ObserveBarrier"),
590
+ RcClone: resumable({ handler: "shared", operation: "RcClone" }),
591
+ RcDowngrade: resumable({ handler: "shared", operation: "RcDowngrade" }),
592
+ RcDrop: refused("RcDrop"),
593
+ RcGet: resumable({ handler: "shared", operation: "RcGet" }),
594
+ RcIsUnique: resumable({ handler: "shared", operation: "RcIsUnique" }),
595
+ RcNew: resumable({ handler: "shared", operation: "RcNew" }),
596
+ RcSet: resumable({ handler: "shared", operation: "RcSet" }),
597
+ RcStrongCount: resumable({ handler: "shared", operation: "RcStrongCount" }),
598
+ RcWeakCount: resumable({ handler: "shared", operation: "RcWeakCount" }),
599
+ WeakCloneRc: resumable({ handler: "shared", operation: "WeakCloneRc" }),
600
+ WeakDropRc: refused("WeakDropRc"),
601
+ WeakUpgradeRc: resumable({ handler: "shared", operation: "WeakUpgradeRc" }),
602
+ RegexCapture: refused("RegexCapture"),
603
+ RegexCompile: refused("RegexCompile"),
604
+ RegexFreeCapture: refused("RegexFreeCapture"),
605
+ RegexHandle: refused("RegexHandle"),
606
+ RegexMatch: refused("RegexMatch"),
607
+ ReplyChannelCancel: refused("ReplyChannelCancel"),
608
+ ReplyChannelFree: refused("ReplyChannelFree"),
609
+ ReplyChannelNew: refused("ReplyChannelNew"),
610
+ ReplyPayloadFree: refused("ReplyPayloadFree"),
611
+ ReplyWait: refused("ReplyWait"),
612
+ SelectFirst: refused("SelectFirst"),
613
+ StreamChunks: refused("StreamChunks"),
614
+ StreamForward: refused("StreamForward"),
615
+ StreamLines: refused("StreamLines"),
616
+ StreamNextLayout: refused("StreamNextLayout"),
617
+ StreamSendLayout: refused("StreamSendLayout"),
618
+ StreamTake: refused("StreamTake"),
619
+ StreamTryNextLayout: refused("StreamTryNextLayout"),
620
+ StreamTrySendLayout: refused("StreamTrySendLayout"),
621
+ StructuralFormat: resumable({ handler: "structural_format" }),
622
+ StringLines: refused("StringLines"),
623
+ ProcessExit: refused("ProcessExit"),
624
+ SupervisorDirectId: refused("SupervisorDirectId"),
625
+ SupervisorChildGet: refused("SupervisorChildGet"),
626
+ LocalPidSupervisorChildGet: refused("LocalPidSupervisorChildGet"),
627
+ SupervisorNestedGet: refused("SupervisorNestedGet"),
628
+ SupervisorPoolChildGet: refused("SupervisorPoolChildGet"),
629
+ LocalPidSupervisorPoolChildRefGet: refused("LocalPidSupervisorPoolChildRefGet"),
630
+ SupervisorPoolLen: refused("SupervisorPoolLen"),
631
+ SupervisorStop: refused("SupervisorStop"),
632
+ TlsAttachLocal: refused("TlsAttachLocal"),
633
+ WebSocketAttachLocal: refused("WebSocketAttachLocal"),
634
+ TaskFree: refused("TaskFree"),
635
+ GeneratorFree: refused("GeneratorFree"),
636
+ VecAppend: refused("VecAppend"),
637
+ VecClear: refused("VecClear"),
638
+ VecClone: refused("VecClone"),
639
+ VecCloneLayout: refused("VecCloneLayout"),
640
+ VecCloneOwned: refused("VecCloneOwned"),
641
+ VecContainsLayout: refused("VecContainsLayout"),
642
+ VecTakeAll: refused("VecTakeAll"),
643
+ VecContainsOwned: refused("VecContainsOwned"),
644
+ VecIsEmpty: refused("VecIsEmpty"),
645
+ VecJoinStr: refused("VecJoinStr"),
646
+ VecLen: refused("VecLen"),
647
+ VecNew: refused("VecNew"),
648
+ VecPopBool: refused("VecPopBool"),
649
+ VecPopLayout: refused("VecPopLayout"),
650
+ VecPopOwned: refused("VecPopOwned"),
651
+ VecPushBool: refused("VecPushBool"),
652
+ VecPushLayout: refused("VecPushLayout"),
653
+ VecPushOwned: refused("VecPushOwned"),
654
+ VecPushOwnedMove: refused("VecPushOwnedMove"),
655
+ VecRemoveAtBool: refused("VecRemoveAtBool"),
656
+ VecRemoveAtLayout: refused("VecRemoveAtLayout"),
657
+ VecRemoveAtOwned: refused("VecRemoveAtOwned"),
658
+ VecSetBool: refused("VecSetBool"),
659
+ VecSetLayout: refused("VecSetLayout"),
660
+ VecSetOwned: refused("VecSetOwned"),
661
+ VecSetOwnedMove: refused("VecSetOwnedMove"),
662
+ VtableDispatchPanicOnOob: refused("VtableDispatchPanicOnOob"),
296
663
  };
297
664
  const VECTOR_SHIMS = {
298
665
  New: () => ({ kind: "vector", elementType: "", items: [] }),
@@ -326,6 +693,12 @@ const VECTOR_SHIMS = {
326
693
  Index: (_host, args) => cloneValue(sequenceElement(args)),
327
694
  IndexBorrow: (_host, args) => sequenceElement(args),
328
695
  Len: (_host, args) => int(BigInt(vec(args, 0).items.length)),
696
+ Join: (_host, args) => {
697
+ const items = vec(args, 0).items;
698
+ return str(items
699
+ .map((_, at) => indexed(items, at).value)
700
+ .join(indexed(args, 1).value));
701
+ },
329
702
  Slice: (_host, args) => {
330
703
  const items = vec(args, 0).items;
331
704
  const start = index(args, 1);
@@ -495,8 +868,6 @@ const FLOAT_METHOD_SHIMS = {
495
868
  },
496
869
  IsSignNegative: (_host, args) => bool(toBits(real(args, 0)) >> 63n === 1n),
497
870
  };
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
871
  const INT_WIDTHS = {
501
872
  I8: { bits: 8, signed: true },
502
873
  I16: { bits: 16, signed: true },
@@ -509,15 +880,6 @@ const INT_WIDTHS = {
509
880
  U64: { bits: 64, signed: false },
510
881
  Usize: { bits: 64, signed: false },
511
882
  };
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
883
  function wrapTo(width, value) {
522
884
  return width.signed
523
885
  ? BigInt.asIntN(width.bits, value)
@@ -541,11 +903,8 @@ function popcount(value) {
541
903
  /// works on the receiver's unsigned bit pattern and the rotate amount is taken
542
904
  /// modulo the width.
543
905
  function intMethodShim(detail) {
544
- const parsed = intDetail(detail);
545
- if (!parsed) {
546
- return undefined;
547
- }
548
- const [op, width] = parsed;
906
+ const [op, name] = detail;
907
+ const width = INT_WIDTHS[name];
549
908
  const bits = BigInt(width.bits);
550
909
  const unsigned = (args) => BigInt.asUintN(width.bits, integer(args, 0));
551
910
  const result = (pattern) => int(wrapTo(width, pattern));
@@ -602,30 +961,34 @@ function intMethodShim(detail) {
602
961
  return (_host, args) => rotate(args, true);
603
962
  case "RotateRight":
604
963
  return (_host, args) => rotate(args, false);
605
- default:
606
- return undefined;
964
+ default: {
965
+ const exhaustive = op;
966
+ throw new Error(`unknown integer operation ${String(exhaustive)}`);
967
+ }
607
968
  }
608
969
  }
609
970
  /// Non-trapping integer arithmetic at the receiver's width: wrapping keeps
610
971
  /// the low bits, saturating clamps to the width's bounds and checked answers
611
972
  /// `None` where native's overflow flag is set.
612
973
  function intArithShim(detail) {
613
- const parsed = intDetail(detail);
614
- if (!parsed) {
615
- return undefined;
616
- }
617
- const [kind, width] = parsed;
974
+ const [kind, name] = detail;
975
+ const width = INT_WIDTHS[name];
618
976
  const [min, max] = intBounds(width);
977
+ const add = (x, y) => x + y;
978
+ const sub = (x, y) => x - y;
979
+ const mul = (x, y) => x * y;
619
980
  const operations = {
620
- Add: (x, y) => x + y,
621
- Sub: (x, y) => x - y,
622
- Mul: (x, y) => x * y,
981
+ WrappingAdd: ["Wrapping", add],
982
+ WrappingSub: ["Wrapping", sub],
983
+ WrappingMul: ["Wrapping", mul],
984
+ SaturatingAdd: ["Saturating", add],
985
+ SaturatingSub: ["Saturating", sub],
986
+ SaturatingMul: ["Saturating", mul],
987
+ CheckedAdd: ["Checked", add],
988
+ CheckedSub: ["Checked", sub],
989
+ CheckedMul: ["Checked", mul],
623
990
  };
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
- }
991
+ const [mode, apply] = operations[kind];
629
992
  const exact = (args) => apply(wrapTo(width, integer(args, 0)), wrapTo(width, integer(args, 1)));
630
993
  switch (mode) {
631
994
  case "Wrapping":
@@ -665,6 +1028,45 @@ function bigMax(left, right) {
665
1028
  }
666
1029
  // ── externs ─────────────────────────────────────────────────────────────────
667
1030
  const EXTERN_SHIMS = {
1031
+ hew_path_separator: () => str("/"),
1032
+ hew_calendar_format: {
1033
+ kind: "ordinary",
1034
+ bridge: "calendarBridge",
1035
+ shim: (host, args, _variant, shape) => executeCalendar(host.calendarBridge, "hew_calendar_format", args, shape),
1036
+ },
1037
+ hew_calendar_parse: {
1038
+ kind: "ordinary",
1039
+ bridge: "calendarBridge",
1040
+ shim: (host, args, _variant, shape) => executeCalendar(host.calendarBridge, "hew_calendar_parse", args, shape),
1041
+ },
1042
+ hew_calendar_components: {
1043
+ kind: "ordinary",
1044
+ bridge: "calendarBridge",
1045
+ shim: (host, args, _variant, shape) => executeCalendar(host.calendarBridge, "hew_calendar_components", args, shape),
1046
+ },
1047
+ hew_calendar_from_parts: {
1048
+ kind: "ordinary",
1049
+ bridge: "calendarBridge",
1050
+ shim: (host, args, _variant, shape) => executeCalendar(host.calendarBridge, "hew_calendar_from_parts", args, shape),
1051
+ },
1052
+ hew_args_count: (host) => int(BigInt(host.argv.length)),
1053
+ // A native null managed string is canonical empty. Each result owns fresh
1054
+ // string storage; releasing it cannot change the host's supplied arguments.
1055
+ hew_args_get: (host, args) => str(host.argv[index(args, 0)] ?? ""),
1056
+ hew_actor_timer_cancel: (_host, args) => {
1057
+ const timer = arg(args, 0);
1058
+ if (timer.kind !== "timer" || timer.closed)
1059
+ throw new Error("invalid timer handle");
1060
+ timer.cancel();
1061
+ return UNIT;
1062
+ },
1063
+ hew_actor_timer_release: (_host, args) => {
1064
+ const timer = arg(args, 0);
1065
+ if (timer.kind !== "timer" || timer.closed)
1066
+ throw new Error("invalid timer handle");
1067
+ timer.closed = true;
1068
+ return UNIT;
1069
+ },
668
1070
  hew_msg_envelope_release: (host, args) => {
669
1071
  host.releaseValue(arg(args, 0));
670
1072
  return UNIT;
@@ -675,7 +1077,7 @@ const EXTERN_SHIMS = {
675
1077
  host.pipes.freePair(arg(args, 0));
676
1078
  return UNIT;
677
1079
  },
678
- hew_stream_last_error: () => ({ kind: "string", value: "" }),
1080
+ hew_stream_last_error: () => str(""),
679
1081
  hew_regex_new: (host, args) => compileRegex(host, args),
680
1082
  hew_regex_clone: (_host, args) => cloneValue(arg(args, 0)),
681
1083
  // Releasing a handle the VM traces by reference is nothing to do.
@@ -721,6 +1123,47 @@ const EXTERN_SHIMS = {
721
1123
  return int(high <= low ? low : low + host.prng.randbelow(high - low));
722
1124
  },
723
1125
  };
1126
+ export function decodeUtf8Checked(value) {
1127
+ if (value.kind !== "vector")
1128
+ throw new TypeError("UTF-8 input must be bytes");
1129
+ const bytes = Uint8Array.from(value.items.map((item) => Number(integer([item], 0))));
1130
+ let at = 0;
1131
+ while (at < bytes.length) {
1132
+ const first = bytes[at];
1133
+ if (first < 128) {
1134
+ at++;
1135
+ continue;
1136
+ }
1137
+ const width = first >= 194 && first <= 223
1138
+ ? 2
1139
+ : first >= 224 && first <= 239
1140
+ ? 3
1141
+ : first >= 240 && first <= 244
1142
+ ? 4
1143
+ : 0;
1144
+ if (width === 0)
1145
+ return { validUpTo: at, errorLen: 1 };
1146
+ for (let offset = 1; offset < width; offset++) {
1147
+ if (at + offset >= bytes.length)
1148
+ return { validUpTo: at, errorLen: null };
1149
+ const next = bytes[at + offset];
1150
+ const lower = offset === 1 && first === 224
1151
+ ? 160
1152
+ : offset === 1 && first === 240
1153
+ ? 144
1154
+ : 128;
1155
+ const upper = offset === 1 && first === 237
1156
+ ? 159
1157
+ : offset === 1 && first === 244
1158
+ ? 143
1159
+ : 191;
1160
+ if (next < lower || next > upper)
1161
+ return { validUpTo: at, errorLen: offset };
1162
+ }
1163
+ at += width;
1164
+ }
1165
+ return { value: new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes) };
1166
+ }
724
1167
  /// `bytes` is a vector of byte values, so decoding walks its items.
725
1168
  function decodeUtf8(value) {
726
1169
  if (value.kind === "string") {
@@ -744,12 +1187,15 @@ function pattern(host, value) {
744
1187
  return value.kind === "string" ? value.value : renderStdout(value);
745
1188
  }
746
1189
  // ── operand helpers ─────────────────────────────────────────────────────────
747
- function detailName(detail) {
748
- return typeof detail === "string" ? detail : "";
749
- }
750
1190
  function arg(args, at) {
751
1191
  return args[at] ?? UNIT;
752
1192
  }
1193
+ function indexed(args, at) {
1194
+ const value = arg(args, at);
1195
+ if (value.kind !== "string")
1196
+ throw new TypeError(`runtime call expected a string operand, got ${value.kind}`);
1197
+ return value;
1198
+ }
753
1199
  function text(args, at) {
754
1200
  const value = arg(args, at);
755
1201
  return value.kind === "string" ? value.value : renderStdout(value);
@@ -779,7 +1225,7 @@ function comparable(value) {
779
1225
  return `${value.kind}:${renderStdout(value)}`;
780
1226
  }
781
1227
  function str(value) {
782
- return { kind: "string", value };
1228
+ return stringValue(value);
783
1229
  }
784
1230
  function bool(value) {
785
1231
  return { kind: "bool", value };
@@ -787,3 +1233,29 @@ function bool(value) {
787
1233
  function int(value) {
788
1234
  return { kind: "i64", value };
789
1235
  }
1236
+ function checkedChar(host, args, shape) {
1237
+ if (!shape)
1238
+ throw new TypeError("try_to_char requires its Option shape");
1239
+ const value = integer(args, 0);
1240
+ return value < 0n ||
1241
+ value > 0x10ffffn ||
1242
+ (value >= 0xd800n && value <= 0xdfffn)
1243
+ ? host.enumValue(shape, "OptionNone", [])
1244
+ : host.enumValue(shape, "OptionSome", [
1245
+ str(String.fromCodePoint(Number(value))),
1246
+ ]);
1247
+ }
1248
+ function stringRange(args) {
1249
+ const value = indexed(args, 0);
1250
+ const inclusive = arg(args, 3);
1251
+ const openEnd = arg(args, 4);
1252
+ if (inclusive.kind !== "bool" || openEnd.kind !== "bool")
1253
+ throw new TypeError("string range flags must be bool");
1254
+ const start = integer(args, 1);
1255
+ const end = openEnd.value
1256
+ ? BigInt(value.scalarLength)
1257
+ : integer(args, 2) + (inclusive.value ? 1n : 0n);
1258
+ if (start < 0n || end < start || end > BigInt(value.scalarLength))
1259
+ return undefined;
1260
+ return value.slice(Number(start), Number(end));
1261
+ }