@hew-lang/sandbox-vm 0.9.3 → 0.10.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/run-program.js +5 -0
- package/dist/interpreter/strings.d.ts +6 -0
- package/dist/interpreter/strings.js +8 -0
- package/dist/interpreter/types.d.ts +1 -1
- package/dist/interpreter/v1/browser-directory.js +2 -2
- package/dist/interpreter/v1/codecs.js +2 -2
- package/dist/interpreter/v1/encoding-storage.d.ts +33 -0
- package/dist/interpreter/v1/encoding-storage.js +113 -0
- package/dist/interpreter/v1/encoding-values.js +28 -18
- package/dist/interpreter/v1/exec.js +1506 -130
- package/dist/interpreter/v1/instructions.generated.d.ts +100 -8
- package/dist/interpreter/v1/instructions.generated.js +1 -0
- package/dist/interpreter/v1/managed-budget.d.ts +88 -0
- package/dist/interpreter/v1/managed-budget.js +561 -0
- package/dist/interpreter/v1/package-graph.js +77 -2
- package/dist/interpreter/v1/package-validator.generated.js +22796 -17531
- package/dist/interpreter/v1/package.d.ts +4 -2
- package/dist/interpreter/v1/package.js +6 -0
- package/dist/interpreter/v1/pipes.d.ts +24 -1
- package/dist/interpreter/v1/pipes.js +91 -8
- package/dist/interpreter/v1/shims.d.ts +1 -0
- package/dist/interpreter/v1/shims.js +88 -62
- package/dist/interpreter/v1/structural.js +4 -2
- package/dist/interpreter/v1/validate.js +4 -2
- package/dist/interpreter/v1/value-operations.d.ts +3 -1
- package/dist/interpreter/v1/value-operations.js +32 -18
- package/dist/interpreter/values.d.ts +45 -11
- package/dist/interpreter/values.js +176 -8
- package/package.json +4 -4
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { vectorElement, vectorItems, vectorLength, vectorStorage, } from "../values.js";
|
|
2
|
+
import { cloneEncoding } from "./encoding-storage.js";
|
|
3
|
+
const U64_MAX = 0xffffffffffffffffn;
|
|
4
|
+
export class HeapExceeded extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("actor heap limit exceeded");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Compiler-authored widths and children describe payloads; VM backings supply
|
|
10
|
+
* identity and capacity. No host-object or allocator size enters this graph. */
|
|
11
|
+
export class ManagedLayouts {
|
|
12
|
+
rows;
|
|
13
|
+
retained;
|
|
14
|
+
types = new WeakMap();
|
|
15
|
+
retired = new WeakSet();
|
|
16
|
+
adoption = new Map();
|
|
17
|
+
constructor(rows, retained) {
|
|
18
|
+
this.rows = rows;
|
|
19
|
+
this.retained = retained;
|
|
20
|
+
}
|
|
21
|
+
row(type) {
|
|
22
|
+
const row = this.rows[type];
|
|
23
|
+
if (!row)
|
|
24
|
+
throw new Error(`managed type ${type} is absent`);
|
|
25
|
+
return row;
|
|
26
|
+
}
|
|
27
|
+
stamp(value, type) {
|
|
28
|
+
this.types.set(value, type);
|
|
29
|
+
const row = this.row(type);
|
|
30
|
+
if (row.kind === "callable" &&
|
|
31
|
+
row.captures &&
|
|
32
|
+
(value.kind === "closure" || value.kind === "record"))
|
|
33
|
+
value.managedCaptures = row.captures;
|
|
34
|
+
if (row.kind === "dynamic" && value.kind === "dyn")
|
|
35
|
+
value.managedPayload ??= this.types.get(value.value);
|
|
36
|
+
if (value.kind === "vector") {
|
|
37
|
+
const storage = value.storage ?? vectorStorage(value);
|
|
38
|
+
if (row.kind === "bytes") {
|
|
39
|
+
storage.bytes = true;
|
|
40
|
+
storage.shareOnCopy = true;
|
|
41
|
+
}
|
|
42
|
+
else if (row.kind === "vector" ||
|
|
43
|
+
row.kind === "array" ||
|
|
44
|
+
row.kind === "slice") {
|
|
45
|
+
storage.shareOnCopy = row.share_on_copy;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
width(type, capacity = 1) {
|
|
50
|
+
return checked(BigInt(this.row(type).slot_bytes) * BigInt(capacity));
|
|
51
|
+
}
|
|
52
|
+
/** Propagate nested compiler descriptors before an unbounded value is copied
|
|
53
|
+
* or suspended. Unknown foreign payloads need no quota graph here; acquiring
|
|
54
|
+
* the same value into a bounded owner still requires its real visitor. */
|
|
55
|
+
stampGraph(root) {
|
|
56
|
+
this.collect(root, new Map(), undefined, undefined, undefined, (value, type) => {
|
|
57
|
+
const row = this.row(type);
|
|
58
|
+
if (row.kind === "provider" && row.provider === "opaque")
|
|
59
|
+
return { identity: value, bytes: 0n, children: [] };
|
|
60
|
+
if (!this.retained)
|
|
61
|
+
throw new Error("managed provider has no retained-value visitor");
|
|
62
|
+
return this.retained(value, type);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
needsAdoption(type) {
|
|
66
|
+
const cached = this.adoption.get(type);
|
|
67
|
+
if (cached != null)
|
|
68
|
+
return cached;
|
|
69
|
+
const active = new Set();
|
|
70
|
+
const contains = (type) => {
|
|
71
|
+
if (active.has(type))
|
|
72
|
+
return false;
|
|
73
|
+
active.add(type);
|
|
74
|
+
const row = this.row(type);
|
|
75
|
+
switch (row.kind) {
|
|
76
|
+
case "generator":
|
|
77
|
+
case "actor_call":
|
|
78
|
+
case "dynamic":
|
|
79
|
+
return true;
|
|
80
|
+
case "provider":
|
|
81
|
+
return ["task", "stream", "sink"].includes(row.provider);
|
|
82
|
+
case "callable":
|
|
83
|
+
return row.captures == null || row.captures.some(contains);
|
|
84
|
+
case "aggregate":
|
|
85
|
+
return row.fields.some(contains);
|
|
86
|
+
case "variant":
|
|
87
|
+
return row.cases.some((fields) => fields.some(contains));
|
|
88
|
+
case "shared":
|
|
89
|
+
return !row.weak;
|
|
90
|
+
case "map":
|
|
91
|
+
return contains(row.key) || contains(row.value);
|
|
92
|
+
case "array":
|
|
93
|
+
case "vector":
|
|
94
|
+
case "slice":
|
|
95
|
+
case "set":
|
|
96
|
+
return contains(row.element);
|
|
97
|
+
default:
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const result = contains(type);
|
|
102
|
+
this.adoption.set(type, result);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
retire(value) {
|
|
106
|
+
this.retired.add(value);
|
|
107
|
+
}
|
|
108
|
+
type(value) {
|
|
109
|
+
const type = this.types.get(value);
|
|
110
|
+
if (type == null)
|
|
111
|
+
throw new Error("managed payload has no compiler descriptor");
|
|
112
|
+
return type;
|
|
113
|
+
}
|
|
114
|
+
/** Describe native copy allocations without retaining or changing inputs. */
|
|
115
|
+
copy(value, type) {
|
|
116
|
+
const row = this.row(type);
|
|
117
|
+
this.stamp(value, type);
|
|
118
|
+
switch (row.kind) {
|
|
119
|
+
case "scalar":
|
|
120
|
+
case "handle":
|
|
121
|
+
case "string":
|
|
122
|
+
case "bytes":
|
|
123
|
+
case "shared":
|
|
124
|
+
return value;
|
|
125
|
+
case "array":
|
|
126
|
+
case "vector":
|
|
127
|
+
case "slice": {
|
|
128
|
+
if (value.kind !== "vector")
|
|
129
|
+
throw new Error("managed copy has no sequence");
|
|
130
|
+
const storage = vectorStorage(value);
|
|
131
|
+
if (row.share_on_copy && vectorLength(value))
|
|
132
|
+
return value;
|
|
133
|
+
const items = vectorItems(value).map((item) => this.copy(item, row.element));
|
|
134
|
+
return {
|
|
135
|
+
...value,
|
|
136
|
+
items,
|
|
137
|
+
offset: 0,
|
|
138
|
+
length: undefined,
|
|
139
|
+
storage: { ...storage, items, capacity: items.length, owners: 1 },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
case "aggregate":
|
|
143
|
+
if (value.kind !== "record")
|
|
144
|
+
throw new Error("managed copy has no fields");
|
|
145
|
+
return {
|
|
146
|
+
...value,
|
|
147
|
+
fields: value.fields.map((item, index) => this.copy(item, row.fields[index])),
|
|
148
|
+
};
|
|
149
|
+
case "variant":
|
|
150
|
+
if (value.kind !== "enum")
|
|
151
|
+
throw new Error("managed copy has no variant");
|
|
152
|
+
return {
|
|
153
|
+
...value,
|
|
154
|
+
payload: value.payload.map((item, index) => this.copy(item, row.cases[value.tag][index])),
|
|
155
|
+
};
|
|
156
|
+
case "callable": {
|
|
157
|
+
if (value.kind === "function")
|
|
158
|
+
return value;
|
|
159
|
+
const environment = value.kind === "record"
|
|
160
|
+
? value
|
|
161
|
+
: value.kind === "closure" && value.environment.kind === "record"
|
|
162
|
+
? value.environment
|
|
163
|
+
: null;
|
|
164
|
+
if (!environment ||
|
|
165
|
+
(value.kind !== "closure" && value.kind !== "record"))
|
|
166
|
+
throw new Error("managed copy has no captures");
|
|
167
|
+
const captures = row.captures ?? value.managedCaptures;
|
|
168
|
+
if (!captures)
|
|
169
|
+
throw new Error("callable has no concrete capture descriptor");
|
|
170
|
+
const copied = {
|
|
171
|
+
...environment,
|
|
172
|
+
fields: environment.fields.map((item, index) => this.copy(item, captures[index])),
|
|
173
|
+
};
|
|
174
|
+
const result = value.kind === "record" ? copied : { ...value, environment: copied };
|
|
175
|
+
if (result.kind === "closure" || result.kind === "record")
|
|
176
|
+
result.managedCaptures = captures;
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
case "map":
|
|
180
|
+
case "set": {
|
|
181
|
+
if (value.kind !== "map")
|
|
182
|
+
throw new Error("managed copy has no map");
|
|
183
|
+
const key = row.kind === "map" ? row.key : row.element;
|
|
184
|
+
return {
|
|
185
|
+
...value,
|
|
186
|
+
entries: new Map([...value.entries].map(([slot, entry]) => [
|
|
187
|
+
slot,
|
|
188
|
+
{
|
|
189
|
+
key: this.copy(entry.key, key),
|
|
190
|
+
value: row.kind === "map"
|
|
191
|
+
? this.copy(entry.value, row.value)
|
|
192
|
+
: entry.value,
|
|
193
|
+
},
|
|
194
|
+
])),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
case "dynamic":
|
|
198
|
+
if (value.kind !== "dyn")
|
|
199
|
+
throw new Error("managed copy has no box");
|
|
200
|
+
return {
|
|
201
|
+
...value,
|
|
202
|
+
value: this.copy(value.value, value.managedPayload ?? this.type(value.value)),
|
|
203
|
+
};
|
|
204
|
+
case "generator":
|
|
205
|
+
throw new Error("a generator cannot be copied");
|
|
206
|
+
case "actor_call":
|
|
207
|
+
return value;
|
|
208
|
+
case "provider":
|
|
209
|
+
if (row.provider === "json" ||
|
|
210
|
+
row.provider === "yaml" ||
|
|
211
|
+
row.provider === "toml") {
|
|
212
|
+
if (value.kind !== "encoding")
|
|
213
|
+
throw new Error("managed encoding has no tree");
|
|
214
|
+
return cloneEncoding(value, true);
|
|
215
|
+
}
|
|
216
|
+
if (row.provider === "task" || row.provider === "sink")
|
|
217
|
+
return value;
|
|
218
|
+
throw new HeapExceeded();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
collect(root, allocations, substitution, stringGrowth, retained, providerGraph = this.retained, storageVisitor) {
|
|
222
|
+
const active = new Set();
|
|
223
|
+
const allocation = (identity, bytes, children = () => { }) => {
|
|
224
|
+
checked(bytes);
|
|
225
|
+
if (storageVisitor) {
|
|
226
|
+
storageVisitor(identity, bytes, children);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const prior = allocations.get(identity);
|
|
230
|
+
if (prior != null && prior !== bytes)
|
|
231
|
+
throw new Error("managed storage has conflicting widths");
|
|
232
|
+
if (prior != null || active.has(identity))
|
|
233
|
+
return;
|
|
234
|
+
allocations.set(identity, bytes);
|
|
235
|
+
active.add(identity);
|
|
236
|
+
children();
|
|
237
|
+
active.delete(identity);
|
|
238
|
+
};
|
|
239
|
+
const visit = (value, type) => {
|
|
240
|
+
if (substitution?.before === value)
|
|
241
|
+
value = substitution.after;
|
|
242
|
+
if (this.retired.has(value))
|
|
243
|
+
return;
|
|
244
|
+
if (value.kind === "unit")
|
|
245
|
+
return;
|
|
246
|
+
this.stamp(value, type);
|
|
247
|
+
const row = this.row(type);
|
|
248
|
+
switch (row.kind) {
|
|
249
|
+
case "scalar":
|
|
250
|
+
case "handle":
|
|
251
|
+
return;
|
|
252
|
+
case "string": {
|
|
253
|
+
if (value.kind !== "string")
|
|
254
|
+
throw new Error("managed string has no UTF-8 backing");
|
|
255
|
+
if (stringGrowth?.value === value) {
|
|
256
|
+
allocation(stringGrowth.identity, BigInt(stringGrowth.capacity));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const storage = value.managedStorage();
|
|
260
|
+
if (storage.capacity)
|
|
261
|
+
allocation(storage.identity, BigInt(storage.capacity));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
case "bytes": {
|
|
265
|
+
if (value.kind !== "vector")
|
|
266
|
+
throw new Error("managed bytes have no backing");
|
|
267
|
+
const storage = value.storage ?? vectorStorage(value);
|
|
268
|
+
if (storage.capacity)
|
|
269
|
+
allocation(storage, BigInt(storage.capacity));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
case "vector":
|
|
273
|
+
case "array":
|
|
274
|
+
case "slice": {
|
|
275
|
+
if (value.kind !== "vector")
|
|
276
|
+
throw new Error("managed sequence has no backing");
|
|
277
|
+
const storage = value.storage ?? vectorStorage(value);
|
|
278
|
+
if (storage.capacity)
|
|
279
|
+
allocation(storage, this.width(row.element, storage.capacity), () => {
|
|
280
|
+
for (let index = 0; index < vectorLength(value); index++)
|
|
281
|
+
visit(vectorElement(value, index), row.element);
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
case "aggregate":
|
|
286
|
+
if (value.kind !== "record")
|
|
287
|
+
throw new Error("managed aggregate has no fields");
|
|
288
|
+
row.fields.forEach((child, index) => visit(value.fields[index], child));
|
|
289
|
+
return;
|
|
290
|
+
case "variant":
|
|
291
|
+
if (value.kind !== "enum")
|
|
292
|
+
throw new Error("managed variant has no payload");
|
|
293
|
+
const payload = () => row.cases[value.tag].forEach((child, index) => visit(value.payload[index], child));
|
|
294
|
+
if (row.indirect)
|
|
295
|
+
allocation(value, BigInt(row.payload_bytes), payload);
|
|
296
|
+
else
|
|
297
|
+
payload();
|
|
298
|
+
return;
|
|
299
|
+
case "map":
|
|
300
|
+
case "set": {
|
|
301
|
+
if (value.kind !== "map")
|
|
302
|
+
throw new Error("managed map has no backing");
|
|
303
|
+
const capacity = value.capacity ?? 0;
|
|
304
|
+
const key = row.kind === "map" ? row.key : row.element;
|
|
305
|
+
const slot = this.width(key) + (row.kind === "map" ? this.width(row.value) : 0n);
|
|
306
|
+
if (capacity)
|
|
307
|
+
allocation(value.entries, checked(slot * BigInt(capacity)), () => {
|
|
308
|
+
for (const entry of value.entries.values()) {
|
|
309
|
+
visit(entry.key, key);
|
|
310
|
+
if (row.kind === "map")
|
|
311
|
+
visit(entry.value, row.value);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
case "callable": {
|
|
317
|
+
if (value.kind === "function")
|
|
318
|
+
return;
|
|
319
|
+
const environment = value.kind === "record"
|
|
320
|
+
? value
|
|
321
|
+
: value.kind === "closure" && value.environment.kind === "record"
|
|
322
|
+
? value.environment
|
|
323
|
+
: null;
|
|
324
|
+
if (!environment ||
|
|
325
|
+
(value.kind !== "closure" && value.kind !== "record"))
|
|
326
|
+
throw new Error("managed callable has no capture environment");
|
|
327
|
+
if (this.retired.has(environment))
|
|
328
|
+
return;
|
|
329
|
+
const fields = environment.fields;
|
|
330
|
+
const captures = row.captures ?? value.managedCaptures;
|
|
331
|
+
if (!captures)
|
|
332
|
+
throw new Error("callable has no concrete capture descriptor");
|
|
333
|
+
const width = captures.reduce((total, type) => checked(total + this.width(type)), 0n);
|
|
334
|
+
allocation(environment, width, () => captures.forEach((type, index) => visit(fields[index], type)));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
case "shared": {
|
|
338
|
+
if (value.kind !== "rc" && value.kind !== "weak")
|
|
339
|
+
throw new Error("managed shared value has no cell");
|
|
340
|
+
if (value.closed || !value.cell || row.weak)
|
|
341
|
+
return;
|
|
342
|
+
retained?.(value, type);
|
|
343
|
+
allocation(value.cell, this.width(row.payload), () => visit(value.cell.value, row.payload));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
case "dynamic": {
|
|
347
|
+
if (value.kind !== "dyn")
|
|
348
|
+
throw new Error("managed dynamic value has no box");
|
|
349
|
+
const payload = value.managedPayload ?? this.types.get(value.value);
|
|
350
|
+
if (payload == null)
|
|
351
|
+
throw new Error("managed dynamic payload has no compiler descriptor");
|
|
352
|
+
allocation(value, this.width(payload), () => visit(value.value, payload));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
case "generator":
|
|
356
|
+
case "actor_call":
|
|
357
|
+
case "provider": {
|
|
358
|
+
retained?.(value, type);
|
|
359
|
+
if (!providerGraph)
|
|
360
|
+
throw new Error("managed provider has no retained-value visitor");
|
|
361
|
+
const graph = providerGraph(value, type);
|
|
362
|
+
allocation(graph.identity, graph.bytes, () => {
|
|
363
|
+
for (const child of graph.children) {
|
|
364
|
+
if (child.slots)
|
|
365
|
+
allocation(child.identity ?? child, this.width(child.type), () => visit(child.value, child.type));
|
|
366
|
+
else
|
|
367
|
+
visit(child.value, child.type);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
if (root.slots)
|
|
375
|
+
allocation(root.identity ?? root, this.width(root.type), () => visit(root.value, root.type));
|
|
376
|
+
else
|
|
377
|
+
visit(root.value, root.type);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** One incarnation owns backing edges. Retaining an existing backing does not
|
|
381
|
+
* revisit its payload; its children belong to that backing until its final
|
|
382
|
+
* local retention ends. Mutable/provider publication invalidates those edges
|
|
383
|
+
* and reconciles them through the same typed visitor before acquisition. */
|
|
384
|
+
export class ManagedBudget {
|
|
385
|
+
layouts;
|
|
386
|
+
limit;
|
|
387
|
+
acquired;
|
|
388
|
+
roots = new Set();
|
|
389
|
+
nodes = new Map();
|
|
390
|
+
edges = new Map();
|
|
391
|
+
retainedBytes = 0n;
|
|
392
|
+
dirty = false;
|
|
393
|
+
constructor(layouts, limit, acquired) {
|
|
394
|
+
this.layouts = layouts;
|
|
395
|
+
this.limit = limit;
|
|
396
|
+
this.acquired = acquired;
|
|
397
|
+
}
|
|
398
|
+
allocations(extra = [], substitution, stringGrowth) {
|
|
399
|
+
const allocations = new Map();
|
|
400
|
+
for (const root of this.roots)
|
|
401
|
+
this.layouts.collect(root, allocations, substitution, stringGrowth);
|
|
402
|
+
for (const root of extra)
|
|
403
|
+
this.layouts.collect(root, allocations);
|
|
404
|
+
return allocations;
|
|
405
|
+
}
|
|
406
|
+
get liveBytes() {
|
|
407
|
+
this.refresh();
|
|
408
|
+
return this.retainedBytes;
|
|
409
|
+
}
|
|
410
|
+
changed() {
|
|
411
|
+
this.dirty = true;
|
|
412
|
+
}
|
|
413
|
+
bytes(nodes) {
|
|
414
|
+
let bytes = 0n;
|
|
415
|
+
for (const node of nodes.values())
|
|
416
|
+
bytes = checked(bytes + node.bytes);
|
|
417
|
+
return bytes;
|
|
418
|
+
}
|
|
419
|
+
graph(roots, known = new Map()) {
|
|
420
|
+
const nodes = new Map();
|
|
421
|
+
const edges = new Map();
|
|
422
|
+
let fresh = false;
|
|
423
|
+
for (const root of roots) {
|
|
424
|
+
let parent = new Set();
|
|
425
|
+
edges.set(root, parent);
|
|
426
|
+
const storage = (identity, bytes, children) => {
|
|
427
|
+
parent.add(identity);
|
|
428
|
+
const prior = nodes.get(identity) ?? known.get(identity);
|
|
429
|
+
if (prior?.bytes === bytes)
|
|
430
|
+
return;
|
|
431
|
+
fresh = true;
|
|
432
|
+
if (prior && !known.has(identity))
|
|
433
|
+
throw new Error("managed storage has conflicting widths");
|
|
434
|
+
const node = { bytes, children: new Set() };
|
|
435
|
+
nodes.set(identity, node);
|
|
436
|
+
const outer = parent;
|
|
437
|
+
parent = node.children;
|
|
438
|
+
children();
|
|
439
|
+
parent = outer;
|
|
440
|
+
};
|
|
441
|
+
this.layouts.collect(root, new Map(), undefined, undefined, undefined, undefined, storage);
|
|
442
|
+
}
|
|
443
|
+
return { nodes: fresh ? nodes : known, edges, fresh };
|
|
444
|
+
}
|
|
445
|
+
refresh() {
|
|
446
|
+
if (!this.dirty)
|
|
447
|
+
return;
|
|
448
|
+
const graph = this.graph([...this.roots]);
|
|
449
|
+
this.nodes = graph.nodes;
|
|
450
|
+
this.edges = graph.edges;
|
|
451
|
+
this.retainedBytes = this.bytes(graph.nodes);
|
|
452
|
+
this.dirty = false;
|
|
453
|
+
}
|
|
454
|
+
prune() {
|
|
455
|
+
const live = new Set();
|
|
456
|
+
const pending = [...this.edges.values()].flatMap((edges) => [...edges]);
|
|
457
|
+
while (pending.length) {
|
|
458
|
+
const identity = pending.pop();
|
|
459
|
+
if (live.has(identity))
|
|
460
|
+
continue;
|
|
461
|
+
live.add(identity);
|
|
462
|
+
pending.push(...(this.nodes.get(identity)?.children ?? []));
|
|
463
|
+
}
|
|
464
|
+
for (const [identity, node] of this.nodes)
|
|
465
|
+
if (!live.has(identity)) {
|
|
466
|
+
this.nodes.delete(identity);
|
|
467
|
+
this.retainedBytes -= node.bytes;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
check(extra) {
|
|
471
|
+
this.refresh();
|
|
472
|
+
let graph = this.graph(extra, this.nodes);
|
|
473
|
+
if (graph.fresh)
|
|
474
|
+
graph = this.graph([...this.roots, ...extra]);
|
|
475
|
+
if (graph.fresh && this.bytes(graph.nodes) > this.limit)
|
|
476
|
+
throw new HeapExceeded();
|
|
477
|
+
}
|
|
478
|
+
checkPayload(bytes, children) {
|
|
479
|
+
const allocations = this.allocations(children);
|
|
480
|
+
allocations.set({}, checked(bytes));
|
|
481
|
+
if (total(allocations) > this.limit)
|
|
482
|
+
throw new HeapExceeded();
|
|
483
|
+
this.changed();
|
|
484
|
+
}
|
|
485
|
+
checkMutation(before, after, detached = false) {
|
|
486
|
+
const allocations = this.allocations([], { before, after });
|
|
487
|
+
if (detached)
|
|
488
|
+
this.layouts.collect({ value: before, type: this.layouts.type(before) }, allocations);
|
|
489
|
+
if (total(allocations) > this.limit)
|
|
490
|
+
throw new HeapExceeded();
|
|
491
|
+
this.changed();
|
|
492
|
+
}
|
|
493
|
+
checkStringGrowth(value, capacity, detached = false) {
|
|
494
|
+
const allocations = this.allocations([], undefined, {
|
|
495
|
+
value,
|
|
496
|
+
identity: {},
|
|
497
|
+
capacity,
|
|
498
|
+
});
|
|
499
|
+
if (detached)
|
|
500
|
+
this.layouts.collect({ value, type: this.layouts.type(value) }, allocations);
|
|
501
|
+
if (total(allocations) > this.limit)
|
|
502
|
+
throw new HeapExceeded();
|
|
503
|
+
this.changed();
|
|
504
|
+
}
|
|
505
|
+
retain(root) {
|
|
506
|
+
return this.retainMany([root]);
|
|
507
|
+
}
|
|
508
|
+
retainMany(roots) {
|
|
509
|
+
roots = roots.map((root) => ({
|
|
510
|
+
...root,
|
|
511
|
+
...(root.slots ? { identity: root.identity ?? root } : {}),
|
|
512
|
+
}));
|
|
513
|
+
this.refresh();
|
|
514
|
+
let graph = this.graph(roots, this.nodes);
|
|
515
|
+
const acquired = graph.fresh;
|
|
516
|
+
// New storage can have been moved into an existing mutable owner since
|
|
517
|
+
// its previous snapshot. Reconcile that ownership before committing any
|
|
518
|
+
// new bytes; ordinary alias retains keep the existing child edges.
|
|
519
|
+
if (graph.fresh)
|
|
520
|
+
graph = this.graph([...this.roots, ...roots]);
|
|
521
|
+
if (graph.fresh && this.bytes(graph.nodes) > this.limit)
|
|
522
|
+
throw new HeapExceeded();
|
|
523
|
+
this.nodes = graph.nodes;
|
|
524
|
+
if (graph.fresh)
|
|
525
|
+
this.retainedBytes = this.bytes(graph.nodes);
|
|
526
|
+
for (const [root, edges] of graph.edges)
|
|
527
|
+
this.edges.set(root, edges);
|
|
528
|
+
roots.forEach((root) => this.roots.add(root));
|
|
529
|
+
if (acquired)
|
|
530
|
+
roots.forEach((root) => this.acquired?.(root));
|
|
531
|
+
let released = false;
|
|
532
|
+
return {
|
|
533
|
+
roots,
|
|
534
|
+
release: () => {
|
|
535
|
+
if (released)
|
|
536
|
+
return;
|
|
537
|
+
released = true;
|
|
538
|
+
this.refresh();
|
|
539
|
+
let retained = false;
|
|
540
|
+
roots.forEach((root) => {
|
|
541
|
+
this.roots.delete(root);
|
|
542
|
+
retained ||= !!this.edges.get(root)?.size;
|
|
543
|
+
this.edges.delete(root);
|
|
544
|
+
});
|
|
545
|
+
if (retained)
|
|
546
|
+
this.prune();
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function checked(bytes) {
|
|
552
|
+
if (bytes < 0n || bytes > U64_MAX)
|
|
553
|
+
throw new HeapExceeded();
|
|
554
|
+
return bytes;
|
|
555
|
+
}
|
|
556
|
+
function total(allocations) {
|
|
557
|
+
let bytes = 0n;
|
|
558
|
+
for (const width of allocations.values())
|
|
559
|
+
bytes = checked(bytes + width);
|
|
560
|
+
return bytes;
|
|
561
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validCodecShape } from "./package-validator.generated.js";
|
|
2
|
+
import { staticOperations } from "./package.js";
|
|
2
3
|
function require(condition, message) {
|
|
3
4
|
if (!condition)
|
|
4
5
|
throw new Error(message);
|
|
@@ -23,6 +24,67 @@ export function validatePackageGraph(pkg) {
|
|
|
23
24
|
const supervisors = pkg.supervisors ?? [];
|
|
24
25
|
const resources = pkg.resources ?? [];
|
|
25
26
|
const recipes = pkg.structural_render ?? [];
|
|
27
|
+
const managed = pkg.managed_types ?? [];
|
|
28
|
+
indexed(managed, "managed type");
|
|
29
|
+
const managedType = (id) => id == null ? null : at(managed, id, "managed type");
|
|
30
|
+
const width = (value) => {
|
|
31
|
+
require(/^(0|[1-9][0-9]*)$/.test(value), "managed width is not decimal u64");
|
|
32
|
+
require(BigInt(value) <= 0xffffffffffffffffn, "managed width exceeds u64");
|
|
33
|
+
};
|
|
34
|
+
for (const row of managed) {
|
|
35
|
+
width(row.slot_bytes);
|
|
36
|
+
switch (row.kind) {
|
|
37
|
+
case "scalar":
|
|
38
|
+
case "handle":
|
|
39
|
+
case "string":
|
|
40
|
+
case "bytes":
|
|
41
|
+
case "dynamic":
|
|
42
|
+
break;
|
|
43
|
+
case "array":
|
|
44
|
+
width(row.length);
|
|
45
|
+
managedType(row.element);
|
|
46
|
+
break;
|
|
47
|
+
case "vector":
|
|
48
|
+
case "slice":
|
|
49
|
+
case "set":
|
|
50
|
+
managedType(row.element);
|
|
51
|
+
break;
|
|
52
|
+
case "map":
|
|
53
|
+
managedType(row.key);
|
|
54
|
+
managedType(row.value);
|
|
55
|
+
break;
|
|
56
|
+
case "aggregate":
|
|
57
|
+
row.fields.forEach(managedType);
|
|
58
|
+
break;
|
|
59
|
+
case "variant":
|
|
60
|
+
width(row.payload_bytes);
|
|
61
|
+
at(pkg.variants, row.shape, "managed variant shape");
|
|
62
|
+
row.cases.forEach((fields) => fields.forEach(managedType));
|
|
63
|
+
break;
|
|
64
|
+
case "callable":
|
|
65
|
+
row.captures?.forEach(managedType);
|
|
66
|
+
break;
|
|
67
|
+
case "provider":
|
|
68
|
+
if (row.invocation_slot_bytes != null)
|
|
69
|
+
width(row.invocation_slot_bytes);
|
|
70
|
+
row.children.forEach(managedType);
|
|
71
|
+
break;
|
|
72
|
+
case "actor_call":
|
|
73
|
+
managedType(row.completion);
|
|
74
|
+
managedType(row.reply);
|
|
75
|
+
break;
|
|
76
|
+
case "generator":
|
|
77
|
+
width(row.payload_bytes);
|
|
78
|
+
managedType(row.yielded);
|
|
79
|
+
managedType(row.returned);
|
|
80
|
+
break;
|
|
81
|
+
case "shared":
|
|
82
|
+
managedType(row.payload);
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
exhaustive(row);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
26
88
|
indexed(pkg.functions, "function");
|
|
27
89
|
indexed(pkg.aggregates, "aggregate");
|
|
28
90
|
indexed(pkg.variants, "variant");
|
|
@@ -149,6 +211,9 @@ export function validatePackageGraph(pkg) {
|
|
|
149
211
|
}
|
|
150
212
|
}
|
|
151
213
|
for (const actor of actors) {
|
|
214
|
+
if (actor.max_heap_bytes != null)
|
|
215
|
+
width(actor.max_heap_bytes);
|
|
216
|
+
managedType(actor.state_type);
|
|
152
217
|
for (const id of [
|
|
153
218
|
actor.init,
|
|
154
219
|
actor.start,
|
|
@@ -314,13 +379,17 @@ export function validatePackageGraph(pkg) {
|
|
|
314
379
|
.length, `edge to block ${edge.to} has ${edge.args.length} arguments, expected ${target.params.length}`);
|
|
315
380
|
edge.args.forEach(value);
|
|
316
381
|
};
|
|
317
|
-
const result = (def) =>
|
|
382
|
+
const result = (def) => {
|
|
383
|
+
managedType(def.managed_type);
|
|
384
|
+
define(def.value);
|
|
385
|
+
};
|
|
318
386
|
fn.params.forEach(result);
|
|
319
387
|
// Results exist on authored successor edges. This pass checks identity,
|
|
320
388
|
// not dominance or whether a particular path owns the referenced value.
|
|
321
389
|
for (const block of fn.blocks) {
|
|
322
390
|
block.params.forEach(result);
|
|
323
|
-
for (const op of block
|
|
391
|
+
for (const op of staticOperations(block)) {
|
|
392
|
+
op.definitions?.forEach((def) => managedType(def.managed_type));
|
|
324
393
|
if ("dst" in op && op.dst != null)
|
|
325
394
|
define(op.dst);
|
|
326
395
|
if (op.op === "destructure" || op.op === "variant.destructure")
|
|
@@ -343,6 +412,7 @@ export function validatePackageGraph(pkg) {
|
|
|
343
412
|
term.arms.forEach((arm) => arm.fields.forEach(result));
|
|
344
413
|
}
|
|
345
414
|
for (const declaration of fn.places) {
|
|
415
|
+
managedType(declaration.managed_type);
|
|
346
416
|
switch (declaration.origin) {
|
|
347
417
|
case "local":
|
|
348
418
|
case "runtime":
|
|
@@ -537,6 +607,7 @@ export function validatePackageGraph(pkg) {
|
|
|
537
607
|
break;
|
|
538
608
|
case "task.race":
|
|
539
609
|
scope(op.scope);
|
|
610
|
+
width(op.member_slot_bytes);
|
|
540
611
|
op.members.forEach(value);
|
|
541
612
|
break;
|
|
542
613
|
case "task.spawn":
|
|
@@ -570,6 +641,10 @@ export function validatePackageGraph(pkg) {
|
|
|
570
641
|
if ("request_shapes" in term)
|
|
571
642
|
requestShapes(term.request_shapes);
|
|
572
643
|
switch (term.op) {
|
|
644
|
+
case "materialize":
|
|
645
|
+
operation(term.operation);
|
|
646
|
+
edge(term.failure);
|
|
647
|
+
break;
|
|
573
648
|
case "return":
|
|
574
649
|
if (term.value)
|
|
575
650
|
boundary(term.value);
|