@hew-lang/sandbox-vm 0.6.0-rc7 → 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/README.md +6 -0
- package/cli/test-runner.mjs +84 -0
- package/dist/interpreter/parity-runner.js +9 -0
- package/dist/interpreter/run-program.d.ts +3 -2
- package/dist/interpreter/run-program.js +6 -4
- package/dist/interpreter/trace.d.ts +1 -0
- package/dist/interpreter/trace.js +4 -0
- package/dist/interpreter/types.d.ts +4 -1
- package/dist/interpreter/v1/codecs.d.ts +9 -0
- package/dist/interpreter/v1/codecs.js +266 -0
- package/dist/interpreter/v1/exec.js +392 -47
- package/dist/interpreter/v1/package.d.ts +45 -12
- package/dist/interpreter/v1/shims.js +292 -19
- package/dist/interpreter/v1/validate.js +33 -4
- package/dist/interpreter/v1/value-operations.js +94 -14
- package/dist/interpreter/values.d.ts +6 -1
- package/dist/interpreter/values.js +17 -3
- package/dist/interpreter/worker.js +22 -4
- package/package.json +8 -4
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
/// where the package says `copy_value`, releases where it says `destroy_value`,
|
|
6
6
|
/// follows the `unwind` edge the package names, and takes the failure edge a
|
|
7
7
|
/// `checked.binary` names rather than trapping on its own.
|
|
8
|
+
import { executeCodec } from "./codecs.js";
|
|
8
9
|
import { TraceBuilder, runtimeFailure } from "../trace.js";
|
|
9
10
|
import { UNIT, cloneValue, toJsonValue } from "../values.js";
|
|
10
11
|
import { runtimeTag } from "./package.js";
|
|
@@ -69,7 +70,7 @@ export function runPackageV1(pkg, options = {}) {
|
|
|
69
70
|
trace.reject(rejection);
|
|
70
71
|
return trace.finish();
|
|
71
72
|
}
|
|
72
|
-
const vm = new ExecutorV1(pkg, trace, StdinReader.fromReplay(replay.inputs), options.schedulerPolicy ?? "round_robin");
|
|
73
|
+
const vm = new ExecutorV1(pkg, trace, StdinReader.fromReplay(replay.inputs), options.schedulerPolicy ?? "round_robin", options);
|
|
73
74
|
try {
|
|
74
75
|
vm.run();
|
|
75
76
|
}
|
|
@@ -85,6 +86,7 @@ class ExecutorV1 {
|
|
|
85
86
|
pkg;
|
|
86
87
|
trace;
|
|
87
88
|
stdin;
|
|
89
|
+
options;
|
|
88
90
|
current;
|
|
89
91
|
host;
|
|
90
92
|
scheduler;
|
|
@@ -96,6 +98,7 @@ class ExecutorV1 {
|
|
|
96
98
|
generators = new Map();
|
|
97
99
|
tasks = new Map();
|
|
98
100
|
completedTasks = 0;
|
|
101
|
+
actorIdentity = 0n;
|
|
99
102
|
faultDebts = new Set();
|
|
100
103
|
nextFaultDebt = 0;
|
|
101
104
|
closingResources = new WeakSet();
|
|
@@ -103,10 +106,11 @@ class ExecutorV1 {
|
|
|
103
106
|
running = false;
|
|
104
107
|
rootComplete = false;
|
|
105
108
|
rootFault = null;
|
|
106
|
-
constructor(pkg, trace, stdin, policy) {
|
|
109
|
+
constructor(pkg, trace, stdin, policy, options) {
|
|
107
110
|
this.pkg = pkg;
|
|
108
111
|
this.trace = trace;
|
|
109
112
|
this.stdin = stdin;
|
|
113
|
+
this.options = options;
|
|
110
114
|
this.scheduler = new FrameScheduler(trace, policy);
|
|
111
115
|
this.ids = new DeterministicIds(trace.replay.seed);
|
|
112
116
|
this.host = {
|
|
@@ -171,7 +175,8 @@ class ExecutorV1 {
|
|
|
171
175
|
const act = this.current;
|
|
172
176
|
while (act.opIndex < act.block.ops.length) {
|
|
173
177
|
const op = act.block.ops[act.opIndex++];
|
|
174
|
-
|
|
178
|
+
if (op.op !== "coverage.hit")
|
|
179
|
+
this.commitStep();
|
|
175
180
|
try {
|
|
176
181
|
this.execute(act, op);
|
|
177
182
|
}
|
|
@@ -350,6 +355,11 @@ class ExecutorV1 {
|
|
|
350
355
|
// ── instructions ─────────────────────────────────────────────────────────
|
|
351
356
|
execute(act, op) {
|
|
352
357
|
switch (op.op) {
|
|
358
|
+
case "coverage.hit": {
|
|
359
|
+
const key = String(op.counter);
|
|
360
|
+
this.trace.coverage[key] = (this.trace.coverage[key] ?? 0) + 1;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
353
363
|
case "const.int":
|
|
354
364
|
this.define(act, op.dst, {
|
|
355
365
|
kind: "i64",
|
|
@@ -404,9 +414,19 @@ class ExecutorV1 {
|
|
|
404
414
|
act.places.set(op.place, { value: UNIT, valid: false });
|
|
405
415
|
return;
|
|
406
416
|
case "store.init":
|
|
407
|
-
case "store.assign":
|
|
408
417
|
writeRef(this.placeRef(act, op.place), this.read(act, op.value));
|
|
409
418
|
return;
|
|
419
|
+
case "store.assign": {
|
|
420
|
+
const ref = this.placeRef(act, op.place);
|
|
421
|
+
const replacement = this.read(act, op.value);
|
|
422
|
+
const previous = liveRef(ref) ? readRef(ref) : UNIT;
|
|
423
|
+
// Retire the old owner before invoking user close code. Publish the
|
|
424
|
+
// replacement after cleanup, including when close records a fault;
|
|
425
|
+
// the following cleanup.dispatch then drains the new owner once.
|
|
426
|
+
invalidateRef(ref);
|
|
427
|
+
this.releaseOperation(act, previous, () => writeRef(ref, replacement));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
410
430
|
case "load.copy":
|
|
411
431
|
this.define(act, op.dst, cloneValue(readRef(this.placeRef(act, op.place))));
|
|
412
432
|
return;
|
|
@@ -531,7 +551,8 @@ class ExecutorV1 {
|
|
|
531
551
|
const shape = this.closureAt(op.closure);
|
|
532
552
|
this.define(act, op.dst, {
|
|
533
553
|
kind: "closure",
|
|
534
|
-
|
|
554
|
+
entries: shape.entries,
|
|
555
|
+
specialization: null,
|
|
535
556
|
environment: {
|
|
536
557
|
kind: "record",
|
|
537
558
|
typeId: "",
|
|
@@ -540,12 +561,23 @@ class ExecutorV1 {
|
|
|
540
561
|
});
|
|
541
562
|
return;
|
|
542
563
|
}
|
|
543
|
-
case "callable.coerce":
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+
}
|
|
548
579
|
return;
|
|
580
|
+
}
|
|
549
581
|
case "dyn.make":
|
|
550
582
|
this.define(act, op.dst, {
|
|
551
583
|
kind: "dyn",
|
|
@@ -576,6 +608,9 @@ class ExecutorV1 {
|
|
|
576
608
|
const duration = this.read(act, op.duration);
|
|
577
609
|
if (duration.kind !== "i64")
|
|
578
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);
|
|
579
614
|
group.cancelTimer = this.scheduler.after(duration.value, () => {
|
|
580
615
|
group.deadline = {
|
|
581
616
|
kind: "panic",
|
|
@@ -617,6 +652,7 @@ class ExecutorV1 {
|
|
|
617
652
|
const context = {
|
|
618
653
|
id,
|
|
619
654
|
actor: act.context.actor,
|
|
655
|
+
requestDeadline: this.requestDeadline(act),
|
|
620
656
|
returned: (value) => this.closeValueAsync(value, null, (fault) => this.finishGenerator(generator, fault)),
|
|
621
657
|
failed: (fault) => this.finishGenerator(generator, fault),
|
|
622
658
|
};
|
|
@@ -634,6 +670,106 @@ class ExecutorV1 {
|
|
|
634
670
|
this.define(act, op.dst, { kind: "generator", id });
|
|
635
671
|
return;
|
|
636
672
|
}
|
|
673
|
+
case "task.race": {
|
|
674
|
+
const scope = act.scopes.get(op.scope);
|
|
675
|
+
if (!scope)
|
|
676
|
+
throw new Error("race has no task scope");
|
|
677
|
+
const members = op.members.map((operand) => {
|
|
678
|
+
const member = this.taskFor(this.read(act, operand));
|
|
679
|
+
this.invalidate(act, operand);
|
|
680
|
+
if (member.grouped)
|
|
681
|
+
throw new Error("race reuses a task handle");
|
|
682
|
+
member.grouped = true;
|
|
683
|
+
return member;
|
|
684
|
+
});
|
|
685
|
+
if (!members.length)
|
|
686
|
+
throw new Error("race requires members");
|
|
687
|
+
const task = this.newTask();
|
|
688
|
+
scope.tasks.push(task);
|
|
689
|
+
let selected = false;
|
|
690
|
+
let winner = null;
|
|
691
|
+
let value = UNIT;
|
|
692
|
+
let failure = null;
|
|
693
|
+
let cancelled = false;
|
|
694
|
+
const losersCancelled = new Set();
|
|
695
|
+
const ready = () => {
|
|
696
|
+
if (task.done || task.finishing)
|
|
697
|
+
return;
|
|
698
|
+
if (!selected) {
|
|
699
|
+
const first = members
|
|
700
|
+
.filter((member) => member.done)
|
|
701
|
+
.sort((a, b) => a.completedAt - b.completedAt)[0];
|
|
702
|
+
if (!first)
|
|
703
|
+
return;
|
|
704
|
+
selected = true;
|
|
705
|
+
if (!first.fault) {
|
|
706
|
+
winner = first;
|
|
707
|
+
value = first.value;
|
|
708
|
+
first.value = UNIT;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (!cancelled) {
|
|
712
|
+
cancelled = true;
|
|
713
|
+
for (const member of members)
|
|
714
|
+
if (!member.done) {
|
|
715
|
+
losersCancelled.add(member);
|
|
716
|
+
member.cancel();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (members.some((member) => !member.done))
|
|
720
|
+
return;
|
|
721
|
+
task.finishing = true;
|
|
722
|
+
for (const member of members) {
|
|
723
|
+
const index = member.changed.indexOf(ready);
|
|
724
|
+
if (index >= 0)
|
|
725
|
+
member.changed.splice(index, 1);
|
|
726
|
+
}
|
|
727
|
+
for (const member of [...members].sort((a, b) => a.completedAt - b.completedAt)) {
|
|
728
|
+
if (member.fault &&
|
|
729
|
+
!(member.fault.kind === "panic" &&
|
|
730
|
+
member.fault.cancelled &&
|
|
731
|
+
losersCancelled.has(member)))
|
|
732
|
+
failure = combineFaults(failure, member.fault);
|
|
733
|
+
member.fault = null;
|
|
734
|
+
}
|
|
735
|
+
const losers = members
|
|
736
|
+
.filter((member) => member !== winner)
|
|
737
|
+
.map((member) => {
|
|
738
|
+
const result = member.value;
|
|
739
|
+
member.value = UNIT;
|
|
740
|
+
return result;
|
|
741
|
+
});
|
|
742
|
+
this.closeValueAsync({ kind: "vector", elementType: "race result", items: losers }, this.faultText(failure), (closeFault) => {
|
|
743
|
+
failure = combineFaults(failure, closeFault);
|
|
744
|
+
const finish = (releaseFault = null) => {
|
|
745
|
+
failure = combineFaults(failure, releaseFault);
|
|
746
|
+
this.settleTask(task, failure ? UNIT : value, failure);
|
|
747
|
+
for (const changed of [...scope.changed])
|
|
748
|
+
changed();
|
|
749
|
+
};
|
|
750
|
+
if (failure && winner)
|
|
751
|
+
this.closeValueAsync(value, this.faultText(failure), finish, act.context.actor);
|
|
752
|
+
else
|
|
753
|
+
finish();
|
|
754
|
+
}, act.context.actor);
|
|
755
|
+
};
|
|
756
|
+
task.cancel = () => {
|
|
757
|
+
if (task.done || task.finishing)
|
|
758
|
+
return;
|
|
759
|
+
failure ??= {
|
|
760
|
+
kind: "panic",
|
|
761
|
+
message: "task cancelled",
|
|
762
|
+
cancelled: true,
|
|
763
|
+
};
|
|
764
|
+
selected = true;
|
|
765
|
+
ready();
|
|
766
|
+
};
|
|
767
|
+
for (const member of members)
|
|
768
|
+
member.changed.push(ready);
|
|
769
|
+
this.define(act, op.dst, { kind: "task", id: task.id });
|
|
770
|
+
ready();
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
637
773
|
case "task.spawn": {
|
|
638
774
|
const group = act.scopes.get(op.scope);
|
|
639
775
|
if (!group)
|
|
@@ -656,7 +792,9 @@ class ExecutorV1 {
|
|
|
656
792
|
if (!fault || (fault.kind === "panic" && fault.cancelled))
|
|
657
793
|
fault = closeFault ?? fault;
|
|
658
794
|
this.settleTask(task, value, fault);
|
|
659
|
-
if (
|
|
795
|
+
if (!task.grouped &&
|
|
796
|
+
fault &&
|
|
797
|
+
!(fault.kind === "panic" && fault.cancelled)) {
|
|
660
798
|
for (const sibling of group.tasks)
|
|
661
799
|
if (!sibling.done)
|
|
662
800
|
sibling.cancel();
|
|
@@ -670,6 +808,7 @@ class ExecutorV1 {
|
|
|
670
808
|
const context = {
|
|
671
809
|
id: task.id,
|
|
672
810
|
actor: act.context.actor,
|
|
811
|
+
requestDeadline: this.requestDeadline(act),
|
|
673
812
|
returned: (value) => settle(value, null),
|
|
674
813
|
failed: (fault) => settle(UNIT, fault),
|
|
675
814
|
};
|
|
@@ -783,6 +922,8 @@ class ExecutorV1 {
|
|
|
783
922
|
...term.args.map((operand) => this.boundary(act, operand)),
|
|
784
923
|
];
|
|
785
924
|
this.current = this.activate(this.functionAt(entry.callee), args, act, term.result, term.normal, term.unwind);
|
|
925
|
+
if (entry.receiver_handback)
|
|
926
|
+
this.current.erasedHandback = receiver;
|
|
786
927
|
return;
|
|
787
928
|
}
|
|
788
929
|
case "call": {
|
|
@@ -797,7 +938,7 @@ class ExecutorV1 {
|
|
|
797
938
|
// `self`. A plain function value carries none.
|
|
798
939
|
const callee = this.boundary(act, term.callee);
|
|
799
940
|
const args = term.args.map((operand) => this.boundary(act, operand));
|
|
800
|
-
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);
|
|
801
942
|
return;
|
|
802
943
|
}
|
|
803
944
|
case "actor.call":
|
|
@@ -805,6 +946,38 @@ class ExecutorV1 {
|
|
|
805
946
|
return;
|
|
806
947
|
case "runtime.call": {
|
|
807
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
|
+
}
|
|
808
981
|
if (entry &&
|
|
809
982
|
(entry.family === "Map" ||
|
|
810
983
|
entry.family === "Set" ||
|
|
@@ -866,6 +1039,11 @@ class ExecutorV1 {
|
|
|
866
1039
|
this.callShim(act, term, shim);
|
|
867
1040
|
return;
|
|
868
1041
|
}
|
|
1042
|
+
case "wire.codec": {
|
|
1043
|
+
const args = term.args.map((operand) => this.boundary(act, operand));
|
|
1044
|
+
this.runValueProgram(act, term, args, executeCodec(this.pkg, term, args[0], this.options.codecBridge, this.options.propertyInput ?? []));
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
869
1047
|
case "value.call": {
|
|
870
1048
|
const args = term.args.map((operand) => this.boundary(act, operand));
|
|
871
1049
|
this.runValueProgram(act, term, args, selectedValue(this.pkg, term.plan, args));
|
|
@@ -960,8 +1138,10 @@ class ExecutorV1 {
|
|
|
960
1138
|
return;
|
|
961
1139
|
case "unreachable":
|
|
962
1140
|
throw new Error(`${act.fn.name}: reached an unreachable endpoint`);
|
|
963
|
-
default:
|
|
964
|
-
|
|
1141
|
+
default: {
|
|
1142
|
+
const exhaustive = term;
|
|
1143
|
+
throw new Error(`unhandled terminator: ${String(exhaustive)}`);
|
|
1144
|
+
}
|
|
965
1145
|
}
|
|
966
1146
|
}
|
|
967
1147
|
takeEdge(act, edge) {
|
|
@@ -992,6 +1172,7 @@ class ExecutorV1 {
|
|
|
992
1172
|
}
|
|
993
1173
|
catch (error) {
|
|
994
1174
|
if (error instanceof ShimFault) {
|
|
1175
|
+
this.keepInputs(act, term, args);
|
|
995
1176
|
this.raiseFault(act, { kind: "trap", trap: error.trap, message: error.message }, term.unwind);
|
|
996
1177
|
return;
|
|
997
1178
|
}
|
|
@@ -999,6 +1180,20 @@ class ExecutorV1 {
|
|
|
999
1180
|
}
|
|
1000
1181
|
this.completeShim(act, term, value);
|
|
1001
1182
|
}
|
|
1183
|
+
/// A runtime call whose contract keeps its inputs on failure fails before
|
|
1184
|
+
/// taking them, so each moved argument stays with the caller, whose unwind
|
|
1185
|
+
/// edge stores it back.
|
|
1186
|
+
keepsInputs(term) {
|
|
1187
|
+
return term.op === "runtime.call" && term.keeps_inputs_on_failure === true;
|
|
1188
|
+
}
|
|
1189
|
+
keepInputs(act, term, args) {
|
|
1190
|
+
if (!this.keepsInputs(term))
|
|
1191
|
+
return;
|
|
1192
|
+
term.args.forEach((operand, index) => {
|
|
1193
|
+
if (operand.decision === "move")
|
|
1194
|
+
this.define(act, operand.value, args[index]);
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1002
1197
|
runValueProgram(act, term, args, iterator) {
|
|
1003
1198
|
this.running = false;
|
|
1004
1199
|
let initial = true;
|
|
@@ -1010,10 +1205,16 @@ class ExecutorV1 {
|
|
|
1010
1205
|
this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
|
|
1011
1206
|
};
|
|
1012
1207
|
const fail = (fault, extra = UNIT) => {
|
|
1013
|
-
const
|
|
1208
|
+
const kept = this.keepsInputs(term) ? ["move"] : [];
|
|
1209
|
+
const owned = args.filter((_arg, index) => {
|
|
1210
|
+
const decision = term.args[index].decision;
|
|
1211
|
+
return (["move", "copy", "snapshot"].includes(decision) &&
|
|
1212
|
+
!kept.includes(decision));
|
|
1213
|
+
});
|
|
1014
1214
|
this.closeValueAsync({ kind: "vector", elementType: "", items: [...owned, extra] }, this.faultText(fault), (closeFault) => {
|
|
1015
1215
|
if (fault.kind === "panic" && fault.cancelled && closeFault)
|
|
1016
1216
|
fault = closeFault;
|
|
1217
|
+
this.keepInputs(act, term, args);
|
|
1017
1218
|
this.raiseFault(act, fault, term.unwind);
|
|
1018
1219
|
resume();
|
|
1019
1220
|
});
|
|
@@ -1116,6 +1317,13 @@ class ExecutorV1 {
|
|
|
1116
1317
|
if (!act.normal) {
|
|
1117
1318
|
throw new Error(`${act.fn.name}: returned from a call with a "never" result`);
|
|
1118
1319
|
}
|
|
1320
|
+
if (act.erasedHandback) {
|
|
1321
|
+
if (value.kind !== "record" || value.fields.length !== 2) {
|
|
1322
|
+
throw new Error(`${act.fn.name}: invalid erased receiver dual return`);
|
|
1323
|
+
}
|
|
1324
|
+
act.erasedHandback.value = value.fields[1];
|
|
1325
|
+
value = value.fields[0];
|
|
1326
|
+
}
|
|
1119
1327
|
// The result is defined on the normal edge, like a block parameter, so it
|
|
1120
1328
|
// is in scope for that edge's arguments.
|
|
1121
1329
|
if (act.result && act.result !== "never") {
|
|
@@ -1166,12 +1374,15 @@ class ExecutorV1 {
|
|
|
1166
1374
|
}
|
|
1167
1375
|
// A failing `var self` method hands its receiver back, defined on the
|
|
1168
1376
|
// unwind edge like a result on the normal edge.
|
|
1169
|
-
if ((handback === null) !== !act.handback) {
|
|
1377
|
+
if ((handback === null) !== !(act.handback || act.erasedHandback)) {
|
|
1170
1378
|
throw new Error(`${act.fn.name}: receiver handback disagrees with its call`);
|
|
1171
1379
|
}
|
|
1172
1380
|
if (handback !== null && act.handback) {
|
|
1173
1381
|
this.define(caller, act.handback.value, handback);
|
|
1174
1382
|
}
|
|
1383
|
+
if (handback !== null && act.erasedHandback) {
|
|
1384
|
+
act.erasedHandback.value = handback;
|
|
1385
|
+
}
|
|
1175
1386
|
caller.fault = fault;
|
|
1176
1387
|
this.current = caller;
|
|
1177
1388
|
this.takeEdge(caller, act.unwind);
|
|
@@ -1305,11 +1516,10 @@ class ExecutorV1 {
|
|
|
1305
1516
|
return;
|
|
1306
1517
|
}
|
|
1307
1518
|
const failure = group.deadline ??
|
|
1308
|
-
group.tasks.find((task) => task.
|
|
1519
|
+
group.tasks.find((task) => !task.grouped &&
|
|
1520
|
+
task.fault &&
|
|
1309
1521
|
!(task.fault.kind === "panic" && task.fault.cancelled))?.fault;
|
|
1310
|
-
wake(UNIT, mode === "propagate_fault"
|
|
1311
|
-
? null
|
|
1312
|
-
: (failure ?? null));
|
|
1522
|
+
wake(UNIT, mode === "propagate_fault" ? null : (failure ?? null));
|
|
1313
1523
|
};
|
|
1314
1524
|
// Cancellation requests descendant cleanup; it cannot bypass the
|
|
1315
1525
|
// join barrier and let the parent retire their captured resources.
|
|
@@ -1380,7 +1590,12 @@ class ExecutorV1 {
|
|
|
1380
1590
|
return;
|
|
1381
1591
|
}
|
|
1382
1592
|
case "NativeIo": {
|
|
1383
|
-
|
|
1593
|
+
if (term.detail?.operation === "SinkFinish") {
|
|
1594
|
+
this.host.pipes.close(this.boundary(act, term.inputs[0]));
|
|
1595
|
+
this.park(act, term)(UNIT);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
// Admission passed `StdinReadLine`. The record is the raw line,
|
|
1384
1599
|
// so the records concatenate to the stdin the program consumed.
|
|
1385
1600
|
const line = this.stdin.readLine() ?? new Uint8Array();
|
|
1386
1601
|
this.trace.recordReplayInput({ kind: "stdin", data: new TextDecoder().decode(line) }, false);
|
|
@@ -1608,6 +1823,7 @@ class ExecutorV1 {
|
|
|
1608
1823
|
newTask() {
|
|
1609
1824
|
const task = {
|
|
1610
1825
|
id: this.ids.task(),
|
|
1826
|
+
grouped: false,
|
|
1611
1827
|
done: false,
|
|
1612
1828
|
finishing: false,
|
|
1613
1829
|
value: UNIT,
|
|
@@ -1700,6 +1916,9 @@ class ExecutorV1 {
|
|
|
1700
1916
|
actorCall(act, term) {
|
|
1701
1917
|
const args = term.args.map((arg) => this.boundary(act, arg));
|
|
1702
1918
|
const operation = term.operation;
|
|
1919
|
+
const caller = act.context.actor
|
|
1920
|
+
? { kind: "Actor", identity: act.context.actor.identity }
|
|
1921
|
+
: { kind: "Task" };
|
|
1703
1922
|
switch (operation.op) {
|
|
1704
1923
|
case "spawn": {
|
|
1705
1924
|
const layout = this.pkg.actors?.find((actor) => actor.id === operation.actor);
|
|
@@ -1707,6 +1926,7 @@ class ExecutorV1 {
|
|
|
1707
1926
|
throw new Error(`actor layout ${operation.actor} is missing`);
|
|
1708
1927
|
let supplied = 0;
|
|
1709
1928
|
const actor = {
|
|
1929
|
+
identity: ++this.actorIdentity,
|
|
1710
1930
|
id: this.ids.actor(),
|
|
1711
1931
|
layout,
|
|
1712
1932
|
state: {
|
|
@@ -1715,6 +1935,7 @@ class ExecutorV1 {
|
|
|
1715
1935
|
fields: layout.state_fields.map((field) => field.deferred ? UNIT : (args[supplied++] ?? UNIT)),
|
|
1716
1936
|
},
|
|
1717
1937
|
mailbox: [],
|
|
1938
|
+
disposals: [],
|
|
1718
1939
|
admission: [],
|
|
1719
1940
|
busy: true,
|
|
1720
1941
|
alive: true,
|
|
@@ -1789,9 +2010,11 @@ class ExecutorV1 {
|
|
|
1789
2010
|
if (!handler)
|
|
1790
2011
|
throw new Error("stream request has no producer handler");
|
|
1791
2012
|
actor.mailbox.push({
|
|
2013
|
+
caller,
|
|
1792
2014
|
handler,
|
|
1793
2015
|
payload: fields,
|
|
1794
2016
|
reply: false,
|
|
2017
|
+
obligated: true,
|
|
1795
2018
|
complete: (_value, error, drained) => {
|
|
1796
2019
|
if (error)
|
|
1797
2020
|
this.closeValue(payload, error);
|
|
@@ -1871,13 +2094,13 @@ class ExecutorV1 {
|
|
|
1871
2094
|
}
|
|
1872
2095
|
const full = actor.layout.mailbox_capacity !== undefined &&
|
|
1873
2096
|
actor.mailbox.length >= actor.layout.mailbox_capacity;
|
|
1874
|
-
//
|
|
1875
|
-
// sender's
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2097
|
+
// Declared loss transfers cleanup to the receiver. Sender-selected
|
|
2098
|
+
// loss still closes the sender's unadmitted request in its own frame.
|
|
2099
|
+
const discardAtReceiver = (value, report) => {
|
|
2100
|
+
actor.disposals.push(value);
|
|
2101
|
+
this.dispatchActor(actor);
|
|
2102
|
+
finish(report);
|
|
2103
|
+
};
|
|
1881
2104
|
const discard = (value, report) => {
|
|
1882
2105
|
let settled = false;
|
|
1883
2106
|
let waiting = false;
|
|
@@ -1900,7 +2123,7 @@ class ExecutorV1 {
|
|
|
1900
2123
|
if (declared === "coalesce" && coalesce) {
|
|
1901
2124
|
const key = coalesce.keys.find((key) => key.message === handler.message_id);
|
|
1902
2125
|
const queued = key
|
|
1903
|
-
? actor.mailbox.findIndex((message) => !message.
|
|
2126
|
+
? actor.mailbox.findIndex((message) => !message.obligated &&
|
|
1904
2127
|
message.handler.message_id === handler.message_id &&
|
|
1905
2128
|
equals(message.payload[key.param], payload.fields[key.param]))
|
|
1906
2129
|
: -1;
|
|
@@ -1908,32 +2131,35 @@ class ExecutorV1 {
|
|
|
1908
2131
|
const superseded = actor.mailbox[queued];
|
|
1909
2132
|
actor.mailbox[queued] = {
|
|
1910
2133
|
...superseded,
|
|
2134
|
+
caller,
|
|
1911
2135
|
payload: payload.fields,
|
|
1912
2136
|
};
|
|
1913
|
-
|
|
2137
|
+
discardAtReceiver({ kind: "record", typeId: "", fields: superseded.payload }, accepted(false));
|
|
1914
2138
|
return;
|
|
1915
2139
|
}
|
|
1916
2140
|
declared = coalesce.fallback;
|
|
1917
2141
|
}
|
|
1918
2142
|
if (declared === "drop_new") {
|
|
1919
|
-
|
|
2143
|
+
discardAtReceiver(payload, accepted(true));
|
|
1920
2144
|
return;
|
|
1921
2145
|
}
|
|
1922
2146
|
// A queued completion call owes its caller a reply, so `drop_old`
|
|
1923
2147
|
// evicts only a one-way message and refuses when there is none.
|
|
1924
2148
|
const oldest = declared === "drop_old"
|
|
1925
|
-
? actor.mailbox.findIndex((message) => !message.
|
|
2149
|
+
? actor.mailbox.findIndex((message) => !message.obligated)
|
|
1926
2150
|
: -1;
|
|
1927
2151
|
if (oldest >= 0) {
|
|
1928
2152
|
const [evicted] = actor.mailbox.splice(oldest, 1);
|
|
1929
2153
|
actor.mailbox.push({
|
|
2154
|
+
caller,
|
|
1930
2155
|
handler,
|
|
1931
2156
|
payload: payload.fields,
|
|
1932
2157
|
reply: false,
|
|
2158
|
+
obligated: false,
|
|
1933
2159
|
complete: (_value, _error, drained) => drained?.(null),
|
|
1934
2160
|
});
|
|
1935
2161
|
this.dispatchActor(actor);
|
|
1936
|
-
|
|
2162
|
+
discardAtReceiver({ kind: "record", typeId: "", fields: evicted.payload }, accepted(false));
|
|
1937
2163
|
return;
|
|
1938
2164
|
}
|
|
1939
2165
|
if (full) {
|
|
@@ -1955,9 +2181,11 @@ class ExecutorV1 {
|
|
|
1955
2181
|
return;
|
|
1956
2182
|
}
|
|
1957
2183
|
actor.mailbox.push({
|
|
2184
|
+
caller,
|
|
1958
2185
|
handler,
|
|
1959
2186
|
payload: payload.fields,
|
|
1960
2187
|
reply: false,
|
|
2188
|
+
obligated: false,
|
|
1961
2189
|
complete: (_value, _error, drained) => drained?.(null),
|
|
1962
2190
|
});
|
|
1963
2191
|
this.trace.snapshot("actor.send", {
|
|
@@ -2142,15 +2370,32 @@ class ExecutorV1 {
|
|
|
2142
2370
|
throw new Error(`actor operation ${operation.op} has no executor`);
|
|
2143
2371
|
}
|
|
2144
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
|
+
}
|
|
2145
2383
|
ask(protocol, inputs, complete, shapes) {
|
|
2146
2384
|
let settled = false;
|
|
2147
2385
|
let admitted = false;
|
|
2148
2386
|
let closing = false;
|
|
2149
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" };
|
|
2150
2394
|
let receiver = null;
|
|
2151
2395
|
const finish = (value, error, drained) => {
|
|
2152
2396
|
const alreadySettled = settled;
|
|
2153
2397
|
settled = true;
|
|
2398
|
+
cancelTimer?.();
|
|
2154
2399
|
if (alreadySettled || complete(value, error) === false) {
|
|
2155
2400
|
// The finishing receiver still owns a reply that its caller no
|
|
2156
2401
|
// longer accepts. Its next turn must wait for this release.
|
|
@@ -2194,12 +2439,17 @@ class ExecutorV1 {
|
|
|
2194
2439
|
if (settled)
|
|
2195
2440
|
return;
|
|
2196
2441
|
settled = true;
|
|
2442
|
+
cancelTimer?.();
|
|
2197
2443
|
complete(null, reason, fault);
|
|
2198
2444
|
});
|
|
2199
2445
|
};
|
|
2200
2446
|
const attempt = () => {
|
|
2201
2447
|
if (settled || closing)
|
|
2202
2448
|
return;
|
|
2449
|
+
if (deadline !== undefined && deadline <= this.trace.virtualTimeMs) {
|
|
2450
|
+
reject("TimedOut");
|
|
2451
|
+
return;
|
|
2452
|
+
}
|
|
2203
2453
|
const role = "id" in target ? this.roles.get(target.id) : undefined;
|
|
2204
2454
|
if (role && this.rolePending(role)) {
|
|
2205
2455
|
role.waiting.push(attempt);
|
|
@@ -2252,17 +2502,50 @@ class ExecutorV1 {
|
|
|
2252
2502
|
}
|
|
2253
2503
|
admitted = true;
|
|
2254
2504
|
receiver = actor;
|
|
2255
|
-
|
|
2505
|
+
message = {
|
|
2506
|
+
caller,
|
|
2507
|
+
handler,
|
|
2508
|
+
payload,
|
|
2509
|
+
complete: finish,
|
|
2510
|
+
reply: true,
|
|
2511
|
+
obligated: true,
|
|
2512
|
+
requestDeadline: deadline,
|
|
2513
|
+
};
|
|
2514
|
+
actor.mailbox.push(message);
|
|
2256
2515
|
this.trace.snapshot("actor.ask", {
|
|
2257
2516
|
actor_id: actor.id,
|
|
2258
2517
|
handler: handler.name,
|
|
2259
2518
|
});
|
|
2260
2519
|
this.dispatchActor(actor);
|
|
2261
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
|
+
}
|
|
2262
2543
|
attempt();
|
|
2263
2544
|
return (done) => {
|
|
2264
2545
|
const wasSettled = settled;
|
|
2265
2546
|
settled = true;
|
|
2547
|
+
cancelTimer?.();
|
|
2548
|
+
withdrawQueued();
|
|
2266
2549
|
if (admitted || (wasSettled && !closing))
|
|
2267
2550
|
done(null);
|
|
2268
2551
|
else
|
|
@@ -2272,6 +2555,18 @@ class ExecutorV1 {
|
|
|
2272
2555
|
dispatchActor(actor) {
|
|
2273
2556
|
if (actor.busy || !actor.alive || actor.drainingMailbox)
|
|
2274
2557
|
return;
|
|
2558
|
+
if (actor.disposals.length > 0) {
|
|
2559
|
+
actor.busy = true;
|
|
2560
|
+
const values = actor.disposals.splice(0);
|
|
2561
|
+
this.closeValueAsync({ kind: "vector", elementType: "message", items: values }, this.faultText(actor.crashing), (fault) => {
|
|
2562
|
+
actor.busy = false;
|
|
2563
|
+
if (fault)
|
|
2564
|
+
this.crashActor(actor, fault);
|
|
2565
|
+
else
|
|
2566
|
+
this.dispatchActor(actor);
|
|
2567
|
+
}, actor);
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2275
2570
|
if (actor.terminating && actor.mailbox.length > 0) {
|
|
2276
2571
|
this.drainMailbox(actor, this.faultText(actor.closingFault), (fault) => {
|
|
2277
2572
|
actor.closingFault = combineFaults(actor.closingFault, fault);
|
|
@@ -2301,6 +2596,18 @@ class ExecutorV1 {
|
|
|
2301
2596
|
}
|
|
2302
2597
|
for (const admit of actor.admission.splice(0))
|
|
2303
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
|
+
}
|
|
2304
2611
|
this.trace.snapshot("actor.receive", {
|
|
2305
2612
|
actor_id: actor.id,
|
|
2306
2613
|
handler: message.handler.name,
|
|
@@ -2308,6 +2615,9 @@ class ExecutorV1 {
|
|
|
2308
2615
|
const frame = this.activate(this.functionAt(message.handler.callable), [actor.state, ...message.payload], null, null, undefined, null, {
|
|
2309
2616
|
id: actor.id,
|
|
2310
2617
|
actor,
|
|
2618
|
+
caller: message.caller,
|
|
2619
|
+
requestDeadline: message.requestDeadline,
|
|
2620
|
+
reply: message.reply,
|
|
2311
2621
|
returned: (value) => {
|
|
2312
2622
|
actor.active = null;
|
|
2313
2623
|
const drained = (fault) => {
|
|
@@ -2345,7 +2655,14 @@ class ExecutorV1 {
|
|
|
2345
2655
|
failed: (fault) => {
|
|
2346
2656
|
actor.active = null;
|
|
2347
2657
|
actor.busy = false;
|
|
2348
|
-
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) {
|
|
2349
2666
|
message.complete(null, "Dead");
|
|
2350
2667
|
this.dispatchActor(actor);
|
|
2351
2668
|
}
|
|
@@ -2649,11 +2966,14 @@ class ExecutorV1 {
|
|
|
2649
2966
|
this.closeValueAsync({
|
|
2650
2967
|
kind: "vector",
|
|
2651
2968
|
elementType: "message",
|
|
2652
|
-
items:
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2969
|
+
items: [
|
|
2970
|
+
...actor.disposals.splice(0),
|
|
2971
|
+
...messages.map((message) => ({
|
|
2972
|
+
kind: "record",
|
|
2973
|
+
typeId: "",
|
|
2974
|
+
fields: message.payload,
|
|
2975
|
+
})),
|
|
2976
|
+
],
|
|
2657
2977
|
}, fault, (closeFault) => {
|
|
2658
2978
|
actor.drainingMailbox = false;
|
|
2659
2979
|
for (const message of messages)
|
|
@@ -2784,8 +3104,10 @@ function combineFaults(primary, secondary) {
|
|
|
2784
3104
|
// Reports own an immutable ordered view of original diagnostics. Faults can
|
|
2785
3105
|
// be shared by a parked unwind, actor completion and multiple observers;
|
|
2786
3106
|
// combining those views must neither duplicate nor mutate their graph.
|
|
2787
|
-
const
|
|
2788
|
-
|
|
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))
|
|
2789
3111
|
: diagnostics;
|
|
2790
3112
|
const [first, ...rest] = retained.length ? retained : diagnostics;
|
|
2791
3113
|
return { ...first, primary: first, secondary: rest };
|
|
@@ -2803,6 +3125,7 @@ function faultInfo(fault) {
|
|
|
2803
3125
|
divide_by_zero: [202, "DivideByZero"],
|
|
2804
3126
|
shift_out_of_range: [204, "ShiftOutOfRange"],
|
|
2805
3127
|
vector_bounds: [205, "IndexOutOfBounds"],
|
|
3128
|
+
actor_refused: [-4, "Refused"],
|
|
2806
3129
|
}[fault.trap];
|
|
2807
3130
|
}
|
|
2808
3131
|
function faultReport(fault, secondary = false) {
|
|
@@ -2872,6 +3195,8 @@ function actorErrorRole(reason) {
|
|
|
2872
3195
|
return "ActorErrorFailed";
|
|
2873
3196
|
case "Trapped":
|
|
2874
3197
|
return "ActorErrorTrapped";
|
|
3198
|
+
case "Refused":
|
|
3199
|
+
return "ActorErrorRefused";
|
|
2875
3200
|
case "Dead":
|
|
2876
3201
|
return "ActorErrorDead";
|
|
2877
3202
|
case "TimedOut":
|
|
@@ -2906,8 +3231,6 @@ function sendErrorRole(reason) {
|
|
|
2906
3231
|
return "SendErrorCancelled";
|
|
2907
3232
|
case "VersionMismatch":
|
|
2908
3233
|
return "SendErrorVersionMismatch";
|
|
2909
|
-
case "Unauthorized":
|
|
2910
|
-
return "SendErrorUnauthorized";
|
|
2911
3234
|
case "Backpressure":
|
|
2912
3235
|
return "SendErrorBackpressure";
|
|
2913
3236
|
case "Dead":
|
|
@@ -2916,12 +3239,17 @@ function sendErrorRole(reason) {
|
|
|
2916
3239
|
throw new Error(`unknown send error reason ${reason}`);
|
|
2917
3240
|
}
|
|
2918
3241
|
}
|
|
2919
|
-
function calleeFunction(value) {
|
|
3242
|
+
function calleeFunction(value, specialization = null) {
|
|
2920
3243
|
switch (value.kind) {
|
|
2921
3244
|
case "function":
|
|
2922
3245
|
return Number(value.id);
|
|
2923
|
-
case "closure":
|
|
2924
|
-
|
|
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
|
+
}
|
|
2925
3253
|
default:
|
|
2926
3254
|
throw new TypeError(`indirect.call expected a callable value, got ${value.kind}`);
|
|
2927
3255
|
}
|
|
@@ -2998,6 +3326,8 @@ function trapMessage(trap) {
|
|
|
2998
3326
|
return "shift amount out of range";
|
|
2999
3327
|
case "vector_bounds":
|
|
3000
3328
|
return "index out of bounds";
|
|
3329
|
+
case "actor_refused":
|
|
3330
|
+
return "delivery refused by interceptor";
|
|
3001
3331
|
}
|
|
3002
3332
|
}
|
|
3003
3333
|
/// The operand type the package records. `isize` and `usize` are 64-bit in the
|
|
@@ -3104,6 +3434,21 @@ function applyBinary(op, lhs, rhs, ty) {
|
|
|
3104
3434
|
return { kind: "bool", value: truth(lhs) && truth(rhs) };
|
|
3105
3435
|
case "Or":
|
|
3106
3436
|
return { kind: "bool", value: truth(lhs) || truth(rhs) };
|
|
3437
|
+
case "BitAnd":
|
|
3438
|
+
case "BitOr":
|
|
3439
|
+
case "BitXor":
|
|
3440
|
+
// Both operands were already evaluated; bool operands stay bool.
|
|
3441
|
+
if (lhs.kind === "bool" && rhs.kind === "bool") {
|
|
3442
|
+
return {
|
|
3443
|
+
kind: "bool",
|
|
3444
|
+
value: op === "BitAnd"
|
|
3445
|
+
? lhs.value && rhs.value
|
|
3446
|
+
: op === "BitOr"
|
|
3447
|
+
? lhs.value || rhs.value
|
|
3448
|
+
: lhs.value !== rhs.value,
|
|
3449
|
+
};
|
|
3450
|
+
}
|
|
3451
|
+
break;
|
|
3107
3452
|
default:
|
|
3108
3453
|
break;
|
|
3109
3454
|
}
|