@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.
@@ -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: refused("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: 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"),
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: refused("InstantDurationSince"),
571
- InstantElapsed: refused("InstantElapsed"),
572
- InstantNow: refused("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: () => str(""),
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) => {
@@ -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.9.1",
4
4
  "hewSource": {
5
- "commit": "7b489ef7392ed3baced2a4d119e1e4741995ece2",
6
- "tag": "v0.7.1",
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.7.1"
24
+ "@hew-lang/wasm": "0.9.1"
25
25
  },
26
26
  "keywords": [
27
27
  "hew",