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