@hew-lang/sandbox-vm 0.8.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/interpreter/v1/exec.js +154 -7
- package/dist/interpreter/v1/instructions.generated.d.ts +2 -1
- package/dist/interpreter/v1/package-validator.generated.js +1507 -1315
- package/dist/interpreter/v1/shims.d.ts +3 -1
- package/dist/interpreter/v1/shims.js +28 -4
- package/dist/interpreter/v1/validate.js +2 -1
- package/package.json +4 -4
|
@@ -143,6 +143,11 @@ class ExecutorV1 {
|
|
|
143
143
|
this.host = {
|
|
144
144
|
argv: [...(options.argv ?? ["main.hew"])],
|
|
145
145
|
unixTimeMillis: () => this.trace.virtualTimeMs,
|
|
146
|
+
monotonicTimeNanos: () => {
|
|
147
|
+
const millis = this.trace.virtualTimeMs;
|
|
148
|
+
return (BigInt(Math.trunc(millis)) * 1000000n +
|
|
149
|
+
BigInt(Math.round((millis % 1) * 1_000_000)));
|
|
150
|
+
},
|
|
146
151
|
calendarBridge: options.calendarBridge,
|
|
147
152
|
codecBridge: options.codecBridge,
|
|
148
153
|
encodingError: (format, message) => {
|
|
@@ -154,6 +159,10 @@ class ExecutorV1 {
|
|
|
154
159
|
errors[format] = message;
|
|
155
160
|
return errors[format] ?? "";
|
|
156
161
|
},
|
|
162
|
+
setFileError: (message) => {
|
|
163
|
+
this.current.context.fileError =
|
|
164
|
+
message === null ? null : { kind: 0, message };
|
|
165
|
+
},
|
|
157
166
|
fileError: (operation) => {
|
|
158
167
|
const error = this.current.context.fileError;
|
|
159
168
|
switch (operation) {
|
|
@@ -1162,7 +1171,15 @@ class ExecutorV1 {
|
|
|
1162
1171
|
// `self`. A plain function value carries none.
|
|
1163
1172
|
const callee = this.boundary(act, term.callee);
|
|
1164
1173
|
const args = term.args.map((operand) => this.boundary(act, operand));
|
|
1165
|
-
|
|
1174
|
+
const body = this.functionAt(calleeFunction(callee, term.specialization));
|
|
1175
|
+
this.current = this.activate(body, callee.kind === "closure" ? [callee.environment, ...args] : args, act, term.result, term.normal, term.unwind);
|
|
1176
|
+
// SIR already distinguishes a consuming body from a borrowed body
|
|
1177
|
+
// exposed through a once-callable view. Only the latter leaves its
|
|
1178
|
+
// receiver cleanup to this invocation boundary.
|
|
1179
|
+
if (callee.kind === "closure" &&
|
|
1180
|
+
term.callee.decision === "move" &&
|
|
1181
|
+
body.params[0]?.own !== "owned")
|
|
1182
|
+
this.current.consumedCallable = callee;
|
|
1166
1183
|
return;
|
|
1167
1184
|
}
|
|
1168
1185
|
case "actor.call":
|
|
@@ -1180,6 +1197,23 @@ class ExecutorV1 {
|
|
|
1180
1197
|
if (registration.kind === "refused")
|
|
1181
1198
|
throw new Error(`runtime family ${registration.capability} was refused after admission`);
|
|
1182
1199
|
switch (registration.handler) {
|
|
1200
|
+
case "context_actor_id":
|
|
1201
|
+
this.completeShim(act, term, {
|
|
1202
|
+
kind: "i64",
|
|
1203
|
+
value: act.context.actor?.identity ?? 0n,
|
|
1204
|
+
});
|
|
1205
|
+
return;
|
|
1206
|
+
case "context_supervisor": {
|
|
1207
|
+
const parent = act.context.actor?.supervisor?.owner;
|
|
1208
|
+
this.completeShim(act, term, parent
|
|
1209
|
+
? { kind: "supervisor", id: parent.id }
|
|
1210
|
+
: { kind: "i64", value: 0n });
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
case "context_trace_span":
|
|
1214
|
+
// The sandbox installs no external tracing span in actor turns.
|
|
1215
|
+
this.completeShim(act, term, { kind: "i64", value: 0n });
|
|
1216
|
+
return;
|
|
1183
1217
|
case "actor_caller": {
|
|
1184
1218
|
const caller = act.context.caller ?? { kind: "Task" };
|
|
1185
1219
|
const payload = [];
|
|
@@ -1683,6 +1717,8 @@ class ExecutorV1 {
|
|
|
1683
1717
|
return true;
|
|
1684
1718
|
}
|
|
1685
1719
|
returnFrom(act, value) {
|
|
1720
|
+
if (this.finishConsumedCallable(act, value, () => this.returnFrom(act, value)))
|
|
1721
|
+
return;
|
|
1686
1722
|
if (this.finishConstruction(act, () => this.returnFrom(act, value)))
|
|
1687
1723
|
return;
|
|
1688
1724
|
const caller = act.caller;
|
|
@@ -1744,6 +1780,8 @@ class ExecutorV1 {
|
|
|
1744
1780
|
this.takeEdge(act, unwind);
|
|
1745
1781
|
}
|
|
1746
1782
|
resumeUnwind(act, handback) {
|
|
1783
|
+
if (this.finishConsumedCallable(act, null, () => this.resumeUnwind(act, handback)))
|
|
1784
|
+
return;
|
|
1747
1785
|
if (this.finishConstruction(act, () => this.resumeUnwind(act, handback)))
|
|
1748
1786
|
return;
|
|
1749
1787
|
const fault = act.fault;
|
|
@@ -1774,6 +1812,34 @@ class ExecutorV1 {
|
|
|
1774
1812
|
this.current = caller;
|
|
1775
1813
|
this.takeEdge(caller, act.unwind);
|
|
1776
1814
|
}
|
|
1815
|
+
finishConsumedCallable(act, value, done) {
|
|
1816
|
+
const callable = act.consumedCallable;
|
|
1817
|
+
if (!callable)
|
|
1818
|
+
return false;
|
|
1819
|
+
act.consumedCallable = undefined;
|
|
1820
|
+
this.running = false;
|
|
1821
|
+
this.closeValueAsync(callable, this.faultText(act.fault), (fault) => {
|
|
1822
|
+
act.fault = combineFaults(act.fault, fault);
|
|
1823
|
+
const resume = () => {
|
|
1824
|
+
if (value !== null && act.fault)
|
|
1825
|
+
this.resumeUnwind(act, null);
|
|
1826
|
+
else
|
|
1827
|
+
done();
|
|
1828
|
+
const current = this.current;
|
|
1829
|
+
this.scheduler.enqueue(current.context.id, () => this.runFrame(current));
|
|
1830
|
+
};
|
|
1831
|
+
// A successful body result is not observable until receiver cleanup
|
|
1832
|
+
// succeeds. A cleanup fault must release that unpublished result.
|
|
1833
|
+
if (value !== null && act.fault)
|
|
1834
|
+
this.closeValueAsync(value, this.faultText(act.fault), (resultFault) => {
|
|
1835
|
+
act.fault = combineFaults(act.fault, resultFault);
|
|
1836
|
+
resume();
|
|
1837
|
+
}, act.context.actor, act.rcOwner);
|
|
1838
|
+
else
|
|
1839
|
+
resume();
|
|
1840
|
+
}, act.context.actor, act.rcOwner);
|
|
1841
|
+
return true;
|
|
1842
|
+
}
|
|
1777
1843
|
suspend(act, input) {
|
|
1778
1844
|
const registration = resolveSuspension(input);
|
|
1779
1845
|
if (registration.kind === "refused")
|
|
@@ -2046,6 +2112,7 @@ class ExecutorV1 {
|
|
|
2046
2112
|
// The record is the raw line,
|
|
2047
2113
|
// so the records concatenate to the stdin the program consumed.
|
|
2048
2114
|
const line = this.stdin.readLine() ?? new Uint8Array();
|
|
2115
|
+
act.context.fileError = null;
|
|
2049
2116
|
this.trace.recordReplayInput({ kind: "stdin", data: new TextDecoder().decode(line) }, false);
|
|
2050
2117
|
this.park(act, term)(bytesValue([...line]));
|
|
2051
2118
|
return;
|
|
@@ -3141,12 +3208,29 @@ class ExecutorV1 {
|
|
|
3141
3208
|
const count = isPool ? args[args.length - 1] : undefined;
|
|
3142
3209
|
if (isPool && count?.kind !== "i64")
|
|
3143
3210
|
throw new Error("pool construction has no count");
|
|
3144
|
-
const
|
|
3145
|
-
|
|
3146
|
-
|
|
3211
|
+
const configured = isPool ? args.slice(0, -1) : args;
|
|
3212
|
+
const initial = layout.backoff
|
|
3213
|
+
? configured[configured.length - 2]
|
|
3214
|
+
: undefined;
|
|
3215
|
+
const maximum = layout.backoff
|
|
3216
|
+
? configured[configured.length - 1]
|
|
3217
|
+
: undefined;
|
|
3218
|
+
const backoff = initial?.kind === "i64" && maximum?.kind === "i64"
|
|
3219
|
+
? { initial: initial.value, maximum: maximum.value }
|
|
3220
|
+
: null;
|
|
3221
|
+
const validBackoff = !layout.backoff ||
|
|
3222
|
+
(backoff !== null &&
|
|
3223
|
+
backoff.initial > 0n &&
|
|
3224
|
+
backoff.maximum >= backoff.initial);
|
|
3225
|
+
const config = layout.backoff ? configured.slice(0, -2) : configured;
|
|
3226
|
+
if (!validBackoff ||
|
|
3227
|
+
(count?.kind === "i64" &&
|
|
3228
|
+
(count.value <= 0n || count.value > 2147483647n))) {
|
|
3147
3229
|
const fault = {
|
|
3148
3230
|
kind: "panic",
|
|
3149
|
-
message:
|
|
3231
|
+
message: !validBackoff
|
|
3232
|
+
? "supervisor backoff requires positive initial and maximum >= initial"
|
|
3233
|
+
: "supervisor pool count must be between 1 and 2147483647",
|
|
3150
3234
|
};
|
|
3151
3235
|
this.running = false;
|
|
3152
3236
|
this.closeValueAsync({ kind: "record", typeId: "", fields: config }, fault.message, (closeFault) => {
|
|
@@ -3174,6 +3258,9 @@ class ExecutorV1 {
|
|
|
3174
3258
|
teardownChild: null,
|
|
3175
3259
|
teardownCancelTimer: null,
|
|
3176
3260
|
restartTimes: [],
|
|
3261
|
+
backoff,
|
|
3262
|
+
restartDelays: new Map(),
|
|
3263
|
+
restartTimers: new Map(),
|
|
3177
3264
|
closed: [],
|
|
3178
3265
|
};
|
|
3179
3266
|
this.supervisors.set(owner.id, owner);
|
|
@@ -3655,6 +3742,30 @@ class ExecutorV1 {
|
|
|
3655
3742
|
for (const timer of [...actor.timers])
|
|
3656
3743
|
timer.cancel();
|
|
3657
3744
|
}
|
|
3745
|
+
bindActorStream(context, sink) {
|
|
3746
|
+
let unwatch = () => { };
|
|
3747
|
+
let active = true;
|
|
3748
|
+
const detach = () => {
|
|
3749
|
+
active = false;
|
|
3750
|
+
unwatch();
|
|
3751
|
+
};
|
|
3752
|
+
const closed = () => {
|
|
3753
|
+
if (!active || !this.pipes.peerClosed(sink))
|
|
3754
|
+
return;
|
|
3755
|
+
// Bind the reader to this exact producer turn, including while it waits
|
|
3756
|
+
// on another operation. Later reader closes must not cancel a new turn.
|
|
3757
|
+
detach();
|
|
3758
|
+
context.pendingCancellation ??= {
|
|
3759
|
+
kind: "panic",
|
|
3760
|
+
message: "actor stream reader closed",
|
|
3761
|
+
cancelled: true,
|
|
3762
|
+
};
|
|
3763
|
+
context.cancel?.(context.pendingCancellation);
|
|
3764
|
+
};
|
|
3765
|
+
unwatch = this.pipes.observe(sink, closed);
|
|
3766
|
+
closed();
|
|
3767
|
+
return detach;
|
|
3768
|
+
}
|
|
3658
3769
|
dispatchActor(actor) {
|
|
3659
3770
|
if (actor.busy || !actor.alive || actor.drainingMailbox)
|
|
3660
3771
|
return;
|
|
@@ -3723,6 +3834,7 @@ class ExecutorV1 {
|
|
|
3723
3834
|
actor_id: actor.id,
|
|
3724
3835
|
handler: message.handler.name,
|
|
3725
3836
|
});
|
|
3837
|
+
let unwatchStream = () => { };
|
|
3726
3838
|
const frame = this.activate(this.functionAt(message.handler.callable), [actor.state, ...message.payload], null, null, undefined, null, {
|
|
3727
3839
|
id: actor.id,
|
|
3728
3840
|
actor,
|
|
@@ -3730,6 +3842,7 @@ class ExecutorV1 {
|
|
|
3730
3842
|
requestDeadline: message.requestDeadline,
|
|
3731
3843
|
reply: message.reply,
|
|
3732
3844
|
returned: (value) => {
|
|
3845
|
+
unwatchStream();
|
|
3733
3846
|
actor.active = null;
|
|
3734
3847
|
const drained = (fault) => {
|
|
3735
3848
|
actor.busy = false;
|
|
@@ -3765,6 +3878,7 @@ class ExecutorV1 {
|
|
|
3765
3878
|
}
|
|
3766
3879
|
},
|
|
3767
3880
|
failed: (fault) => {
|
|
3881
|
+
unwatchStream();
|
|
3768
3882
|
actor.active = null;
|
|
3769
3883
|
actor.busy = false;
|
|
3770
3884
|
if (fault.kind === "trap" &&
|
|
@@ -3785,6 +3899,13 @@ class ExecutorV1 {
|
|
|
3785
3899
|
},
|
|
3786
3900
|
});
|
|
3787
3901
|
actor.active = frame.context;
|
|
3902
|
+
if (message.handler.streams) {
|
|
3903
|
+
// StreamStart's checked producer protocol appends its output sink.
|
|
3904
|
+
const sink = message.payload.at(-1);
|
|
3905
|
+
if (sink?.kind !== "sink")
|
|
3906
|
+
throw new Error("actor stream producer has no final sink");
|
|
3907
|
+
unwatchStream = this.bindActorStream(frame.context, sink);
|
|
3908
|
+
}
|
|
3788
3909
|
this.runFrame(frame);
|
|
3789
3910
|
});
|
|
3790
3911
|
}
|
|
@@ -4038,7 +4159,7 @@ class ExecutorV1 {
|
|
|
4038
4159
|
wake();
|
|
4039
4160
|
}
|
|
4040
4161
|
}
|
|
4041
|
-
restartChild(owner, child, debts) {
|
|
4162
|
+
restartChild(owner, child, debts, delayElapsed = false) {
|
|
4042
4163
|
if (!owner.alive ||
|
|
4043
4164
|
owner.specs[child].restart === "temporary" ||
|
|
4044
4165
|
owner.spent.has(child)) {
|
|
@@ -4052,6 +4173,24 @@ class ExecutorV1 {
|
|
|
4052
4173
|
return;
|
|
4053
4174
|
}
|
|
4054
4175
|
const now = this.trace.virtualTimeMs;
|
|
4176
|
+
if (owner.backoff && debts.length > 0 && !delayElapsed) {
|
|
4177
|
+
const previous = owner.restartDelays.get(child);
|
|
4178
|
+
const healthy = previous?.publishedAt != null &&
|
|
4179
|
+
now - previous.publishedAt >= owner.layout.window_secs * 1000;
|
|
4180
|
+
const doubled = previous ? previous.delay * 2n : owner.backoff.initial;
|
|
4181
|
+
const delay = !previous || healthy
|
|
4182
|
+
? owner.backoff.initial
|
|
4183
|
+
: doubled < owner.backoff.maximum
|
|
4184
|
+
? doubled
|
|
4185
|
+
: owner.backoff.maximum;
|
|
4186
|
+
owner.restartDelays.set(child, { delay, publishedAt: null });
|
|
4187
|
+
owner.recovering.add(child);
|
|
4188
|
+
owner.restartTimers.set(child, this.scheduler.after(delay, () => {
|
|
4189
|
+
owner.restartTimers.delete(child);
|
|
4190
|
+
this.restartChild(owner, child, debts, true);
|
|
4191
|
+
}));
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4055
4194
|
owner.restartTimes = owner.restartTimes.filter((at) => now - at < owner.layout.window_secs * 1000);
|
|
4056
4195
|
if (owner.restartTimes.length >= owner.layout.max_restarts) {
|
|
4057
4196
|
this.escalateSupervisor(owner, debts);
|
|
@@ -4106,7 +4245,12 @@ class ExecutorV1 {
|
|
|
4106
4245
|
this.restartChild(owner, pending.child, pending.debts);
|
|
4107
4246
|
return;
|
|
4108
4247
|
}
|
|
4109
|
-
this.startChild(owner, index,
|
|
4248
|
+
this.startChild(owner, index, () => {
|
|
4249
|
+
const delay = owner.restartDelays.get(index);
|
|
4250
|
+
if (delay)
|
|
4251
|
+
delay.publishedAt = this.trace.virtualTimeMs;
|
|
4252
|
+
start();
|
|
4253
|
+
}, () => {
|
|
4110
4254
|
owner.restartBusy = false;
|
|
4111
4255
|
this.restartChild(owner, index, debts);
|
|
4112
4256
|
});
|
|
@@ -4182,6 +4326,9 @@ class ExecutorV1 {
|
|
|
4182
4326
|
if (!owner.alive)
|
|
4183
4327
|
return;
|
|
4184
4328
|
owner.alive = false;
|
|
4329
|
+
for (const cancel of owner.restartTimers.values())
|
|
4330
|
+
cancel();
|
|
4331
|
+
owner.restartTimers.clear();
|
|
4185
4332
|
if (owner.constructor) {
|
|
4186
4333
|
owner.constructor.pendingCancellation ??= {
|
|
4187
4334
|
kind: "panic",
|
|
@@ -659,7 +659,7 @@ export type RuntimeCallFamily = {
|
|
|
659
659
|
format: EncodingFormat;
|
|
660
660
|
op: EncodingOp;
|
|
661
661
|
};
|
|
662
|
-
} | "ActorDemonitor" | "ActorLink" | "ActorMonitor" | "ActorCaller" | "ActorRefusalLog" | "ActorUnlink" | "AutoMutexAlloc" | "AutoMutexFree" | "AutoMutexLock" | "AutoMutexUnlock" | "DequePopFront" | "DequePopBack" | "BytesAppend" | "BytesClear" | "BytesConcat" | "BytesContains" | "BytesDecodeUtf8" | "BytesDecodeUtf8Lossy" | "BytesEndsWith" | "BytesGet" | "BytesIndex" | "BytesIsEmpty" | "BytesLen" | "BytesPop" | "BytesPush" | "BytesSet" | "BytesSlice" | "BytesSliceFrom" | "BytesStartsWith" | "BytesNew" | "CancelTokenIsRequested" | "CancelTokenRelease" | "CancelTokenRetain" | "ActorRequestRelease" | "ActorCallFree" | "ActorRequestTake" | "DurationAbs" | "DurationHours" | "DurationIsZero" | "DurationMicros" | "DurationMillis" | "DurationMins" | "DurationNanos" | "DurationSecs" | "DynBoxAlloc" | "DynBoxFree" | "HashMapContainsKeyLayout" | "HashMapClearLayout" | "HashMapCloneLayout" | "HashMapEntriesLayout" | "HashMapFreeLayout" | "HashMapGetLayout" | "HashMapInsertLayout" | "HashMapKeysLayout" | "HashMapLenLayout" | "HashMapNew" | "HashMapNewWithLayout" | "HashMapRemoveLayout" | "HashMapValuesLayout" | "HashSetContainsLayout" | "HashSetClearLayout" | "HashSetCloneLayout" | "HashSetFreeLayout" | "HashSetInsertLayout" | "HashSetIsEmptyLayout" | "HashSetLenLayout" | "HashSetNew" | "HashSetNewWithLayout" | "HashSetRemoveLayout" | "HashSetToVecLayout" | "InstantDurationSince" | "InstantElapsed" | "InstantNow" | "TimestampNow" | {
|
|
662
|
+
} | "ActorDemonitor" | "ActorLink" | "ActorMonitor" | "ActorCaller" | "ActorRefusalLog" | "ActorUnlink" | "AutoMutexAlloc" | "AutoMutexFree" | "AutoMutexLock" | "AutoMutexUnlock" | "DequePopFront" | "DequePopBack" | "BytesAppend" | "BytesClear" | "BytesConcat" | "BytesContains" | "BytesDecodeUtf8" | "BytesDecodeUtf8Lossy" | "BytesEndsWith" | "BytesGet" | "BytesIndex" | "BytesIsEmpty" | "BytesLen" | "BytesPop" | "BytesPush" | "BytesSet" | "BytesSlice" | "BytesSliceFrom" | "BytesStartsWith" | "BytesNew" | "CancelTokenIsRequested" | "CancelTokenRelease" | "CancelTokenRetain" | "ContextActorId" | "ContextSupervisor" | "ContextTraceSpan" | "ActorRequestRelease" | "ActorCallFree" | "ActorRequestTake" | "DurationAbs" | "DurationHours" | "DurationIsZero" | "DurationMicros" | "DurationMillis" | "DurationMins" | "DurationNanos" | "DurationSecs" | "DynBoxAlloc" | "DynBoxFree" | "HashMapContainsKeyLayout" | "HashMapClearLayout" | "HashMapCloneLayout" | "HashMapEntriesLayout" | "HashMapFreeLayout" | "HashMapGetLayout" | "HashMapInsertLayout" | "HashMapKeysLayout" | "HashMapLenLayout" | "HashMapNew" | "HashMapNewWithLayout" | "HashMapRemoveLayout" | "HashMapValuesLayout" | "HashSetContainsLayout" | "HashSetClearLayout" | "HashSetCloneLayout" | "HashSetFreeLayout" | "HashSetInsertLayout" | "HashSetIsEmptyLayout" | "HashSetLenLayout" | "HashSetNew" | "HashSetNewWithLayout" | "HashSetRemoveLayout" | "HashSetToVecLayout" | "InstantDurationSince" | "InstantElapsed" | "InstantNow" | "TimestampNow" | {
|
|
663
663
|
MathIntrinsic: MathIntrinsic;
|
|
664
664
|
} | {
|
|
665
665
|
IntMethod: [IntBitOp, IntMethodWidth];
|
|
@@ -791,6 +791,7 @@ export type SupervisorShape = {
|
|
|
791
791
|
strategy: RestartStrategy;
|
|
792
792
|
max_restarts: number;
|
|
793
793
|
window_secs: number;
|
|
794
|
+
backoff?: boolean;
|
|
794
795
|
children: Array<SupervisorChild>;
|
|
795
796
|
startup_order?: Array<number>;
|
|
796
797
|
};
|