@hew-lang/sandbox-vm 0.7.1 → 0.9.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/v1/exec.js +182 -8
- package/dist/interpreter/v1/instructions.generated.d.ts +2 -1
- package/dist/interpreter/v1/package-validator.generated.js +1507 -1315
- package/dist/interpreter/v1/semaphores.d.ts +39 -0
- package/dist/interpreter/v1/semaphores.js +109 -0
- package/dist/interpreter/v1/shims.d.ts +19 -1
- package/dist/interpreter/v1/shims.js +52 -13
- package/dist/interpreter/v1/validate.js +2 -1
- package/dist/interpreter/values.d.ts +1 -1
- package/dist/interpreter/values.js +8 -0
- package/package.json +4 -4
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { VmValue } from "../values.js";
|
|
2
|
+
interface State {
|
|
3
|
+
count: bigint;
|
|
4
|
+
waiting: Set<Acquisition>;
|
|
5
|
+
}
|
|
6
|
+
export interface SemaphoreHandle {
|
|
7
|
+
kind: "semaphore";
|
|
8
|
+
state: State;
|
|
9
|
+
closed: boolean;
|
|
10
|
+
}
|
|
11
|
+
type After = (nanos: bigint, ready: () => void) => () => void;
|
|
12
|
+
/** A released permit belongs to its selected waiter until the resume edge
|
|
13
|
+
* takes it. Cancellation before that edge hands the permit onwards. */
|
|
14
|
+
export declare class Acquisition {
|
|
15
|
+
private readonly state;
|
|
16
|
+
private readonly ready;
|
|
17
|
+
private status;
|
|
18
|
+
private cancelTimer;
|
|
19
|
+
constructor(state: State, ready: (acquired: boolean) => void);
|
|
20
|
+
start(timeoutMs: bigint | null, after: After): void;
|
|
21
|
+
reserve(): void;
|
|
22
|
+
take(): void;
|
|
23
|
+
cancel(): void;
|
|
24
|
+
private detach;
|
|
25
|
+
}
|
|
26
|
+
/** The VM executes the compiler's semaphore operations against one shared
|
|
27
|
+
* permit count. Explicit clones own independent handles, not independent counts. */
|
|
28
|
+
export declare class Semaphores {
|
|
29
|
+
private readonly after;
|
|
30
|
+
constructor(after: After);
|
|
31
|
+
create(count: bigint): VmValue;
|
|
32
|
+
clone(value: VmValue): VmValue;
|
|
33
|
+
close(value: VmValue): void;
|
|
34
|
+
count(value: VmValue): bigint;
|
|
35
|
+
tryAcquire(value: VmValue): boolean;
|
|
36
|
+
release(value: VmValue): void;
|
|
37
|
+
acquire(value: VmValue, timeoutMs: bigint | null, ready: (acquired: boolean) => void): Acquisition;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
function handle(value) {
|
|
2
|
+
if (value.kind !== "semaphore" || value.closed)
|
|
3
|
+
throw new Error("expected a live semaphore handle");
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
function release(state) {
|
|
7
|
+
const waiter = state.waiting.values().next().value;
|
|
8
|
+
if (waiter)
|
|
9
|
+
waiter.reserve();
|
|
10
|
+
else if (state.count < 2147483647n)
|
|
11
|
+
state.count += 1n;
|
|
12
|
+
}
|
|
13
|
+
/** A released permit belongs to its selected waiter until the resume edge
|
|
14
|
+
* takes it. Cancellation before that edge hands the permit onwards. */
|
|
15
|
+
export class Acquisition {
|
|
16
|
+
state;
|
|
17
|
+
ready;
|
|
18
|
+
status = "waiting";
|
|
19
|
+
cancelTimer = () => { };
|
|
20
|
+
constructor(state, ready) {
|
|
21
|
+
this.state = state;
|
|
22
|
+
this.ready = ready;
|
|
23
|
+
}
|
|
24
|
+
start(timeoutMs, after) {
|
|
25
|
+
if (this.state.count > 0n) {
|
|
26
|
+
this.state.count -= 1n;
|
|
27
|
+
this.reserve();
|
|
28
|
+
}
|
|
29
|
+
else if (timeoutMs !== null && timeoutMs <= 0n) {
|
|
30
|
+
this.status = "finished";
|
|
31
|
+
this.ready(false);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
this.state.waiting.add(this);
|
|
35
|
+
if (timeoutMs !== null)
|
|
36
|
+
this.cancelTimer = after(timeoutMs * 1000000n, () => {
|
|
37
|
+
if (this.status !== "waiting")
|
|
38
|
+
return;
|
|
39
|
+
this.detach();
|
|
40
|
+
this.status = "finished";
|
|
41
|
+
this.ready(false);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
reserve() {
|
|
46
|
+
this.detach();
|
|
47
|
+
this.status = "reserved";
|
|
48
|
+
this.ready(true);
|
|
49
|
+
}
|
|
50
|
+
take() {
|
|
51
|
+
if (this.status === "reserved")
|
|
52
|
+
this.status = "finished";
|
|
53
|
+
}
|
|
54
|
+
cancel() {
|
|
55
|
+
this.detach();
|
|
56
|
+
const reserved = this.status === "reserved";
|
|
57
|
+
this.status = "finished";
|
|
58
|
+
if (reserved)
|
|
59
|
+
release(this.state);
|
|
60
|
+
}
|
|
61
|
+
detach() {
|
|
62
|
+
this.state.waiting.delete(this);
|
|
63
|
+
this.cancelTimer();
|
|
64
|
+
this.cancelTimer = () => { };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** The VM executes the compiler's semaphore operations against one shared
|
|
68
|
+
* permit count. Explicit clones own independent handles, not independent counts. */
|
|
69
|
+
export class Semaphores {
|
|
70
|
+
after;
|
|
71
|
+
constructor(after) {
|
|
72
|
+
this.after = after;
|
|
73
|
+
}
|
|
74
|
+
create(count) {
|
|
75
|
+
if (count < 0n || count > 2147483647n)
|
|
76
|
+
throw new Error("semaphore capacity is outside the supported range");
|
|
77
|
+
return {
|
|
78
|
+
kind: "semaphore",
|
|
79
|
+
state: { count, waiting: new Set() },
|
|
80
|
+
closed: false,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
clone(value) {
|
|
84
|
+
return { ...handle(value), closed: false };
|
|
85
|
+
}
|
|
86
|
+
close(value) {
|
|
87
|
+
if (value.kind !== "semaphore")
|
|
88
|
+
throw new Error("expected a semaphore handle");
|
|
89
|
+
value.closed = true;
|
|
90
|
+
}
|
|
91
|
+
count(value) {
|
|
92
|
+
return handle(value).state.count;
|
|
93
|
+
}
|
|
94
|
+
tryAcquire(value) {
|
|
95
|
+
const state = handle(value).state;
|
|
96
|
+
if (state.count === 0n)
|
|
97
|
+
return false;
|
|
98
|
+
state.count -= 1n;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
release(value) {
|
|
102
|
+
release(handle(value).state);
|
|
103
|
+
}
|
|
104
|
+
acquire(value, timeoutMs, ready) {
|
|
105
|
+
const operation = new Acquisition(handle(value).state, ready);
|
|
106
|
+
operation.start(timeoutMs, this.after);
|
|
107
|
+
return operation;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -11,11 +11,14 @@ export declare class ShimFault extends Error {
|
|
|
11
11
|
export interface ShimHost {
|
|
12
12
|
readonly argv: readonly string[];
|
|
13
13
|
unixTimeMillis(): number;
|
|
14
|
+
monotonicTimeNanos(): bigint;
|
|
14
15
|
fileError(operation: "HasError" | "LastErrorKind" | "LastErrno" | "LastError"): VmValue;
|
|
16
|
+
setFileError(message: string | null): void;
|
|
15
17
|
calendarBridge?: (request: string) => string;
|
|
16
18
|
codecBridge?: (request: string) => string;
|
|
17
19
|
encodingError?(format: EncodingFormat, message?: string): string;
|
|
18
20
|
pipes?: Pipes;
|
|
21
|
+
semaphores?: import("./semaphores.js").Semaphores;
|
|
19
22
|
newPipe?(capacity: number): VmValue;
|
|
20
23
|
generatorStream?(generator: VmValue): VmValue;
|
|
21
24
|
encoderSink?(sink: VmValue, encoder: VmValue): VmValue;
|
|
@@ -47,7 +50,7 @@ type Ordinary = {
|
|
|
47
50
|
bridge?: BridgeRequirement;
|
|
48
51
|
};
|
|
49
52
|
export type RuntimeContinuation = {
|
|
50
|
-
handler: "actor_caller" | "actor_refusal_log" | "structural_format" | "utf8_decode" | "close";
|
|
53
|
+
handler: "actor_caller" | "context_actor_id" | "context_supervisor" | "context_trace_span" | "actor_refusal_log" | "structural_format" | "utf8_decode" | "close";
|
|
51
54
|
} | {
|
|
52
55
|
handler: "supervisor_pool";
|
|
53
56
|
operation: Extract<RuntimeFamilyEntry, {
|
|
@@ -209,6 +212,21 @@ export declare function resolveSuspension(term: SuspendTerm): Refused | {
|
|
|
209
212
|
resumes: Array<import("./instructions.generated.js").Edge>;
|
|
210
213
|
cancel: import("./instructions.generated.js").Edge | null;
|
|
211
214
|
unwind: import("./instructions.generated.js").Edge | null;
|
|
215
|
+
} & {
|
|
216
|
+
kind: "SemaphoreAcquire";
|
|
217
|
+
detail: import("./instructions.generated.js").SemaphoreAcquireDetail;
|
|
218
|
+
}) | ({
|
|
219
|
+
span?: import("./instructions.generated.js").SpanV1 | null | null;
|
|
220
|
+
} & {
|
|
221
|
+
op: "suspend";
|
|
222
|
+
request_shapes?: import("./instructions.generated.js").RequestShapes | null | null;
|
|
223
|
+
result_shape?: number | null | null;
|
|
224
|
+
error_shape?: number | null | null;
|
|
225
|
+
inputs: Array<import("./instructions.generated.js").BoundaryOperand>;
|
|
226
|
+
result: import("./instructions.generated.js").CallResult;
|
|
227
|
+
resumes: Array<import("./instructions.generated.js").Edge>;
|
|
228
|
+
cancel: import("./instructions.generated.js").Edge | null;
|
|
229
|
+
unwind: import("./instructions.generated.js").Edge | null;
|
|
212
230
|
} & {
|
|
213
231
|
kind: "Ask";
|
|
214
232
|
detail: import("./instructions.generated.js").AskDetail;
|
|
@@ -16,6 +16,22 @@ export class ShimFault extends Error {
|
|
|
16
16
|
this.name = "ShimFault";
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
function checkedWrite(host, value, stderr) {
|
|
20
|
+
try {
|
|
21
|
+
if (value.length !== 0) {
|
|
22
|
+
if (stderr)
|
|
23
|
+
host.writeStderr(value);
|
|
24
|
+
else
|
|
25
|
+
host.writeStdout(value);
|
|
26
|
+
}
|
|
27
|
+
host.setFileError(null);
|
|
28
|
+
return int(0n);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
host.setFileError(error instanceof Error ? error.message : String(error));
|
|
32
|
+
return int(-1n);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
19
35
|
function ordinary(shim) {
|
|
20
36
|
return { kind: "ordinary", shim };
|
|
21
37
|
}
|
|
@@ -188,7 +204,7 @@ const SUSPENSION_ROUTES = {
|
|
|
188
204
|
Accept: refused("Accept", true),
|
|
189
205
|
SleepUntil: refused("SleepUntil"),
|
|
190
206
|
ServiceAcquire: refused("ServiceAcquire"),
|
|
191
|
-
SemaphoreAcquire:
|
|
207
|
+
SemaphoreAcquire: "SemaphoreAcquire",
|
|
192
208
|
ActorSend: refused("ActorSend"),
|
|
193
209
|
RestartWait: refused("RestartWait"),
|
|
194
210
|
CallClosure: refused("CallClosure"),
|
|
@@ -519,6 +535,9 @@ const UNIT_FAMILIES = {
|
|
|
519
535
|
ActorLink: refused("ActorLink"),
|
|
520
536
|
ActorMonitor: refused("ActorMonitor"),
|
|
521
537
|
ActorCaller: resumable({ handler: "actor_caller" }),
|
|
538
|
+
ContextActorId: resumable({ handler: "context_actor_id" }),
|
|
539
|
+
ContextSupervisor: resumable({ handler: "context_supervisor" }),
|
|
540
|
+
ContextTraceSpan: resumable({ handler: "context_trace_span" }),
|
|
522
541
|
ActorRefusalLog: resumable({ handler: "actor_refusal_log" }),
|
|
523
542
|
ActorUnlink: refused("ActorUnlink"),
|
|
524
543
|
AutoMutexAlloc: refused("AutoMutexAlloc"),
|
|
@@ -533,14 +552,17 @@ const UNIT_FAMILIES = {
|
|
|
533
552
|
ActorRequestRelease: refused("ActorRequestRelease"),
|
|
534
553
|
ActorCallFree: refused("ActorCallFree"),
|
|
535
554
|
ActorRequestTake: refused("ActorRequestTake"),
|
|
536
|
-
DurationAbs:
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
555
|
+
DurationAbs: (_host, args) => {
|
|
556
|
+
const nanos = integer(args, 0);
|
|
557
|
+
return int(nanos === -(1n << 63n) ? (1n << 63n) - 1n : nanos < 0n ? -nanos : nanos);
|
|
558
|
+
},
|
|
559
|
+
DurationHours: (_host, args) => int(integer(args, 0) / 3600000000000n),
|
|
560
|
+
DurationIsZero: (_host, args) => bool(integer(args, 0) === 0n),
|
|
561
|
+
DurationMicros: (_host, args) => int(integer(args, 0) / 1000n),
|
|
562
|
+
DurationMillis: (_host, args) => int(integer(args, 0) / 1000000n),
|
|
563
|
+
DurationMins: (_host, args) => int(integer(args, 0) / 60000000000n),
|
|
564
|
+
DurationNanos: (_host, args) => int(integer(args, 0)),
|
|
565
|
+
DurationSecs: (_host, args) => int(integer(args, 0) / 1000000000n),
|
|
544
566
|
DynBoxAlloc: refused("DynBoxAlloc"),
|
|
545
567
|
DynBoxFree: refused("DynBoxFree"),
|
|
546
568
|
HashMapContainsKeyLayout: refused("HashMapContainsKeyLayout"),
|
|
@@ -567,9 +589,9 @@ const UNIT_FAMILIES = {
|
|
|
567
589
|
HashSetNewWithLayout: refused("HashSetNewWithLayout"),
|
|
568
590
|
HashSetRemoveLayout: refused("HashSetRemoveLayout"),
|
|
569
591
|
HashSetToVecLayout: refused("HashSetToVecLayout"),
|
|
570
|
-
InstantDurationSince:
|
|
571
|
-
InstantElapsed:
|
|
572
|
-
InstantNow:
|
|
592
|
+
InstantDurationSince: (_host, args) => int(saturatingI64(integer(args, 0) - integer(args, 1))),
|
|
593
|
+
InstantElapsed: (host, args) => int(saturatingI64(saturatingI64(host.monotonicTimeNanos()) - integer(args, 0))),
|
|
594
|
+
InstantNow: (host) => int(saturatingI64(host.monotonicTimeNanos())),
|
|
573
595
|
NodeConnect: refused("NodeConnect"),
|
|
574
596
|
NodeId: refused("NodeId"),
|
|
575
597
|
NodeIdDisplay: refused("NodeIdDisplay"),
|
|
@@ -1065,8 +1087,23 @@ function bigMin(left, right) {
|
|
|
1065
1087
|
function bigMax(left, right) {
|
|
1066
1088
|
return right > left ? right : left;
|
|
1067
1089
|
}
|
|
1090
|
+
function saturatingI64(value) {
|
|
1091
|
+
return bigMax(-(1n << 63n), bigMin((1n << 63n) - 1n, value));
|
|
1092
|
+
}
|
|
1068
1093
|
// ── externs ─────────────────────────────────────────────────────────────────
|
|
1069
1094
|
const EXTERN_SHIMS = {
|
|
1095
|
+
hew_semaphore_new: (host, args) => host.semaphores.create(integer(args, 0)),
|
|
1096
|
+
hew_semaphore_clone: (host, args) => host.semaphores.clone(arg(args, 0)),
|
|
1097
|
+
hew_semaphore_try_acquire: (host, args) => bool(host.semaphores.tryAcquire(arg(args, 0))),
|
|
1098
|
+
hew_semaphore_count: (host, args) => int(host.semaphores.count(arg(args, 0))),
|
|
1099
|
+
hew_semaphore_release: (host, args) => {
|
|
1100
|
+
host.semaphores.release(arg(args, 0));
|
|
1101
|
+
return UNIT;
|
|
1102
|
+
},
|
|
1103
|
+
hew_semaphore_free: (host, args) => {
|
|
1104
|
+
host.semaphores.close(arg(args, 0));
|
|
1105
|
+
return UNIT;
|
|
1106
|
+
},
|
|
1070
1107
|
hew_path_separator: () => str("/"),
|
|
1071
1108
|
hew_calendar_format: {
|
|
1072
1109
|
kind: "ordinary",
|
|
@@ -1116,7 +1153,9 @@ const EXTERN_SHIMS = {
|
|
|
1116
1153
|
host.pipes.freePair(arg(args, 0));
|
|
1117
1154
|
return UNIT;
|
|
1118
1155
|
},
|
|
1119
|
-
hew_stream_last_error: () =>
|
|
1156
|
+
hew_stream_last_error: (host) => host.fileError("LastError"),
|
|
1157
|
+
hew_io_write: (host, args) => checkedWrite(host, text(args, 0), false),
|
|
1158
|
+
hew_io_write_err: (host, args) => checkedWrite(host, text(args, 0), true),
|
|
1120
1159
|
hew_regex_new: (host, args) => compileRegex(host, args),
|
|
1121
1160
|
hew_regex_clone: (_host, args) => cloneValue(arg(args, 0)),
|
|
1122
1161
|
// Releasing a handle the VM traces by reference is nothing to do.
|
|
@@ -209,7 +209,8 @@ export function admitPackage(pkg, bridges = {}) {
|
|
|
209
209
|
const id = term.operation.supervisor;
|
|
210
210
|
const supervisor = pkg.supervisors?.find((layout) => layout.id === id);
|
|
211
211
|
const config = term.args.length -
|
|
212
|
-
(supervisor?.children.some((child) => child.is_pool) ? 1 : 0)
|
|
212
|
+
(supervisor?.children.some((child) => child.is_pool) ? 1 : 0) -
|
|
213
|
+
(supervisor?.backoff ? 2 : 0);
|
|
213
214
|
if (!supervisor ||
|
|
214
215
|
config < 0 ||
|
|
215
216
|
supervisor.children.some((child) => {
|
|
@@ -33,6 +33,10 @@ export function cloneValue(value) {
|
|
|
33
33
|
return { kind: "weak", cell: value.cell, closed: false };
|
|
34
34
|
case "host_resource":
|
|
35
35
|
throw new Error("a filesystem owner cannot be copied");
|
|
36
|
+
case "semaphore":
|
|
37
|
+
// The opaque ABI pointer is copied for calls borrowing the enclosing
|
|
38
|
+
// affine Semaphore. Only its explicit clone operation creates an owner.
|
|
39
|
+
return value;
|
|
36
40
|
case "timer":
|
|
37
41
|
throw new Error("a timer cancellation handle cannot be copied");
|
|
38
42
|
case "unit":
|
|
@@ -208,6 +212,8 @@ export function renderF64(value) {
|
|
|
208
212
|
}
|
|
209
213
|
export function renderStdout(value) {
|
|
210
214
|
switch (value.kind) {
|
|
215
|
+
case "semaphore":
|
|
216
|
+
return "Semaphore";
|
|
211
217
|
case "encoding":
|
|
212
218
|
return JSON.stringify(value.value);
|
|
213
219
|
case "host_resource":
|
|
@@ -269,6 +275,8 @@ export function renderStdout(value) {
|
|
|
269
275
|
}
|
|
270
276
|
export function toJsonValue(value) {
|
|
271
277
|
switch (value.kind) {
|
|
278
|
+
case "semaphore":
|
|
279
|
+
return "Semaphore";
|
|
272
280
|
case "encoding":
|
|
273
281
|
return value.value;
|
|
274
282
|
case "host_resource":
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hew-lang/sandbox-vm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"hewSource": {
|
|
5
|
-
"commit": "
|
|
6
|
-
"tag": "v0.
|
|
5
|
+
"commit": "17e06579cb14a167be1e369bdda5388e14e95f80",
|
|
6
|
+
"tag": "v0.9.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.
|
|
24
|
+
"@hew-lang/wasm": "0.9.1"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"hew",
|