@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.
@@ -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: refused("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: refused("DurationAbs"),
537
- DurationHours: refused("DurationHours"),
538
- DurationIsZero: refused("DurationIsZero"),
539
- DurationMicros: refused("DurationMicros"),
540
- DurationMillis: refused("DurationMillis"),
541
- DurationMins: refused("DurationMins"),
542
- DurationNanos: refused("DurationNanos"),
543
- DurationSecs: refused("DurationSecs"),
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",
@@ -15,7 +15,7 @@ export interface RcCell {
15
15
  owner: RcOwner | null;
16
16
  typeName: string;
17
17
  }
18
- export type VmValue = {
18
+ export type VmValue = import("./v1/semaphores.js").SemaphoreHandle | {
19
19
  kind: "encoding";
20
20
  format: EncodingFormat;
21
21
  value: EncodingNode | null;
@@ -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.7.1",
3
+ "version": "0.8.0",
4
4
  "hewSource": {
5
- "commit": "7b489ef7392ed3baced2a4d119e1e4741995ece2",
6
- "tag": "v0.7.1",
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.7.1"
24
+ "@hew-lang/wasm": "0.8.0"
25
25
  },
26
26
  "keywords": [
27
27
  "hew",