@hew-lang/sandbox-vm 0.6.0 → 0.6.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/README.md +5 -0
- package/dist/interpreter/run-program.js +1 -0
- package/dist/interpreter/strings.d.ts +21 -0
- package/dist/interpreter/strings.js +191 -0
- package/dist/interpreter/types.d.ts +3 -1
- package/dist/interpreter/unicode-data.d.ts +4 -0
- package/dist/interpreter/unicode-data.js +1673 -0
- package/dist/interpreter/unicode.d.ts +3 -0
- package/dist/interpreter/unicode.js +35 -0
- package/dist/interpreter/v1/actor-identity.d.ts +3 -0
- package/dist/interpreter/v1/actor-identity.js +7 -0
- package/dist/interpreter/v1/actor-waits.d.ts +6 -0
- package/dist/interpreter/v1/actor-waits.js +33 -0
- package/dist/interpreter/v1/codecs.js +6 -9
- package/dist/interpreter/v1/exec.js +1489 -332
- package/dist/interpreter/v1/package.d.ts +46 -6
- package/dist/interpreter/v1/pipes.d.ts +1 -0
- package/dist/interpreter/v1/pipes.js +9 -6
- package/dist/interpreter/v1/scheduler.d.ts +3 -0
- package/dist/interpreter/v1/scheduler.js +18 -8
- package/dist/interpreter/v1/shims.d.ts +10 -0
- package/dist/interpreter/v1/shims.js +243 -42
- package/dist/interpreter/v1/validate.js +103 -7
- package/dist/interpreter/v1/value-operations.d.ts +5 -2
- package/dist/interpreter/v1/value-operations.js +58 -23
- package/dist/interpreter/values.d.ts +32 -9
- package/dist/interpreter/values.js +42 -8
- package/package.json +4 -4
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { SUPPORTED_NATIVE_IO, SUPPORTED_SUSPEND_KINDS, resolveExternShim, resolveRuntimeShim, } from "./shims.js";
|
|
12
12
|
const UNSUPPORTED = "sandbox.capability.unsupported";
|
|
13
13
|
const NATIVE_FAMILIES = new Set(["AsyncIo", "FileRead", "Tcp"]);
|
|
14
|
-
const NATIVE_SUSPENSIONS = new Set(["NativeIo", "
|
|
14
|
+
const NATIVE_SUSPENSIONS = new Set(["NativeIo", "Read", "Accept"]);
|
|
15
15
|
const OPS = new Set([
|
|
16
16
|
"coverage.hit",
|
|
17
17
|
"const.int",
|
|
@@ -64,6 +64,7 @@ const OPS = new Set([
|
|
|
64
64
|
"task_scope.enter",
|
|
65
65
|
"task_scope.close",
|
|
66
66
|
"task.spawn",
|
|
67
|
+
"task.invoke",
|
|
67
68
|
"task.race",
|
|
68
69
|
]);
|
|
69
70
|
const TERMS = new Set([
|
|
@@ -92,6 +93,7 @@ const TERMS = new Set([
|
|
|
92
93
|
"unreachable",
|
|
93
94
|
]);
|
|
94
95
|
const ACTOR_OPS = new Set([
|
|
96
|
+
"identity",
|
|
95
97
|
"spawn",
|
|
96
98
|
"self_handle",
|
|
97
99
|
"stop",
|
|
@@ -102,6 +104,7 @@ const ACTOR_OPS = new Set([
|
|
|
102
104
|
"call_take",
|
|
103
105
|
"submit",
|
|
104
106
|
"stream_start",
|
|
107
|
+
"timer_schedule",
|
|
105
108
|
"supervisor_spawn",
|
|
106
109
|
"supervisor_child",
|
|
107
110
|
"supervisor_pool_view",
|
|
@@ -114,13 +117,11 @@ function capabilityMessage(capability, native) {
|
|
|
114
117
|
NativeIo: "Host input and output",
|
|
115
118
|
Read: "Host input and output",
|
|
116
119
|
Accept: "Accepting network connections",
|
|
117
|
-
RemoteAsk: "Remote actor calls",
|
|
118
120
|
};
|
|
119
121
|
const features = {
|
|
120
122
|
Vector: "This vector operation",
|
|
121
123
|
Map: "This map operation",
|
|
122
124
|
Set: "This set operation",
|
|
123
|
-
"actor.periodic": "Periodic actor handlers",
|
|
124
125
|
"actor.local_observation": "Actor links and monitors",
|
|
125
126
|
"actor.heap_limit": "Per-actor heap limits",
|
|
126
127
|
"actor.ingress_adapter": "Actor ingress adapters",
|
|
@@ -150,6 +151,7 @@ export function admitPackage(pkg) {
|
|
|
150
151
|
span: null,
|
|
151
152
|
};
|
|
152
153
|
}
|
|
154
|
+
const timerResources = new Set();
|
|
153
155
|
for (const function_ of pkg.functions) {
|
|
154
156
|
for (const block of function_.blocks) {
|
|
155
157
|
if (block.ops.some((op) => op.op === "actor.ingress_adapter"))
|
|
@@ -182,6 +184,66 @@ export function admitPackage(pkg) {
|
|
|
182
184
|
span: null,
|
|
183
185
|
};
|
|
184
186
|
}
|
|
187
|
+
const term = block.term;
|
|
188
|
+
if (term.op === "runtime.call" &&
|
|
189
|
+
pkg.runtime_families[term.family]?.family === "BytesDecodeUtf8") {
|
|
190
|
+
const shapes = term.variant_result_shapes;
|
|
191
|
+
const error = shapes && pkg.aggregates[shapes.error];
|
|
192
|
+
const length = shapes && pkg.variants[shapes.error_len];
|
|
193
|
+
const result = term.result_shape === null
|
|
194
|
+
? undefined
|
|
195
|
+
: pkg.variants[term.result_shape];
|
|
196
|
+
if (!error ||
|
|
197
|
+
error.fields.length !== 2 ||
|
|
198
|
+
!length ||
|
|
199
|
+
!result ||
|
|
200
|
+
length.runtime_tags?.OptionSome === undefined ||
|
|
201
|
+
length.runtime_tags?.OptionNone === undefined ||
|
|
202
|
+
result.runtime_tags?.ResultOk === undefined ||
|
|
203
|
+
result.runtime_tags?.ResultErr === undefined)
|
|
204
|
+
return {
|
|
205
|
+
category: "invalid_package",
|
|
206
|
+
code: "sandbox.package.variant_result_shapes",
|
|
207
|
+
capability: null,
|
|
208
|
+
message: "UTF-8 decode lacks its checked result carrier",
|
|
209
|
+
span: null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (term.op === "actor.call" && term.operation.op === "timer_schedule") {
|
|
213
|
+
const operation = term.operation;
|
|
214
|
+
const handler = pkg.actors
|
|
215
|
+
?.find((actor) => actor.id === operation.actor)
|
|
216
|
+
?.handlers.find((handler) => handler.message_id === operation.message);
|
|
217
|
+
const resource = pkg.resources?.[operation.resource];
|
|
218
|
+
if (!handler ||
|
|
219
|
+
handler.streams ||
|
|
220
|
+
term.args.length !== 5 ||
|
|
221
|
+
!["reject", "drop_newest"].includes(operation.policy) ||
|
|
222
|
+
resource?.kind !== "opaque" ||
|
|
223
|
+
resource.close === undefined ||
|
|
224
|
+
!pkg.functions.some((fn) => fn.id === resource.close))
|
|
225
|
+
return {
|
|
226
|
+
category: "invalid_package",
|
|
227
|
+
code: "sandbox.package.invalid_timer",
|
|
228
|
+
capability: "actor.timer_schedule",
|
|
229
|
+
message: "timer schedule lacks its checked handler, timing or non-waiting admission",
|
|
230
|
+
span: null,
|
|
231
|
+
};
|
|
232
|
+
timerResources.add(operation.resource);
|
|
233
|
+
}
|
|
234
|
+
if (term.op === "suspend") {
|
|
235
|
+
const mandatoryClose = (pkg.resources ?? []).some((resource) => resource.close === function_.id);
|
|
236
|
+
if (term.kind === "Safepoint"
|
|
237
|
+
? (term.cancel == null) !== mandatoryClose
|
|
238
|
+
: term.cancel == null)
|
|
239
|
+
return {
|
|
240
|
+
category: "invalid_package",
|
|
241
|
+
code: "sandbox.package.cancellation_ancestry",
|
|
242
|
+
capability: null,
|
|
243
|
+
message: "safepoint cancellation ancestry must match its authored close lifecycle",
|
|
244
|
+
span: null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
185
247
|
const unknown = block.ops.find((op) => !OPS.has(op.op))?.op ??
|
|
186
248
|
(!TERMS.has(block.term.op) ? block.term.op : null);
|
|
187
249
|
if (unknown)
|
|
@@ -194,7 +256,7 @@ export function admitPackage(pkg) {
|
|
|
194
256
|
};
|
|
195
257
|
}
|
|
196
258
|
}
|
|
197
|
-
for (const resource of pkg.resources ?? []) {
|
|
259
|
+
for (const [index, resource] of (pkg.resources ?? []).entries()) {
|
|
198
260
|
if (resource.kind === "record" &&
|
|
199
261
|
(resource.shape === undefined ||
|
|
200
262
|
!pkg.aggregates.some((shape) => shape.id === resource.shape)))
|
|
@@ -205,15 +267,49 @@ export function admitPackage(pkg) {
|
|
|
205
267
|
message: "record resource has no exact aggregate shape",
|
|
206
268
|
span: null,
|
|
207
269
|
};
|
|
208
|
-
if (resource.kind === "opaque")
|
|
270
|
+
if (resource.kind === "opaque" && !timerResources.has(index))
|
|
209
271
|
return unavailable("resource.opaque_close");
|
|
210
272
|
if (resource.kind === "nominal" &&
|
|
211
273
|
!resolveExternShim(resource.release ?? ""))
|
|
212
274
|
return unavailable("resource.external_close");
|
|
213
275
|
}
|
|
214
276
|
for (const actor of pkg.actors ?? []) {
|
|
215
|
-
if (actor.
|
|
216
|
-
return
|
|
277
|
+
if (typeof actor.name !== "string" || actor.name.length === 0)
|
|
278
|
+
return {
|
|
279
|
+
category: "invalid_package",
|
|
280
|
+
code: "sandbox.package.actor_name",
|
|
281
|
+
capability: null,
|
|
282
|
+
message: "actor lacks its declared diagnostic name",
|
|
283
|
+
span: null,
|
|
284
|
+
};
|
|
285
|
+
if (actor.crash != null &&
|
|
286
|
+
(actor.crash_info == null ||
|
|
287
|
+
!pkg.aggregates[actor.crash_info] ||
|
|
288
|
+
actor.crash_kind == null ||
|
|
289
|
+
!pkg.variants[actor.crash_kind] ||
|
|
290
|
+
actor.crash_action == null ||
|
|
291
|
+
!pkg.variants[actor.crash_action]))
|
|
292
|
+
return {
|
|
293
|
+
category: "invalid_package",
|
|
294
|
+
code: "sandbox.package.crash_hook_shapes",
|
|
295
|
+
capability: null,
|
|
296
|
+
message: "crash hook lacks its exact info, kind or action shape",
|
|
297
|
+
span: null,
|
|
298
|
+
};
|
|
299
|
+
if (actor.handlers.some((handler) => handler.every_ns != null &&
|
|
300
|
+
(!Number.isInteger(handler.every_ns) ||
|
|
301
|
+
handler.every_ns < 1_000_000 ||
|
|
302
|
+
handler.params !== 0 ||
|
|
303
|
+
handler.streams ||
|
|
304
|
+
handler.fallible ||
|
|
305
|
+
!pkg.functions.some((fn) => fn.id === handler.callable && fn.params.length === 1))))
|
|
306
|
+
return {
|
|
307
|
+
category: "invalid_package",
|
|
308
|
+
code: "sandbox.package.invalid_periodic",
|
|
309
|
+
capability: "actor.periodic",
|
|
310
|
+
message: "periodic handler lacks its checked interval or zero-argument body",
|
|
311
|
+
span: null,
|
|
312
|
+
};
|
|
217
313
|
if (actor.exit != null || actor.down != null)
|
|
218
314
|
return unavailable("actor.local_observation");
|
|
219
315
|
if (actor.max_heap_bytes != null)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { type VmValue } from "../values.js";
|
|
1
|
+
import { type VmValue, type RcOwner } from "../values.js";
|
|
2
2
|
import { type PackageV1 } from "./package.js";
|
|
3
3
|
export type ValueRequest = {
|
|
4
|
+
kind: "fault";
|
|
5
|
+
message: string;
|
|
6
|
+
} | {
|
|
4
7
|
kind: "call";
|
|
5
8
|
callee: number;
|
|
6
9
|
args: VmValue[];
|
|
@@ -13,7 +16,7 @@ export type ValueProgram = Generator<ValueRequest, VmValue, VmValue>;
|
|
|
13
16
|
/** Execute the exact checked plan, including every derived component selection.
|
|
14
17
|
* A user method yields to an ordinary VM frame and can suspend or fail. */
|
|
15
18
|
export declare function selectedValue(pkg: PackageV1, id: number, args: VmValue[]): ValueProgram;
|
|
16
|
-
export declare function sharedOperation(operation: string, args: VmValue[]): ValueProgram;
|
|
19
|
+
export declare function sharedOperation(operation: string, args: VmValue[], owner: RcOwner | null, typeName: string, option: (value: VmValue | null) => VmValue): ValueProgram;
|
|
17
20
|
/** Open-addressed collection operations preserve native callback ordering:
|
|
18
21
|
* growth hashes occupied slots before probing the incoming key. Staged slots
|
|
19
22
|
* borrow their owners until every fallible callback has completed. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { UNIT, cloneValue } from "../values.js";
|
|
1
|
+
import { UNIT, cloneValue, compareUtf8, } from "../values.js";
|
|
2
2
|
import { runtimeTag, } from "./package.js";
|
|
3
3
|
import { ShimFault } from "./shims.js";
|
|
4
|
+
import { actorIdentical } from "./actor-identity.js";
|
|
4
5
|
const OFFSET = 0xcbf29ce484222325n;
|
|
5
6
|
const PRIME = 0x100000001b3n;
|
|
6
7
|
const integer = (value) => ({
|
|
@@ -32,16 +33,6 @@ function orderingResult(capability, comparison) {
|
|
|
32
33
|
return boolean(comparison >= 0);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
function compareBytes(left, right) {
|
|
36
|
-
const length = Math.min(left.length, right.length);
|
|
37
|
-
for (let i = 0; i < length; i++) {
|
|
38
|
-
if (left[i] < right[i])
|
|
39
|
-
return -1;
|
|
40
|
-
if (left[i] > right[i])
|
|
41
|
-
return 1;
|
|
42
|
-
}
|
|
43
|
-
return left.length < right.length ? -1 : left.length > right.length ? 1 : 0;
|
|
44
|
-
}
|
|
45
36
|
function scalarOrdering(left, right, ty, capability) {
|
|
46
37
|
if (left.kind === "f64") {
|
|
47
38
|
if (right?.kind !== "f64" ||
|
|
@@ -113,7 +104,7 @@ export function* selectedValue(pkg, id, args) {
|
|
|
113
104
|
return boolean(false);
|
|
114
105
|
if (equality)
|
|
115
106
|
return boolean(left.value === right.value);
|
|
116
|
-
return orderingResult(ordering,
|
|
107
|
+
return orderingResult(ordering, compareUtf8(left.value, right.value));
|
|
117
108
|
}
|
|
118
109
|
if (plan.ty === "bytes") {
|
|
119
110
|
if (left.kind !== "vector")
|
|
@@ -127,10 +118,16 @@ export function* selectedValue(pkg, id, args) {
|
|
|
127
118
|
bytes.every((value, i) => BigInt(value) === bits(right.items[i], "u8")));
|
|
128
119
|
throw new Error("bytes ordering is outside the checked capability domain");
|
|
129
120
|
}
|
|
121
|
+
if (left.kind === "actor" || left.kind === "supervisor") {
|
|
122
|
+
if (equality)
|
|
123
|
+
return boolean(actorIdentical(left, right));
|
|
124
|
+
throw new Error("actor capability is outside the checked equality domain");
|
|
125
|
+
}
|
|
130
126
|
const components = plan.components ?? [];
|
|
131
127
|
let fields;
|
|
132
128
|
let others = [];
|
|
133
129
|
let selections;
|
|
130
|
+
let vectorHashLength;
|
|
134
131
|
if (left.kind === "record") {
|
|
135
132
|
fields = left.fields;
|
|
136
133
|
selections = components;
|
|
@@ -154,13 +151,20 @@ export function* selectedValue(pkg, id, args) {
|
|
|
154
151
|
selections = plan.variants?.[left.tag] ?? [];
|
|
155
152
|
}
|
|
156
153
|
else if (left.kind === "vector") {
|
|
157
|
-
if (hash)
|
|
158
|
-
throw new Error("derived vector Hash is outside the checked capability domain");
|
|
159
|
-
if (right?.kind !== "vector" || left.items.length !== right.items.length)
|
|
160
|
-
return boolean(false);
|
|
161
154
|
fields = left.items;
|
|
162
|
-
|
|
163
|
-
|
|
155
|
+
if (hash) {
|
|
156
|
+
const elementSelection = components[0];
|
|
157
|
+
if (!elementSelection)
|
|
158
|
+
throw new Error("derived vector Hash lacks its checked element selection");
|
|
159
|
+
selections = fields.map(() => elementSelection);
|
|
160
|
+
vectorHashLength = fields.length;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
if (right?.kind !== "vector" || left.items.length !== right.items.length)
|
|
164
|
+
return boolean(false);
|
|
165
|
+
others = right.items;
|
|
166
|
+
selections = fields.map(() => components[0]);
|
|
167
|
+
}
|
|
164
168
|
}
|
|
165
169
|
else {
|
|
166
170
|
if (hash)
|
|
@@ -169,7 +173,9 @@ export function* selectedValue(pkg, id, args) {
|
|
|
169
173
|
return boolean(!!right && bits(left, plan.ty) === bits(right, plan.ty));
|
|
170
174
|
return scalarOrdering(left, right, plan.ty, ordering);
|
|
171
175
|
}
|
|
172
|
-
let state =
|
|
176
|
+
let state = vectorHashLength === undefined
|
|
177
|
+
? OFFSET
|
|
178
|
+
: mix(OFFSET, BigInt(vectorHashLength));
|
|
173
179
|
for (const [at, field] of fields.entries()) {
|
|
174
180
|
const selection = selections[at];
|
|
175
181
|
if (!selection)
|
|
@@ -257,15 +263,44 @@ function option(pkg, shape, value) {
|
|
|
257
263
|
payload: value === undefined ? [] : [value],
|
|
258
264
|
};
|
|
259
265
|
}
|
|
260
|
-
export function* sharedOperation(operation, args) {
|
|
261
|
-
if (operation === "RcNew")
|
|
262
|
-
|
|
266
|
+
export function* sharedOperation(operation, args, owner, typeName, option) {
|
|
267
|
+
if (operation === "RcNew") {
|
|
268
|
+
if (owner?.closed)
|
|
269
|
+
throw new Error("Rc allocation after actor cleanup quiesced");
|
|
270
|
+
const cell = {
|
|
271
|
+
value: args[0],
|
|
272
|
+
refs: 1,
|
|
273
|
+
weak: 0,
|
|
274
|
+
dying: false,
|
|
275
|
+
owner,
|
|
276
|
+
typeName,
|
|
277
|
+
};
|
|
278
|
+
owner?.blocks.add(cell);
|
|
279
|
+
return { kind: "rc", cell, closed: false };
|
|
280
|
+
}
|
|
263
281
|
const receiver = args[0];
|
|
282
|
+
if (operation === "WeakCloneRc" || operation === "WeakUpgradeRc") {
|
|
283
|
+
if (receiver?.kind !== "weak" || receiver.closed)
|
|
284
|
+
throw new Error("shared operation has no live Weak");
|
|
285
|
+
if (operation === "WeakCloneRc")
|
|
286
|
+
return cloneValue(receiver);
|
|
287
|
+
if (receiver.cell.dying || receiver.cell.refs === 0)
|
|
288
|
+
return option(null);
|
|
289
|
+
receiver.cell.refs += 1;
|
|
290
|
+
return option({ kind: "rc", cell: receiver.cell, closed: false });
|
|
291
|
+
}
|
|
264
292
|
if (receiver?.kind !== "rc" || receiver.closed)
|
|
265
293
|
throw new Error("shared operation has no live Rc");
|
|
294
|
+
if (!receiver.cell || receiver.cell.dying) {
|
|
295
|
+
yield { kind: "fault", message: "Rc payload is being released" };
|
|
296
|
+
return UNIT;
|
|
297
|
+
}
|
|
266
298
|
switch (operation) {
|
|
267
299
|
case "RcClone":
|
|
268
300
|
return cloneValue(receiver);
|
|
301
|
+
case "RcDowngrade":
|
|
302
|
+
receiver.cell.weak += 1;
|
|
303
|
+
return { kind: "weak", cell: receiver.cell, closed: false };
|
|
269
304
|
case "RcGet":
|
|
270
305
|
return cloneValue(receiver.cell.value);
|
|
271
306
|
case "RcIsUnique":
|
|
@@ -273,7 +308,7 @@ export function* sharedOperation(operation, args) {
|
|
|
273
308
|
case "RcStrongCount":
|
|
274
309
|
return integer(BigInt(receiver.cell.refs));
|
|
275
310
|
case "RcWeakCount":
|
|
276
|
-
return integer(
|
|
311
|
+
return integer(BigInt(receiver.cell.weak));
|
|
277
312
|
case "RcSet": {
|
|
278
313
|
const old = receiver.cell.value;
|
|
279
314
|
receiver.cell.value = args[1];
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import type { JsonValue } from "./types.js";
|
|
2
|
+
import { IndexedString } from "./strings.js";
|
|
3
|
+
export { stringValue } from "./strings.js";
|
|
4
|
+
/** One creator-owned membership list; payload destruction uses the ordinary VM release path. */
|
|
5
|
+
export interface RcOwner {
|
|
6
|
+
blocks: Set<RcCell>;
|
|
7
|
+
closed: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface RcCell {
|
|
10
|
+
value: VmValue;
|
|
11
|
+
refs: number;
|
|
12
|
+
weak: number;
|
|
13
|
+
dying: boolean;
|
|
14
|
+
owner: RcOwner | null;
|
|
15
|
+
typeName: string;
|
|
16
|
+
}
|
|
2
17
|
export type VmValue = {
|
|
3
18
|
kind: "rc";
|
|
4
|
-
cell:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
19
|
+
cell: RcCell | null;
|
|
20
|
+
closed: boolean;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "weak";
|
|
23
|
+
cell: RcCell;
|
|
8
24
|
closed: boolean;
|
|
9
25
|
} | {
|
|
10
26
|
kind: "unit";
|
|
@@ -17,10 +33,7 @@ export type VmValue = {
|
|
|
17
33
|
} | {
|
|
18
34
|
kind: "f64";
|
|
19
35
|
value: number;
|
|
20
|
-
} | {
|
|
21
|
-
kind: "string";
|
|
22
|
-
value: string;
|
|
23
|
-
} | {
|
|
36
|
+
} | IndexedString | {
|
|
24
37
|
kind: "actor";
|
|
25
38
|
id: string;
|
|
26
39
|
} | {
|
|
@@ -38,6 +51,11 @@ export type VmValue = {
|
|
|
38
51
|
} | {
|
|
39
52
|
kind: "task";
|
|
40
53
|
id: string;
|
|
54
|
+
} | {
|
|
55
|
+
kind: "timer";
|
|
56
|
+
resource: number;
|
|
57
|
+
cancel(): void;
|
|
58
|
+
closed: boolean;
|
|
41
59
|
} | {
|
|
42
60
|
kind: "generator";
|
|
43
61
|
id: string;
|
|
@@ -102,10 +120,15 @@ export type VmValue = {
|
|
|
102
120
|
* owned by the closure, so `cloneValue` clones it. */
|
|
103
121
|
| {
|
|
104
122
|
kind: "closure";
|
|
105
|
-
|
|
123
|
+
entries: ReadonlyArray<{
|
|
124
|
+
body: number;
|
|
125
|
+
specialization: number | null;
|
|
126
|
+
}>;
|
|
127
|
+
specialization: number | null;
|
|
106
128
|
environment: VmValue;
|
|
107
129
|
};
|
|
108
130
|
export declare const UNIT: VmValue;
|
|
131
|
+
export declare function compareUtf8(a: string, b: string): number;
|
|
109
132
|
export declare function cloneValue(value: VmValue): VmValue;
|
|
110
133
|
export declare function valueFromLiteral(value: JsonValue): VmValue;
|
|
111
134
|
export declare function renderF64(value: number): string;
|
|
@@ -1,18 +1,41 @@
|
|
|
1
|
+
import { stringValue } from "./strings.js";
|
|
2
|
+
export { stringValue } from "./strings.js";
|
|
1
3
|
export const UNIT = { kind: "unit" };
|
|
4
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
5
|
+
export function compareUtf8(a, b) {
|
|
6
|
+
const left = UTF8_ENCODER.encode(a);
|
|
7
|
+
const right = UTF8_ENCODER.encode(b);
|
|
8
|
+
const length = Math.min(left.length, right.length);
|
|
9
|
+
for (let i = 0; i < length; i++) {
|
|
10
|
+
if (left[i] < right[i])
|
|
11
|
+
return -1;
|
|
12
|
+
if (left[i] > right[i])
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
return left.length < right.length ? -1 : left.length > right.length ? 1 : 0;
|
|
16
|
+
}
|
|
2
17
|
const I64_LITERAL_DECIMAL = /^(?:0|-?[1-9]\d*)$/;
|
|
3
18
|
export function cloneValue(value) {
|
|
4
19
|
switch (value.kind) {
|
|
5
20
|
case "rc":
|
|
6
21
|
if (value.closed)
|
|
7
22
|
throw new Error("cannot copy a released Rc");
|
|
23
|
+
if (!value.cell || value.cell.dying)
|
|
24
|
+
return { kind: "rc", cell: null, closed: false };
|
|
8
25
|
value.cell.refs += 1;
|
|
9
26
|
return { kind: "rc", cell: value.cell, closed: false };
|
|
27
|
+
case "weak":
|
|
28
|
+
if (value.closed)
|
|
29
|
+
throw new Error("cannot copy a released Weak");
|
|
30
|
+
value.cell.weak += 1;
|
|
31
|
+
return { kind: "weak", cell: value.cell, closed: false };
|
|
32
|
+
case "timer":
|
|
33
|
+
throw new Error("a timer cancellation handle cannot be copied");
|
|
10
34
|
case "unit":
|
|
11
35
|
return UNIT;
|
|
12
36
|
case "bool":
|
|
13
37
|
case "i64":
|
|
14
38
|
case "f64":
|
|
15
|
-
case "string":
|
|
16
39
|
case "actor":
|
|
17
40
|
case "supervisor":
|
|
18
41
|
case "monitor":
|
|
@@ -25,6 +48,8 @@ export function cloneValue(value) {
|
|
|
25
48
|
case "duplex":
|
|
26
49
|
case "function": // function values are immutable — identity clone
|
|
27
50
|
return { ...value };
|
|
51
|
+
case "string":
|
|
52
|
+
return value.clone();
|
|
28
53
|
case "regex":
|
|
29
54
|
return {
|
|
30
55
|
kind: "regex",
|
|
@@ -61,7 +86,8 @@ export function cloneValue(value) {
|
|
|
61
86
|
case "closure":
|
|
62
87
|
return {
|
|
63
88
|
kind: "closure",
|
|
64
|
-
|
|
89
|
+
entries: value.entries,
|
|
90
|
+
specialization: value.specialization,
|
|
65
91
|
environment: cloneValue(value.environment),
|
|
66
92
|
};
|
|
67
93
|
case "map":
|
|
@@ -92,9 +118,9 @@ export function valueFromLiteral(value) {
|
|
|
92
118
|
? { kind: "i64", value: BigInt(value) }
|
|
93
119
|
: { kind: "f64", value };
|
|
94
120
|
case "string":
|
|
95
|
-
return
|
|
121
|
+
return stringValue(value);
|
|
96
122
|
default:
|
|
97
|
-
return
|
|
123
|
+
return stringValue(canonicalJson(value));
|
|
98
124
|
}
|
|
99
125
|
}
|
|
100
126
|
function taggedI64Literal(value) {
|
|
@@ -178,6 +204,8 @@ export function renderF64(value) {
|
|
|
178
204
|
}
|
|
179
205
|
export function renderStdout(value) {
|
|
180
206
|
switch (value.kind) {
|
|
207
|
+
case "timer":
|
|
208
|
+
return "Timer";
|
|
181
209
|
case "unit":
|
|
182
210
|
return "()";
|
|
183
211
|
case "bool":
|
|
@@ -218,7 +246,9 @@ export function renderStdout(value) {
|
|
|
218
246
|
case "vector":
|
|
219
247
|
return canonicalJson(value.items.map((item) => toJsonValue(item)));
|
|
220
248
|
case "rc":
|
|
221
|
-
return
|
|
249
|
+
return "Rc";
|
|
250
|
+
case "weak":
|
|
251
|
+
return "Weak";
|
|
222
252
|
case "map":
|
|
223
253
|
return canonicalJson(mapEntriesJson(value));
|
|
224
254
|
case "dyn":
|
|
@@ -226,11 +256,13 @@ export function renderStdout(value) {
|
|
|
226
256
|
case "function":
|
|
227
257
|
return value.id;
|
|
228
258
|
case "closure":
|
|
229
|
-
return `closure:${value.body}`;
|
|
259
|
+
return `closure:${value.entries.find((entry) => entry.specialization === value.specialization)?.body ?? "generic"}`;
|
|
230
260
|
}
|
|
231
261
|
}
|
|
232
262
|
export function toJsonValue(value) {
|
|
233
263
|
switch (value.kind) {
|
|
264
|
+
case "timer":
|
|
265
|
+
return "Timer";
|
|
234
266
|
case "unit":
|
|
235
267
|
return null;
|
|
236
268
|
case "bool":
|
|
@@ -266,7 +298,9 @@ export function toJsonValue(value) {
|
|
|
266
298
|
case "vector":
|
|
267
299
|
return value.items.map(toJsonValue);
|
|
268
300
|
case "rc":
|
|
269
|
-
return
|
|
301
|
+
return "Rc";
|
|
302
|
+
case "weak":
|
|
303
|
+
return "Weak";
|
|
270
304
|
case "map":
|
|
271
305
|
return mapEntriesJson(value);
|
|
272
306
|
case "dyn":
|
|
@@ -274,7 +308,7 @@ export function toJsonValue(value) {
|
|
|
274
308
|
case "function":
|
|
275
309
|
return value.id;
|
|
276
310
|
case "closure":
|
|
277
|
-
return `closure:${value.body}`;
|
|
311
|
+
return `closure:${value.entries.find((entry) => entry.specialization === value.specialization)?.body ?? "generic"}`;
|
|
278
312
|
}
|
|
279
313
|
}
|
|
280
314
|
/// A map renders as its entries in insertion order, each a `[key, value]` pair.
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hew-lang/sandbox-vm",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"hewSource": {
|
|
5
|
-
"commit": "
|
|
6
|
-
"tag": "v0.6.
|
|
5
|
+
"commit": "dd0911184af12136eda7e89052e5b564d56ba643",
|
|
6
|
+
"tag": "v0.6.2",
|
|
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.6.
|
|
24
|
+
"@hew-lang/wasm": "0.6.2"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"hew",
|