@hew-lang/sandbox-vm 0.7.1 → 0.8.0
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 +28 -1
- package/dist/interpreter/v1/semaphores.d.ts +39 -0
- package/dist/interpreter/v1/semaphores.js +109 -0
- package/dist/interpreter/v1/shims.d.ts +16 -0
- package/dist/interpreter/v1/shims.js +24 -9
- package/dist/interpreter/values.d.ts +1 -1
- package/dist/interpreter/values.js +8 -0
- package/package.json +4 -4
|
@@ -16,6 +16,7 @@ import { StructuralRenderer } from "./structural.js";
|
|
|
16
16
|
import { timestampBinary } from "./timestamp.js";
|
|
17
17
|
import { selectedValue, collectionOperation, sharedOperation, } from "./value-operations.js";
|
|
18
18
|
import { Pipes } from "./pipes.js";
|
|
19
|
+
import { Semaphores } from "./semaphores.js";
|
|
19
20
|
import { FrameScheduler } from "./scheduler.js";
|
|
20
21
|
import { DeterministicIds } from "../../scheduler/ids.js";
|
|
21
22
|
import { Mt19937 } from "./mt19937.js";
|
|
@@ -128,6 +129,7 @@ class ExecutorV1 {
|
|
|
128
129
|
rootFault = null;
|
|
129
130
|
pkg;
|
|
130
131
|
directories;
|
|
132
|
+
semaphores;
|
|
131
133
|
constructor(admission, trace, stdin, policy, options) {
|
|
132
134
|
this.trace = trace;
|
|
133
135
|
this.stdin = stdin;
|
|
@@ -136,6 +138,7 @@ class ExecutorV1 {
|
|
|
136
138
|
this.pkg = pkg;
|
|
137
139
|
this.directories = new BrowserDirectories(options.directoryRoots);
|
|
138
140
|
this.scheduler = new FrameScheduler(trace, policy);
|
|
141
|
+
this.semaphores = new Semaphores((nanos, ready) => this.scheduler.after(nanos, ready));
|
|
139
142
|
this.ids = new DeterministicIds(trace.replay.seed);
|
|
140
143
|
this.host = {
|
|
141
144
|
argv: [...(options.argv ?? ["main.hew"])],
|
|
@@ -168,6 +171,7 @@ class ExecutorV1 {
|
|
|
168
171
|
}
|
|
169
172
|
},
|
|
170
173
|
pipes: this.pipes,
|
|
174
|
+
semaphores: this.semaphores,
|
|
171
175
|
generatorStream: (value) => {
|
|
172
176
|
const id = this.ids.channel();
|
|
173
177
|
this.generatorStreams.set(id, {
|
|
@@ -2080,6 +2084,25 @@ class ExecutorV1 {
|
|
|
2080
2084
|
}
|
|
2081
2085
|
return;
|
|
2082
2086
|
}
|
|
2087
|
+
case "SemaphoreAcquire": {
|
|
2088
|
+
const handle = this.boundary(act, term.inputs[0]);
|
|
2089
|
+
const timeout = term.detail.operation === "AcquireTimeout"
|
|
2090
|
+
? this.boundary(act, term.inputs[1])
|
|
2091
|
+
: null;
|
|
2092
|
+
if (timeout !== null && timeout.kind !== "i64")
|
|
2093
|
+
throw new Error("semaphore timeout must be an integer");
|
|
2094
|
+
let operation;
|
|
2095
|
+
const wake = this.park(act, term, () => { }, () => operation?.take());
|
|
2096
|
+
operation = this.semaphores.acquire(handle, timeout?.value ?? null, (acquired) => {
|
|
2097
|
+
wake(timeout === null ? UNIT : { kind: "bool", value: acquired });
|
|
2098
|
+
});
|
|
2099
|
+
const cancel = act.context.cancel;
|
|
2100
|
+
act.context.cancel = (fault) => {
|
|
2101
|
+
operation?.cancel();
|
|
2102
|
+
cancel?.(fault);
|
|
2103
|
+
};
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2083
2106
|
case "Sleep": {
|
|
2084
2107
|
const duration = term.inputs[0]
|
|
2085
2108
|
? this.boundary(act, term.inputs[0])
|
|
@@ -2113,7 +2136,7 @@ class ExecutorV1 {
|
|
|
2113
2136
|
message: `local actor wait cycle at ${operation}: ${path.map((actor) => actor.identity).join(" -> ")}`,
|
|
2114
2137
|
});
|
|
2115
2138
|
}
|
|
2116
|
-
park(act, term, cleanup = () => { }) {
|
|
2139
|
+
park(act, term, cleanup = () => { }, accepted = () => { }) {
|
|
2117
2140
|
this.running = false;
|
|
2118
2141
|
act.context.parked = true;
|
|
2119
2142
|
let pending = null;
|
|
@@ -2138,6 +2161,7 @@ class ExecutorV1 {
|
|
|
2138
2161
|
: term.unwind);
|
|
2139
2162
|
}
|
|
2140
2163
|
else {
|
|
2164
|
+
accepted();
|
|
2141
2165
|
if (term.result && term.result !== "never")
|
|
2142
2166
|
this.define(act, term.result.value, outcome.value);
|
|
2143
2167
|
const edge = term.resumes[outcome.edge];
|
|
@@ -2480,6 +2504,9 @@ class ExecutorV1 {
|
|
|
2480
2504
|
value.cell.weak -= 1;
|
|
2481
2505
|
}
|
|
2482
2506
|
break;
|
|
2507
|
+
case "semaphore":
|
|
2508
|
+
this.semaphores.close(value);
|
|
2509
|
+
break;
|
|
2483
2510
|
case "host_resource": {
|
|
2484
2511
|
if (value.owner.closed)
|
|
2485
2512
|
break;
|
|
@@ -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
|
+
}
|
|
@@ -16,6 +16,7 @@ export interface ShimHost {
|
|
|
16
16
|
codecBridge?: (request: string) => string;
|
|
17
17
|
encodingError?(format: EncodingFormat, message?: string): string;
|
|
18
18
|
pipes?: Pipes;
|
|
19
|
+
semaphores?: import("./semaphores.js").Semaphores;
|
|
19
20
|
newPipe?(capacity: number): VmValue;
|
|
20
21
|
generatorStream?(generator: VmValue): VmValue;
|
|
21
22
|
encoderSink?(sink: VmValue, encoder: VmValue): VmValue;
|
|
@@ -209,6 +210,21 @@ export declare function resolveSuspension(term: SuspendTerm): Refused | {
|
|
|
209
210
|
resumes: Array<import("./instructions.generated.js").Edge>;
|
|
210
211
|
cancel: import("./instructions.generated.js").Edge | null;
|
|
211
212
|
unwind: import("./instructions.generated.js").Edge | null;
|
|
213
|
+
} & {
|
|
214
|
+
kind: "SemaphoreAcquire";
|
|
215
|
+
detail: import("./instructions.generated.js").SemaphoreAcquireDetail;
|
|
216
|
+
}) | ({
|
|
217
|
+
span?: import("./instructions.generated.js").SpanV1 | null | null;
|
|
218
|
+
} & {
|
|
219
|
+
op: "suspend";
|
|
220
|
+
request_shapes?: import("./instructions.generated.js").RequestShapes | null | null;
|
|
221
|
+
result_shape?: number | null | null;
|
|
222
|
+
error_shape?: number | null | null;
|
|
223
|
+
inputs: Array<import("./instructions.generated.js").BoundaryOperand>;
|
|
224
|
+
result: import("./instructions.generated.js").CallResult;
|
|
225
|
+
resumes: Array<import("./instructions.generated.js").Edge>;
|
|
226
|
+
cancel: import("./instructions.generated.js").Edge | null;
|
|
227
|
+
unwind: import("./instructions.generated.js").Edge | null;
|
|
212
228
|
} & {
|
|
213
229
|
kind: "Ask";
|
|
214
230
|
detail: import("./instructions.generated.js").AskDetail;
|
|
@@ -188,7 +188,7 @@ const SUSPENSION_ROUTES = {
|
|
|
188
188
|
Accept: refused("Accept", true),
|
|
189
189
|
SleepUntil: refused("SleepUntil"),
|
|
190
190
|
ServiceAcquire: refused("ServiceAcquire"),
|
|
191
|
-
SemaphoreAcquire:
|
|
191
|
+
SemaphoreAcquire: "SemaphoreAcquire",
|
|
192
192
|
ActorSend: refused("ActorSend"),
|
|
193
193
|
RestartWait: refused("RestartWait"),
|
|
194
194
|
CallClosure: refused("CallClosure"),
|
|
@@ -533,14 +533,17 @@ const UNIT_FAMILIES = {
|
|
|
533
533
|
ActorRequestRelease: refused("ActorRequestRelease"),
|
|
534
534
|
ActorCallFree: refused("ActorCallFree"),
|
|
535
535
|
ActorRequestTake: refused("ActorRequestTake"),
|
|
536
|
-
DurationAbs:
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
536
|
+
DurationAbs: (_host, args) => {
|
|
537
|
+
const nanos = integer(args, 0);
|
|
538
|
+
return int(nanos === -(1n << 63n) ? (1n << 63n) - 1n : nanos < 0n ? -nanos : nanos);
|
|
539
|
+
},
|
|
540
|
+
DurationHours: (_host, args) => int(integer(args, 0) / 3600000000000n),
|
|
541
|
+
DurationIsZero: (_host, args) => bool(integer(args, 0) === 0n),
|
|
542
|
+
DurationMicros: (_host, args) => int(integer(args, 0) / 1000n),
|
|
543
|
+
DurationMillis: (_host, args) => int(integer(args, 0) / 1000000n),
|
|
544
|
+
DurationMins: (_host, args) => int(integer(args, 0) / 60000000000n),
|
|
545
|
+
DurationNanos: (_host, args) => int(integer(args, 0)),
|
|
546
|
+
DurationSecs: (_host, args) => int(integer(args, 0) / 1000000000n),
|
|
544
547
|
DynBoxAlloc: refused("DynBoxAlloc"),
|
|
545
548
|
DynBoxFree: refused("DynBoxFree"),
|
|
546
549
|
HashMapContainsKeyLayout: refused("HashMapContainsKeyLayout"),
|
|
@@ -1067,6 +1070,18 @@ function bigMax(left, right) {
|
|
|
1067
1070
|
}
|
|
1068
1071
|
// ── externs ─────────────────────────────────────────────────────────────────
|
|
1069
1072
|
const EXTERN_SHIMS = {
|
|
1073
|
+
hew_semaphore_new: (host, args) => host.semaphores.create(integer(args, 0)),
|
|
1074
|
+
hew_semaphore_clone: (host, args) => host.semaphores.clone(arg(args, 0)),
|
|
1075
|
+
hew_semaphore_try_acquire: (host, args) => bool(host.semaphores.tryAcquire(arg(args, 0))),
|
|
1076
|
+
hew_semaphore_count: (host, args) => int(host.semaphores.count(arg(args, 0))),
|
|
1077
|
+
hew_semaphore_release: (host, args) => {
|
|
1078
|
+
host.semaphores.release(arg(args, 0));
|
|
1079
|
+
return UNIT;
|
|
1080
|
+
},
|
|
1081
|
+
hew_semaphore_free: (host, args) => {
|
|
1082
|
+
host.semaphores.close(arg(args, 0));
|
|
1083
|
+
return UNIT;
|
|
1084
|
+
},
|
|
1070
1085
|
hew_path_separator: () => str("/"),
|
|
1071
1086
|
hew_calendar_format: {
|
|
1072
1087
|
kind: "ordinary",
|
|
@@ -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.8.0",
|
|
4
4
|
"hewSource": {
|
|
5
|
-
"commit": "
|
|
6
|
-
"tag": "v0.
|
|
5
|
+
"commit": "9322b962e46f92cc9c595b7cf742f73202751d1a",
|
|
6
|
+
"tag": "v0.8.0",
|
|
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.8.0"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"hew",
|