@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.
@@ -1,4 +1,5 @@
1
1
  import { BrowserDirectories, } from "./browser-directory.js";
2
+ import { encodingStorage, releaseEncoding } from "./encoding-storage.js";
2
3
  /// The `hew.sandbox.bytecode.v1` executor.
3
4
  ///
4
5
  /// The package is a projection of verified ownership SIR, so this executor
@@ -10,8 +11,8 @@ import { executeCodec } from "./codecs.js";
10
11
  import { actorIdentical } from "./actor-identity.js";
11
12
  import { ActorWaits } from "./actor-waits.js";
12
13
  import { TraceBuilder, runtimeFailure } from "../trace.js";
13
- import { UNIT, cloneValue, stringValue, toJsonValue, } from "../values.js";
14
- import { runtimeTag } from "./package.js";
14
+ import { UNIT, cloneValue, releaseVector, byteVectorValue, vectorStorage, vectorItems, vectorLength, vectorElement, stringValue, toJsonValue, } from "../values.js";
15
+ import { runtimeTag, staticOperations } from "./package.js";
15
16
  import { StructuralRenderer } from "./structural.js";
16
17
  import { timestampBinary } from "./timestamp.js";
17
18
  import { selectedValue, collectionOperation, sharedOperation, } from "./value-operations.js";
@@ -22,8 +23,14 @@ import { DeterministicIds } from "../../scheduler/ids.js";
22
23
  import { Mt19937 } from "./mt19937.js";
23
24
  import { ShimFault, CODEC_REGISTRATION, decodeUtf8Checked, resolveExtern, resolveRuntime, resolveSuspension, } from "./shims.js";
24
25
  import { AdmittedPackage } from "./validate.js";
26
+ import { HeapExceeded, ManagedBudget, ManagedLayouts, } from "./managed-budget.js";
25
27
  const DEFAULT_STEP_BUDGET = 1_000_000;
26
28
  const DEFAULT_ACTOR_STOP_NS = 5_000_000_000;
29
+ /// Deepest activation a task may reach below its root before a call fails
30
+ /// with `StackOverflow`. HEW-SPEC-2026 §2.2 guarantees 10,000 frames. Twice
31
+ /// that matches what a native stack admits for small frames and leaves an
32
+ /// unbounded recursion failing well inside the default step budget.
33
+ const MAX_FRAME_DEPTH = 20_000;
27
34
  /// Ends the run. The status is already recorded on the trace.
28
35
  class Halt extends Error {
29
36
  status;
@@ -119,6 +126,7 @@ class ExecutorV1 {
119
126
  tasks = new Map();
120
127
  completedTasks = 0;
121
128
  actorIdentity = 0n;
129
+ arenaIdentity = 0n;
122
130
  faultDebts = new Set();
123
131
  nextFaultDebt = 0;
124
132
  closingResources = new WeakSet();
@@ -128,6 +136,7 @@ class ExecutorV1 {
128
136
  terminating = false;
129
137
  rootFault = null;
130
138
  pkg;
139
+ managed;
131
140
  directories;
132
141
  semaphores;
133
142
  constructor(admission, trace, stdin, policy, options) {
@@ -136,12 +145,14 @@ class ExecutorV1 {
136
145
  this.options = options;
137
146
  const pkg = admission.bytecode;
138
147
  this.pkg = pkg;
148
+ this.managed = new ManagedLayouts(pkg.managed_types ?? [], (value, type) => this.managedRetained(value, type));
139
149
  this.directories = new BrowserDirectories(options.directoryRoots);
140
150
  this.scheduler = new FrameScheduler(trace, policy);
141
151
  this.semaphores = new Semaphores((nanos, ready) => this.scheduler.after(nanos, ready));
142
152
  this.ids = new DeterministicIds(trace.replay.seed);
143
153
  this.host = {
144
154
  argv: [...(options.argv ?? ["main.hew"])],
155
+ arenaInstanceId: () => this.arenaIdentity === 0x7fffffffffffffffn ? 0n : ++this.arenaIdentity,
145
156
  unixTimeMillis: () => this.trace.virtualTimeMs,
146
157
  monotonicTimeNanos: () => {
147
158
  const millis = this.trace.virtualTimeMs;
@@ -189,6 +200,7 @@ class ExecutorV1 {
189
200
  pending: null,
190
201
  fault: null,
191
202
  receiving: false,
203
+ observers: new Set(),
192
204
  });
193
205
  return { kind: "stream", channelId: id };
194
206
  },
@@ -226,12 +238,13 @@ class ExecutorV1 {
226
238
  this.current = this.activate(this.functionAt(pkg.entry.function), [], null, null, undefined, null, {
227
239
  id: "actor:root",
228
240
  actor: null,
229
- returned: (value) => {
241
+ returned: (value, released) => {
230
242
  this.completeRoot();
231
243
  this.trace.exitCode =
232
244
  pkg.entry?.exit === "status" && value.kind === "i64"
233
245
  ? Number(value.value)
234
246
  : 0;
247
+ released();
235
248
  },
236
249
  failed: (fault) => {
237
250
  this.rootFault = fault;
@@ -288,6 +301,7 @@ class ExecutorV1 {
288
301
  this.running = true;
289
302
  while (this.running) {
290
303
  const act = this.current;
304
+ act.context.activeFrame = act;
291
305
  act.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
292
306
  if (this.scopeDeadline(act, fault) !== fault)
293
307
  act.context.pendingCancellation ??= fault;
@@ -337,29 +351,42 @@ class ExecutorV1 {
337
351
  : parentContext;
338
352
  const env = new Map();
339
353
  for (const [at, param] of fn.params.entries()) {
340
- env.set(param.value, ownedRef(args[at] ?? UNIT));
354
+ env.set(param.value, ownedRef(args[at] ?? UNIT, param.own === "owned" ? executionContext.actor?.budget : undefined, param.managed_type ?? undefined));
341
355
  }
342
356
  const places = new Map();
343
357
  const trivial = new Set();
358
+ const owned = new Set();
359
+ const managedTypes = new Map();
344
360
  const record = (value) => {
345
361
  if (value.own === "none")
346
362
  trivial.add(value.value);
363
+ if (value.own === "owned")
364
+ owned.add(value.value);
365
+ if (value.managed_type != null)
366
+ managedTypes.set(value.value, value.managed_type);
347
367
  };
348
368
  fn.params.forEach(record);
349
369
  for (const block of fn.blocks) {
350
370
  block.params.forEach(record);
351
- for (const op of block.ops) {
371
+ for (const op of staticOperations(block)) {
372
+ op.definitions?.forEach(record);
352
373
  if ("dst" in op && op.dst != null && op.own === "none")
353
374
  trivial.add(op.dst);
354
375
  }
355
376
  const term = block.term;
356
377
  if ("result" in term && term.result && term.result !== "never")
357
378
  record(term.result);
379
+ if (term.op === "call" && term.handback)
380
+ record(term.handback);
358
381
  if (term.op === "switch.variant")
359
382
  for (const arm of term.arms)
360
383
  arm.fields.forEach(record);
361
384
  }
385
+ for (const [index, param] of fn.params.entries())
386
+ if (param.managed_type != null && args[index])
387
+ this.stampManaged(args[index], param.managed_type, executionContext.actor);
362
388
  return {
389
+ accountActor: executionContext.actor,
363
390
  rcOwner: fn.actor_construction
364
391
  ? { blocks: new Set(), closed: false }
365
392
  : (caller?.rcOwner ??
@@ -372,11 +399,17 @@ class ExecutorV1 {
372
399
  opIndex: 0,
373
400
  env,
374
401
  trivial,
402
+ owned,
403
+ managedTypes,
404
+ managedInputs: [],
405
+ managedCleanup: new Set(),
406
+ managedChildren: new Set(),
375
407
  scopes: new Map(),
376
408
  places,
377
409
  fault: null,
378
410
  parked: [],
379
411
  caller,
412
+ depth: caller ? caller.depth + 1 : 0,
380
413
  result,
381
414
  normal: normal ?? undefined,
382
415
  unwind,
@@ -414,7 +447,11 @@ class ExecutorV1 {
414
447
  }
415
448
  read(act, id) {
416
449
  try {
417
- return readRef(this.refOf(act, id));
450
+ const value = readRef(this.refOf(act, id));
451
+ const type = act.managedTypes.get(id);
452
+ if (type != null)
453
+ this.stampManaged(value, type, act.context.actor);
454
+ return value;
418
455
  }
419
456
  catch (error) {
420
457
  throw new Error(`${act.fn.name} block ${act.block.id} value ${id}: ${error.message}`);
@@ -424,7 +461,416 @@ class ExecutorV1 {
424
461
  if (id === null || id === undefined) {
425
462
  return;
426
463
  }
427
- act.env.set(id, ownedRef(value));
464
+ const type = act.managedTypes.get(id);
465
+ if (type != null)
466
+ this.stampManaged(value, type, act.context.actor);
467
+ const budget = act.owned.has(id) ? act.context.actor?.budget : undefined;
468
+ act.env.set(id, ownedRef(value, budget, type));
469
+ }
470
+ stampManaged(value, type, actor) {
471
+ const budget = actor?.budget;
472
+ this.managed.stamp(value, type);
473
+ if (value.kind === "rc" &&
474
+ !value.closed &&
475
+ value.cell?.owner &&
476
+ budget &&
477
+ !value.cell.managedLease) {
478
+ const row = this.managed.row(type);
479
+ if (row.kind !== "shared")
480
+ throw new Error("Rc has no canonical payload descriptor");
481
+ value.cell.managedPayload = row.payload;
482
+ value.cell.managedLease = budget.retain({
483
+ value: { kind: "rc", cell: value.cell, closed: false },
484
+ type,
485
+ });
486
+ }
487
+ if (value.kind === "task") {
488
+ const task = this.taskFor(value);
489
+ task.managedType = type;
490
+ task.handles.add(value);
491
+ }
492
+ if (value.kind === "stream" && !this.generatorStreams.has(value.channelId))
493
+ this.bindManagedStream(value, type, actor);
494
+ if (value.kind === "generator")
495
+ this.generatorFor(value).managedType = type;
496
+ if (value.kind === "encoding") {
497
+ value.prepareMutation = budget
498
+ ? (replacement, detached) => budget.checkMutation(value, replacement, detached)
499
+ : undefined;
500
+ }
501
+ else if (value.kind === "string") {
502
+ value.prepareGrowth = budget
503
+ ? (capacity, detached) => budget.checkStringGrowth(value, capacity, detached)
504
+ : undefined;
505
+ }
506
+ else if (value.kind === "map") {
507
+ value.prepareInsert = budget
508
+ ? (staged, slot, key, item, copyKey) => {
509
+ const row = this.managed.row(type);
510
+ if (row.kind !== "map" && row.kind !== "set")
511
+ throw new Error("map has no canonical entry descriptor");
512
+ const entries = new Map(staged.entries);
513
+ entries.set(slot, {
514
+ key: copyKey
515
+ ? this.managed.copy(key, row.kind === "map" ? row.key : row.element)
516
+ : key,
517
+ value: item,
518
+ });
519
+ budget.checkMutation(value, { ...staged, entries });
520
+ }
521
+ : undefined;
522
+ }
523
+ else if (value.kind === "vector") {
524
+ value.prepareMutation = budget
525
+ ? (capacity, detached, appended) => {
526
+ const row = this.managed.row(type);
527
+ const storage = vectorStorage(value);
528
+ let items = detached &&
529
+ (row.kind === "vector" ||
530
+ row.kind === "array" ||
531
+ row.kind === "slice")
532
+ ? vectorItems(value).map((item) => this.managed.copy(item, row.element))
533
+ : vectorItems(value);
534
+ if (appended?.length) {
535
+ if (row.kind !== "vector" &&
536
+ row.kind !== "array" &&
537
+ row.kind !== "slice")
538
+ throw new Error("vector append lacks its canonical element type");
539
+ items = [
540
+ ...items,
541
+ ...appended.map((item) => this.managed.copy(item, row.element)),
542
+ ];
543
+ }
544
+ budget.checkMutation(value, {
545
+ ...value,
546
+ items,
547
+ offset: 0,
548
+ length: undefined,
549
+ storage: { ...storage, capacity, items, owners: 1 },
550
+ }, detached);
551
+ }
552
+ : undefined;
553
+ }
554
+ }
555
+ frameManagedRoots(frame) {
556
+ const roots = [];
557
+ for (const [id, ref] of frame.env) {
558
+ const type = frame.managedTypes.get(id);
559
+ if (type != null && frame.owned.has(id) && liveRef(ref))
560
+ roots.push({ value: readRef(ref), type });
561
+ }
562
+ for (const [id, cell] of frame.places) {
563
+ const type = frame.fn.places[id]?.managed_type;
564
+ if (type != null && cell.valid)
565
+ roots.push({ value: cell.value, type });
566
+ }
567
+ for (const scope of frame.scopes.values())
568
+ for (const task of scope.tasks)
569
+ if (task.managedType != null)
570
+ roots.push({
571
+ value: { kind: "task", id: task.id },
572
+ type: task.managedType,
573
+ });
574
+ for (const lease of frame.managedInputs)
575
+ roots.push(...(lease.roots ?? []));
576
+ for (const cleanup of frame.managedCleanup)
577
+ roots.push(...cleanup.roots);
578
+ for (const child of frame.managedChildren)
579
+ roots.push(...this.frameManagedRoots(child.context.activeFrame ?? child));
580
+ if (frame.caller)
581
+ roots.push(...this.frameManagedRoots(frame.caller));
582
+ return roots;
583
+ }
584
+ bindManagedStream(value, type, actor) {
585
+ const row = this.managed.row(type);
586
+ if (row.kind !== "provider" ||
587
+ row.provider !== "stream" ||
588
+ value.kind !== "stream" ||
589
+ this.generatorStreams.has(value.channelId))
590
+ return;
591
+ const budget = actor?.budget;
592
+ this.pipes.bind(value, budget && actor
593
+ ? {
594
+ owner: actor,
595
+ acquire: (envelope) => {
596
+ const lease = budget.retain({
597
+ value: envelope.value,
598
+ type: row.children[0],
599
+ slots: true,
600
+ identity: envelope,
601
+ });
602
+ return () => lease.release();
603
+ },
604
+ refused: () => this.failActorBudget(actor),
605
+ }
606
+ : undefined);
607
+ }
608
+ adoptManaged(root, actor) {
609
+ if (!this.managed.needsAdoption(root.type))
610
+ return;
611
+ this.managed.collect(root, new Map(), undefined, undefined, (value, type) => {
612
+ this.bindManagedStream(value, type, actor);
613
+ const row = this.managed.row(type);
614
+ if (value.kind === "rc" && value.cell && !value.closed) {
615
+ if (row.kind !== "shared")
616
+ throw new Error("Rc has no canonical payload descriptor");
617
+ const cell = value.cell;
618
+ if (cell.owner !== actor.rcOwner && !cell.dying) {
619
+ cell.owner?.blocks.delete(cell);
620
+ cell.owner = actor.rcOwner;
621
+ cell.managedPayload = row.payload;
622
+ actor.rcOwner.blocks.add(cell);
623
+ cell.managedLease = this.rehomeLease(cell.managedLease, actor.budget);
624
+ if (!cell.managedLease && actor.budget)
625
+ cell.managedLease = actor.budget.retain({ value, type });
626
+ }
627
+ }
628
+ else if (row.kind === "generator")
629
+ this.rehomeFrame(this.generatorFor(value).frame, actor);
630
+ else if (value.kind === "task")
631
+ this.rehomeTask(this.taskFor(value), actor);
632
+ }, (value, type) => {
633
+ const row = this.managed.row(type);
634
+ // Encoding trees and opaque host providers cannot contain receiving
635
+ // stream handles. This traversal changes queue ownership only;
636
+ // their payload accounting keeps its separate canonical visitor.
637
+ return row.kind === "provider" &&
638
+ ["json", "yaml", "toml", "opaque"].includes(row.provider)
639
+ ? { identity: value, bytes: 0n, children: [] }
640
+ : this.managedRetained(value, type);
641
+ });
642
+ }
643
+ rehomeLease(lease, budget) {
644
+ if (!lease?.roots)
645
+ return lease;
646
+ const replacement = this.retainRoots(lease.roots, budget);
647
+ lease.release();
648
+ return replacement;
649
+ }
650
+ retainRoots(roots, budget) {
651
+ // Unbounded frames keep typed ownership metadata for a later generator
652
+ // transfer, without building an allocation graph for an account.
653
+ return budget?.retainMany(roots) ?? { roots, release() { } };
654
+ }
655
+ retainCleanup(frame, roots) {
656
+ const cleanup = {
657
+ roots,
658
+ lease: this.retainRoots(roots, frame.accountActor?.budget),
659
+ };
660
+ frame.managedCleanup.add(cleanup);
661
+ return {
662
+ roots,
663
+ release: () => {
664
+ if (!frame.managedCleanup.delete(cleanup))
665
+ return;
666
+ cleanup.lease.release();
667
+ },
668
+ };
669
+ }
670
+ rehomeTask(task, actor) {
671
+ const frame = task.frame?.context.activeFrame ?? task.frame;
672
+ if (task.budget === actor.budget && frame?.accountActor === actor)
673
+ return;
674
+ task.budget = actor.budget;
675
+ if (frame)
676
+ this.rehomeFrame(frame, actor);
677
+ task.managedLease = this.rehomeLease(task.managedLease, actor.budget);
678
+ }
679
+ rehomeFrame(frame, actor) {
680
+ if (frame.accountActor === actor)
681
+ return;
682
+ const previous = frame.accountActor;
683
+ frame.accountActor = actor;
684
+ frame.rcOwner = actor.rcOwner;
685
+ frame.context.actor = actor;
686
+ frame.context.id = actor.id;
687
+ const cells = new Set();
688
+ const rehome = (cell) => {
689
+ if (cells.has(cell))
690
+ return;
691
+ cells.add(cell);
692
+ cell.budget = actor.budget;
693
+ if (cell.valid && cell.type != null)
694
+ this.stampManaged(cell.value, cell.type, actor);
695
+ cell.lease = this.rehomeLease(cell.lease, actor.budget);
696
+ if (!cell.lease && cell.valid && cell.type != null)
697
+ cell.lease = actor.budget?.retain({
698
+ value: cell.value,
699
+ type: cell.type,
700
+ });
701
+ };
702
+ const ref = (value) => {
703
+ if (value.kind === "cell")
704
+ rehome(value.cell);
705
+ else
706
+ ref(value.parent);
707
+ };
708
+ for (const value of frame.env.values())
709
+ ref(value);
710
+ for (const cell of frame.places.values())
711
+ rehome(cell);
712
+ frame.managedInputs = frame.managedInputs.flatMap((lease) => {
713
+ const replacement = this.rehomeLease(lease, actor.budget);
714
+ return replacement ? [replacement] : [];
715
+ });
716
+ for (const cleanup of frame.managedCleanup)
717
+ cleanup.lease = this.rehomeLease(cleanup.lease, actor.budget);
718
+ for (const child of frame.managedChildren)
719
+ this.rehomeFrame(child.context.activeFrame ?? child, actor);
720
+ for (const scope of frame.scopes.values())
721
+ for (const task of scope.tasks)
722
+ this.rehomeTask(task, actor);
723
+ if (frame.caller)
724
+ this.rehomeFrame(frame.caller, actor);
725
+ previous?.budget?.changed();
726
+ actor.budget?.changed();
727
+ }
728
+ failActorBudget(actor) {
729
+ const fault = { kind: "trap", trap: "heap_exceeded" };
730
+ if (!actor.active)
731
+ this.crashActor(actor, fault);
732
+ else {
733
+ // A suspended turn owns its frame until authored unwind and lexical
734
+ // task drains finish. Admission must not retire its state underneath it.
735
+ actor.closing = true;
736
+ actor.crashing ??= fault;
737
+ actor.active.pendingCancellation ??= fault;
738
+ actor.active.cancel?.(fault);
739
+ }
740
+ }
741
+ managedRetained(value, type) {
742
+ const row = this.managed.row(type);
743
+ const typed = (value) => ({
744
+ value,
745
+ type: this.managed.type(value),
746
+ });
747
+ if (row.kind === "generator") {
748
+ const generator = this.generatorFor(value);
749
+ return {
750
+ identity: generator,
751
+ bytes: BigInt(row.payload_bytes),
752
+ children: [
753
+ typed(generator.callable),
754
+ ...this.frameManagedRoots(generator.frame),
755
+ ],
756
+ };
757
+ }
758
+ if (row.kind === "actor_call") {
759
+ const task = this.taskFor(value);
760
+ const reply = !task.taken && !task.fault && task.managedReply;
761
+ return {
762
+ identity: task,
763
+ bytes: reply ? this.managed.width(row.reply) : 0n,
764
+ children: reply ? [{ value: reply, type: row.reply }] : [],
765
+ };
766
+ }
767
+ if (row.kind !== "provider")
768
+ throw new Error("managed retained visitor received no provider");
769
+ switch (row.provider) {
770
+ case "json":
771
+ case "yaml":
772
+ case "toml": {
773
+ if (value.kind !== "encoding")
774
+ throw new Error("managed encoding has no tree");
775
+ const retained = encodingStorage(value);
776
+ return {
777
+ identity: retained,
778
+ bytes: value.value === null ? 0n : retained.bytes,
779
+ children: [],
780
+ };
781
+ }
782
+ case "task": {
783
+ const task = this.taskFor(value);
784
+ const reply = task.resultInitialized &&
785
+ !task.taken &&
786
+ !task.fault &&
787
+ (!task.actorError || task.actorError === "Failed");
788
+ const children = task.frame && !task.done
789
+ ? this.frameManagedRoots(task.frame.context.activeFrame ?? task.frame)
790
+ : [];
791
+ if (task.callable?.kind === "closure" && !task.done)
792
+ children.push(typed(task.callable));
793
+ if (reply)
794
+ children.push({
795
+ value: task.managedResult ?? task.value,
796
+ type: row.children[0],
797
+ });
798
+ return {
799
+ identity: task,
800
+ bytes: (task.invocationPending
801
+ ? BigInt(row.invocation_slot_bytes ??
802
+ (() => {
803
+ throw new Error("task invocation has no canonical carrier width");
804
+ })())
805
+ : 0n) +
806
+ this.managed.width(row.children[0]) +
807
+ (task.memberBytes ?? 0n),
808
+ children,
809
+ };
810
+ }
811
+ case "sink": {
812
+ if (value.kind !== "sink")
813
+ throw new Error("typed sink has no endpoint");
814
+ const sender = this.senderEndpoints.get(value.channelId);
815
+ if (sender) {
816
+ const children = [
817
+ typed(sender.owner.sink),
818
+ typed(sender.owner.encoder),
819
+ ];
820
+ return {
821
+ identity: sender.owner,
822
+ bytes: children.reduce((bytes, child) => bytes + this.managed.width(child.type), 0n),
823
+ children,
824
+ };
825
+ }
826
+ return {
827
+ identity: this.pipes.managedRetained(value).identity,
828
+ bytes: 0n,
829
+ children: [],
830
+ };
831
+ }
832
+ case "stream": {
833
+ if (value.kind !== "stream")
834
+ throw new Error("typed stream has no endpoint");
835
+ const adapter = this.generatorStreams.get(value.channelId);
836
+ if (adapter) {
837
+ const generatorType = adapter.generator.managedType;
838
+ if (generatorType == null)
839
+ throw new Error("generator stream lacks its producer descriptor");
840
+ const children = [
841
+ {
842
+ value: { kind: "generator", id: adapter.generator.id },
843
+ type: generatorType,
844
+ },
845
+ ];
846
+ if (adapter.pending)
847
+ children.push({
848
+ value: adapter.pending,
849
+ type: row.children[0],
850
+ slots: true,
851
+ identity: adapter,
852
+ });
853
+ return {
854
+ identity: adapter,
855
+ bytes: this.managed.width(generatorType),
856
+ children,
857
+ };
858
+ }
859
+ const graph = this.pipes.managedRetained(value);
860
+ return {
861
+ identity: graph.identity,
862
+ bytes: 0n,
863
+ children: graph.queued.map((item) => ({
864
+ value: item.value,
865
+ type: row.children[0],
866
+ slots: true,
867
+ identity: item,
868
+ })),
869
+ };
870
+ }
871
+ default:
872
+ throw new HeapExceeded();
873
+ }
428
874
  }
429
875
  invalidate(act, id) {
430
876
  // A boundary's transfer of a trivial value has no ownership to end. SIR
@@ -476,11 +922,26 @@ class ExecutorV1 {
476
922
  /// and a borrow passes the live value.
477
923
  boundary(act, operand) {
478
924
  const value = this.read(act, operand.value);
925
+ const budget = act.context.actor?.budget;
926
+ const type = act.managedTypes.get(operand.value);
927
+ const transferable = act.context.generator || act.context.retainsTransferableFrame;
928
+ if (!budget && transferable && type != null)
929
+ this.managed.stampGraph({ value, type });
479
930
  switch (operand.decision) {
480
931
  case "copy":
481
- case "snapshot":
482
- return cloneValue(value);
932
+ case "snapshot": {
933
+ if (budget && type != null)
934
+ budget.check([{ value: this.managed.copy(value, type), type }]);
935
+ const copy = cloneValue(value);
936
+ if (!budget && transferable && type != null)
937
+ this.managed.stampGraph({ value: copy, type });
938
+ if (type != null)
939
+ act.managedInputs.push(this.retainRoots([{ value: copy, type }], budget));
940
+ return copy;
941
+ }
483
942
  case "move":
943
+ if (type != null)
944
+ act.managedInputs.push(this.retainRoots([{ value, type }], budget));
484
945
  this.invalidate(act, operand.value);
485
946
  return value;
486
947
  default:
@@ -489,6 +950,24 @@ class ExecutorV1 {
489
950
  }
490
951
  // ── instructions ─────────────────────────────────────────────────────────
491
952
  execute(act, op) {
953
+ try {
954
+ this.executeOperation(act, op);
955
+ }
956
+ finally {
957
+ switch (op.op) {
958
+ case "store.init":
959
+ case "store.assign":
960
+ case "load.take":
961
+ case "destructure":
962
+ case "variant.destructure":
963
+ case "destroy_value":
964
+ case "end_lifetime":
965
+ case "task_scope.close":
966
+ act.context.actor?.budget?.changed();
967
+ }
968
+ }
969
+ }
970
+ executeOperation(act, op) {
492
971
  switch (op.op) {
493
972
  case "coverage.hit": {
494
973
  const key = String(op.counter);
@@ -532,8 +1011,10 @@ class ExecutorV1 {
532
1011
  return;
533
1012
  case "destroy_value": {
534
1013
  const value = this.read(act, op.value);
1014
+ const type = act.managedTypes.get(op.value);
1015
+ const cleanup = type == null ? undefined : this.retainCleanup(act, [{ value, type }]);
535
1016
  this.invalidate(act, op.value);
536
- this.releaseOperation(act, value);
1017
+ this.releaseOperation(act, value, () => cleanup?.release());
537
1018
  return;
538
1019
  }
539
1020
  case "begin_borrow":
@@ -543,20 +1024,34 @@ class ExecutorV1 {
543
1024
  case "finish_linear_receiver":
544
1025
  return;
545
1026
  case "alloc_place":
546
- act.places.set(op.place, { value: UNIT, valid: false });
1027
+ act.places.set(op.place, {
1028
+ value: UNIT,
1029
+ valid: false,
1030
+ budget: act.context.actor?.budget,
1031
+ type: act.fn.places[op.place]?.managed_type ?? undefined,
1032
+ });
547
1033
  return;
548
1034
  case "store.init":
549
1035
  writeRef(this.placeRef(act, op.place), this.read(act, op.value));
1036
+ this.invalidate(act, op.value);
550
1037
  return;
551
1038
  case "store.assign": {
552
1039
  const ref = this.placeRef(act, op.place);
553
1040
  const replacement = this.read(act, op.value);
554
1041
  const previous = liveRef(ref) ? readRef(ref) : UNIT;
1042
+ const type = act.fn.places[op.place]?.managed_type;
1043
+ const cleanup = type == null
1044
+ ? undefined
1045
+ : this.retainCleanup(act, [{ value: previous, type }]);
555
1046
  // Retire the old owner before invoking user close code. Publish the
556
1047
  // replacement after cleanup, including when close records a fault;
557
1048
  // the following cleanup.dispatch then drains the new owner once.
558
1049
  invalidateRef(ref);
559
- this.releaseOperation(act, previous, () => writeRef(ref, replacement));
1050
+ this.releaseOperation(act, previous, () => {
1051
+ writeRef(ref, replacement);
1052
+ this.invalidate(act, op.value);
1053
+ cleanup?.release();
1054
+ });
560
1055
  return;
561
1056
  }
562
1057
  case "load.copy":
@@ -574,8 +1069,10 @@ class ExecutorV1 {
574
1069
  case "end_lifetime": {
575
1070
  const ref = this.placeRef(act, op.place);
576
1071
  const value = liveRef(ref) ? readRef(ref) : UNIT;
1072
+ const type = act.fn.places[op.place]?.managed_type;
1073
+ const cleanup = type == null ? undefined : this.retainCleanup(act, [{ value, type }]);
577
1074
  invalidateRef(ref);
578
- this.releaseOperation(act, value);
1075
+ this.releaseOperation(act, value, () => cleanup?.release());
579
1076
  return;
580
1077
  }
581
1078
  case "tuple.make":
@@ -584,6 +1081,7 @@ class ExecutorV1 {
584
1081
  typeId: "",
585
1082
  fields: op.elements.map((id) => this.read(act, id)),
586
1083
  });
1084
+ op.elements.forEach((id) => this.invalidate(act, id));
587
1085
  return;
588
1086
  case "tuple.get":
589
1087
  this.define(act, op.dst, cloneValue(fieldsOf(this.read(act, op.tuple))[op.index] ?? UNIT));
@@ -595,6 +1093,7 @@ class ExecutorV1 {
595
1093
  shape: op.shape ?? undefined,
596
1094
  fields: op.fields.map((id) => this.read(act, id)),
597
1095
  });
1096
+ op.fields.forEach((id) => this.invalidate(act, id));
598
1097
  return;
599
1098
  case "aggregate.project_copy":
600
1099
  this.define(act, op.dst, cloneValue(fieldsOf(this.read(act, op.aggregate))[op.field] ?? UNIT));
@@ -616,14 +1115,19 @@ class ExecutorV1 {
616
1115
  elementType: "",
617
1116
  items: op.elements.map((id) => this.read(act, id)),
618
1117
  });
1118
+ op.elements.forEach((id) => this.invalidate(act, id));
619
1119
  return;
620
- case "array.repeat":
1120
+ case "array.repeat": {
1121
+ const seed = this.read(act, op.value);
1122
+ const count = this.arrayRepeatLength(op);
621
1123
  this.define(act, op.dst, {
622
1124
  kind: "vector",
623
1125
  elementType: "",
624
- items: Array.from({ length: op.count ?? 0 }, () => cloneValue(this.read(act, op.value))),
1126
+ items: Array.from({ length: count }, (_, index) => index === 0 ? seed : cloneValue(seed)),
625
1127
  });
1128
+ this.invalidate(act, op.value);
626
1129
  return;
1130
+ }
627
1131
  case "variant.make":
628
1132
  this.define(act, op.dst, {
629
1133
  kind: "enum",
@@ -632,6 +1136,7 @@ class ExecutorV1 {
632
1136
  tag: op.variant,
633
1137
  payload: op.fields.map((id) => this.read(act, id)),
634
1138
  });
1139
+ op.fields.forEach((id) => this.invalidate(act, id));
635
1140
  return;
636
1141
  case "variant.is":
637
1142
  this.define(act, op.dst, {
@@ -691,6 +1196,7 @@ class ExecutorV1 {
691
1196
  fields: op.fields.map((id) => this.read(act, id)),
692
1197
  },
693
1198
  });
1199
+ op.fields.forEach((id) => this.invalidate(act, id));
694
1200
  return;
695
1201
  }
696
1202
  case "callable.coerce": {
@@ -716,6 +1222,7 @@ class ExecutorV1 {
716
1222
  vtable: op.vtable,
717
1223
  value: this.read(act, op.value),
718
1224
  });
1225
+ this.invalidate(act, op.value);
719
1226
  return;
720
1227
  // The registration reserves the free places the body reaches; the drain
721
1228
  // order is already the CFG's, so there is nothing to record here.
@@ -771,9 +1278,12 @@ class ExecutorV1 {
771
1278
  id: task.id,
772
1279
  })),
773
1280
  }, () => {
774
- for (const task of group?.tasks ?? [])
775
- this.tasks.delete(task.id);
776
1281
  act.scopes.delete(op.scope);
1282
+ act.context.actor?.budget?.changed();
1283
+ for (const task of group?.tasks ?? [])
1284
+ task.scopeAlive = false;
1285
+ for (const task of group?.tasks ?? [])
1286
+ this.releaseTaskState(task);
777
1287
  });
778
1288
  return;
779
1289
  }
@@ -786,7 +1296,10 @@ class ExecutorV1 {
786
1296
  actor: act.context.actor,
787
1297
  rcOwner: act.rcOwner,
788
1298
  requestDeadline: this.requestDeadline(act),
789
- returned: (value) => this.closeValueAsync(value, null, (fault) => this.finishGenerator(generator, fault), act.context.actor, act.rcOwner),
1299
+ returned: (value, released) => this.closeValueAsync(value, null, (fault) => {
1300
+ released();
1301
+ this.finishGenerator(generator, fault);
1302
+ }, act.context.actor, act.rcOwner),
790
1303
  failed: (fault) => this.finishGenerator(generator, fault),
791
1304
  };
792
1305
  const generator = {
@@ -807,8 +1320,9 @@ class ExecutorV1 {
807
1320
  const scope = act.scopes.get(op.scope);
808
1321
  if (!scope)
809
1322
  throw new Error("race has no task scope");
810
- const members = op.members.map((operand) => {
811
- const member = this.taskFor(this.read(act, operand));
1323
+ const memberValues = op.members.map((operand) => this.read(act, operand));
1324
+ const members = op.members.map((operand, index) => {
1325
+ const member = this.taskFor(memberValues[index]);
812
1326
  this.invalidate(act, operand);
813
1327
  if (member.grouped)
814
1328
  throw new Error("race reuses a task handle");
@@ -817,7 +1331,8 @@ class ExecutorV1 {
817
1331
  });
818
1332
  if (!members.length)
819
1333
  throw new Error("race requires members");
820
- const task = this.newTask();
1334
+ const task = this.newTask(act.context.actor?.budget, act.managedTypes.get(op.dst), BigInt(op.member_slot_bytes) * BigInt(members.length), true);
1335
+ task.members = memberValues;
821
1336
  scope.tasks.push(task);
822
1337
  let selected = false;
823
1338
  let winner = null;
@@ -839,6 +1354,9 @@ class ExecutorV1 {
839
1354
  winner = first;
840
1355
  value = first.value;
841
1356
  first.value = UNIT;
1357
+ first.taken = true;
1358
+ task.value = value;
1359
+ task.resultInitialized = true;
842
1360
  }
843
1361
  }
844
1362
  if (!cancelled) {
@@ -869,13 +1387,18 @@ class ExecutorV1 {
869
1387
  .filter((member) => member !== winner)
870
1388
  .map((member) => {
871
1389
  const result = member.value;
872
- member.value = UNIT;
873
1390
  return result;
874
1391
  });
875
1392
  this.closeValueAsync({ kind: "vector", elementType: "race result", items: losers }, this.faultText(failure), (closeFault) => {
1393
+ for (const member of members)
1394
+ if (member !== winner) {
1395
+ member.value = UNIT;
1396
+ member.taken = true;
1397
+ }
876
1398
  failure = combineFaults(failure, closeFault);
877
1399
  const finish = (releaseFault = null) => {
878
1400
  failure = combineFaults(failure, releaseFault);
1401
+ this.releaseRaceMembers(task);
879
1402
  this.settleTask(task, failure ? UNIT : value, failure);
880
1403
  for (const changed of [...scope.changed])
881
1404
  changed();
@@ -941,8 +1464,9 @@ class ExecutorV1 {
941
1464
  const specialization = op.op === "task.invoke" && "specialization" in op.callee
942
1465
  ? op.callee.specialization
943
1466
  : null;
944
- const task = this.newTask();
1467
+ const task = this.newTask(act.context.actor?.budget, act.managedTypes.get(op.dst), 0n, true);
945
1468
  group.tasks.push(task);
1469
+ let releaseReturn = () => { };
946
1470
  const settle = (value, fault) => {
947
1471
  if (task.done || task.finishing)
948
1472
  return;
@@ -955,6 +1479,7 @@ class ExecutorV1 {
955
1479
  if (!fault || (fault.kind === "panic" && fault.cancelled))
956
1480
  fault = closeFault ?? fault;
957
1481
  this.settleTask(task, value, fault);
1482
+ releaseReturn();
958
1483
  if (!task.grouped &&
959
1484
  fault &&
960
1485
  !(fault.kind === "panic" && fault.cancelled)) {
@@ -1008,9 +1533,11 @@ class ExecutorV1 {
1008
1533
  const context = {
1009
1534
  id: task.id,
1010
1535
  actor: act.context.actor,
1536
+ retainsTransferableFrame: !!act.context.generator || act.context.retainsTransferableFrame,
1011
1537
  rcOwner: act.rcOwner,
1012
1538
  requestDeadline: this.requestDeadline(act),
1013
- returned: (value) => {
1539
+ returned: (value, released) => {
1540
+ releaseReturn = released;
1014
1541
  if (dynamicEntry?.receiver_handback &&
1015
1542
  dynamicReceiver?.kind === "dyn") {
1016
1543
  if (value.kind !== "record" || value.fields.length !== 2)
@@ -1039,6 +1566,9 @@ class ExecutorV1 {
1039
1566
  },
1040
1567
  };
1041
1568
  const frame = this.activate(this.functionAt(calleeFunction(callable, specialization)), callable.kind === "closure" ? [callable.environment, ...args] : args, null, null, undefined, null, context);
1569
+ task.frame = frame;
1570
+ task.callable = callable;
1571
+ task.invocationPending = true;
1042
1572
  task.cancel = () => {
1043
1573
  if (task.done || task.finishing)
1044
1574
  return;
@@ -1052,6 +1582,8 @@ class ExecutorV1 {
1052
1582
  this.scheduler.enqueue(task.id, () => {
1053
1583
  if (task.done || task.finishing)
1054
1584
  return;
1585
+ task.invocationPending = false;
1586
+ task.budget?.changed();
1055
1587
  this.runFrame(frame);
1056
1588
  });
1057
1589
  this.trace.allocateId("task", task.id, act.context.id);
@@ -1091,7 +1623,43 @@ class ExecutorV1 {
1091
1623
  }
1092
1624
  // ── terminators ──────────────────────────────────────────────────────────
1093
1625
  terminate(act, term) {
1626
+ const mutable = [
1627
+ "runtime.call",
1628
+ "extern.call",
1629
+ "value.call",
1630
+ "wire.codec",
1631
+ "suspend",
1632
+ ].includes(term.op);
1633
+ if (mutable)
1634
+ act.context.actor?.budget?.changed();
1635
+ try {
1636
+ this.terminateOperation(act, term);
1637
+ }
1638
+ finally {
1639
+ if (mutable)
1640
+ act.context.actor?.budget?.changed();
1641
+ }
1642
+ }
1643
+ terminateOperation(act, term) {
1094
1644
  switch (term.op) {
1645
+ case "materialize": {
1646
+ const budget = act.context.actor?.budget;
1647
+ try {
1648
+ if (term.operation.op === "array.repeat")
1649
+ this.arrayRepeatLength(term.operation);
1650
+ if (budget)
1651
+ this.prepareMaterialize(act, term.operation, budget);
1652
+ }
1653
+ catch (error) {
1654
+ if (!(error instanceof HeapExceeded))
1655
+ throw error;
1656
+ this.takeEdge(act, term.failure);
1657
+ return;
1658
+ }
1659
+ this.execute(act, term.operation);
1660
+ this.takeEdge(act, term.normal);
1661
+ return;
1662
+ }
1095
1663
  case "goto":
1096
1664
  this.takeEdge(act, term.edge);
1097
1665
  return;
@@ -1107,6 +1675,11 @@ class ExecutorV1 {
1107
1675
  throw new Error(`switch.variant has no arm for tag ${tag}`);
1108
1676
  }
1109
1677
  this.spread(act, payloadOf(scrutinee), arm.fields);
1678
+ if (act.owned.has(term.scrutinee)) {
1679
+ this.invalidate(act, term.scrutinee);
1680
+ this.managed.retire(scrutinee);
1681
+ act.context.actor?.budget?.changed();
1682
+ }
1110
1683
  this.takeEdge(act, arm.edge);
1111
1684
  return;
1112
1685
  }
@@ -1130,7 +1703,13 @@ class ExecutorV1 {
1130
1703
  case "checked.binary": {
1131
1704
  const failure = checkedFailure(term.binary_op, this.read(act, term.lhs), this.read(act, term.rhs), term.ty);
1132
1705
  if (failure) {
1133
- const edge = term.failures.find((candidate) => candidate.trap === failure);
1706
+ // Earlier v1 packages fold the signed minimum by -1 into their
1707
+ // overflow edge, so they keep the IntegerOverflow trap their
1708
+ // compiler emitted.
1709
+ const edge = term.failures.find((candidate) => candidate.trap === failure) ??
1710
+ (failure === "signed_min_div_neg_one"
1711
+ ? term.failures.find((candidate) => candidate.trap === "integer_overflow")
1712
+ : undefined);
1134
1713
  if (!edge) {
1135
1714
  throw new Error(`checked.binary names no ${failure} edge`);
1136
1715
  }
@@ -1247,12 +1826,30 @@ class ExecutorV1 {
1247
1826
  return;
1248
1827
  }
1249
1828
  case "collection": {
1829
+ const receiverType = term.args[0] && act.managedTypes.get(term.args[0].value);
1830
+ const receiverLayout = receiverType == null ? undefined : this.managed.row(receiverType);
1831
+ const elementType = receiverLayout &&
1832
+ (receiverLayout.kind === "vector" ||
1833
+ receiverLayout.kind === "array")
1834
+ ? receiverLayout.element
1835
+ : undefined;
1250
1836
  const args = term.args.map((operand) => this.boundary(act, operand));
1251
- const iterator = collectionOperation(this.pkg, registration, args, term.callbacks ?? [], term.result_member_shapes?.[1] ?? term.result_shape);
1837
+ const iterator = collectionOperation(this.pkg, registration, args, term.callbacks ?? [], term.result_member_shapes?.[1] ?? term.result_shape, elementType);
1252
1838
  this.runValueProgram(act, term, args, iterator);
1253
1839
  return;
1254
1840
  }
1255
1841
  case "shared": {
1842
+ if (act.context.actor?.budget &&
1843
+ (registration.operation === "RcNew" ||
1844
+ registration.operation === "RcSet")) {
1845
+ const constructing = registration.operation === "RcNew";
1846
+ const operand = term.args[constructing ? 0 : 1];
1847
+ const type = act.managedTypes.get(operand.value);
1848
+ if (type == null)
1849
+ throw new Error("Rc payload lacks its typed descriptor");
1850
+ if (!this.checkRuntimePayload(act, term, constructing ? this.managed.width(type) : 0n, [{ value: this.read(act, operand.value), type }]))
1851
+ return;
1852
+ }
1256
1853
  const args = term.args.map((operand) => this.boundary(act, operand));
1257
1854
  this.runValueProgram(act, term, args, sharedOperation(registration.operation, args, act.rcOwner, term.shared_payload_type ?? "", (value) => this.variant(term.result_shape, value ? "OptionSome" : "OptionNone", value ? [value] : [])));
1258
1855
  return;
@@ -1405,6 +2002,26 @@ class ExecutorV1 {
1405
2002
  tag: deadline ? term.deadline_variant : term.fault_variant,
1406
2003
  payload: [stringValue(faultRecoveryMessage(fault))],
1407
2004
  };
2005
+ // Scope recovery must preserve the original fault even when the
2006
+ // current actor cannot retain its diagnostic text. Preflight the same
2007
+ // managed graph that completion will retain, then use the canonical
2008
+ // empty string on refusal without publishing an uncharged owner.
2009
+ const result = term.result?.value;
2010
+ const type = result == null ? undefined : act.managedTypes.get(result);
2011
+ const budget = result != null && act.owned.has(result)
2012
+ ? act.context.actor?.budget
2013
+ : undefined;
2014
+ if (budget && type != null) {
2015
+ try {
2016
+ this.managed.stampGraph({ value, type });
2017
+ budget.check([{ value, type }]);
2018
+ }
2019
+ catch (error) {
2020
+ if (!(error instanceof HeapExceeded))
2021
+ throw error;
2022
+ value.payload[0] = stringValue("");
2023
+ }
2024
+ }
1408
2025
  act.fault = null;
1409
2026
  if (act.context.actor)
1410
2027
  act.context.actor.crashing = null;
@@ -1425,11 +2042,150 @@ class ExecutorV1 {
1425
2042
  }
1426
2043
  }
1427
2044
  }
2045
+ /** Reserve the exact constructor/copy graph before any operand is consumed. */
2046
+ arrayRepeatLength(op) {
2047
+ const type = op.definitions?.[0]?.managed_type;
2048
+ const row = type == null ? undefined : this.managed.row(type);
2049
+ if (row?.kind !== "array")
2050
+ throw new Error("array repeat lacks its canonical length");
2051
+ const count = BigInt(row.length);
2052
+ this.managed.width(row.element, count);
2053
+ if (count > 0xffffffffn)
2054
+ throw new HeapExceeded();
2055
+ return Number(count);
2056
+ }
2057
+ prepareMaterialize(act, op, budget) {
2058
+ const type = op.definitions?.[0]?.managed_type;
2059
+ if (type == null)
2060
+ throw new Error("materialization has no managed result descriptor");
2061
+ const read = (id) => this.read(act, id);
2062
+ const copy = (value) => this.managed.copy(value, type);
2063
+ let value;
2064
+ switch (op.op) {
2065
+ case "const.str":
2066
+ value = stringValue(this.pkg.strings[op.str] ?? "");
2067
+ break;
2068
+ case "const.bytes":
2069
+ value = bytesValue(this.pkg.bytes[op.bytes] ?? []);
2070
+ break;
2071
+ case "copy_value":
2072
+ value = copy(read(op.source));
2073
+ break;
2074
+ case "load.copy":
2075
+ value = copy(readRef(this.placeRef(act, op.place)));
2076
+ break;
2077
+ case "tuple.get":
2078
+ value = copy(fieldsOf(read(op.tuple))[op.index]);
2079
+ break;
2080
+ case "aggregate.project_copy":
2081
+ value = copy(fieldsOf(read(op.aggregate))[op.field]);
2082
+ break;
2083
+ case "variant.project_copy":
2084
+ value = copy(payloadOf(read(op.source))[op.field]);
2085
+ break;
2086
+ case "array.make":
2087
+ value = {
2088
+ kind: "vector",
2089
+ elementType: "",
2090
+ items: op.elements.map(read),
2091
+ };
2092
+ break;
2093
+ case "array.repeat": {
2094
+ const count = this.arrayRepeatLength(op);
2095
+ const row = this.managed.row(type);
2096
+ if (row.kind !== "array")
2097
+ throw new Error("array repeat lacks its element descriptor");
2098
+ const empty = [];
2099
+ const storage = {
2100
+ items: empty,
2101
+ capacity: count,
2102
+ owners: 1,
2103
+ bytes: false,
2104
+ shareOnCopy: row.share_on_copy,
2105
+ };
2106
+ value = { kind: "vector", elementType: "", items: empty, storage };
2107
+ budget.check([{ value, type }]);
2108
+ const seed = read(op.value);
2109
+ storage.items = Array.from({ length: count }, (_, index) => index ? this.managed.copy(seed, row.element) : seed);
2110
+ value.items = storage.items;
2111
+ break;
2112
+ }
2113
+ case "variant.make":
2114
+ value = {
2115
+ kind: "enum",
2116
+ typeId: "",
2117
+ shape: op.shape,
2118
+ tag: op.variant,
2119
+ payload: op.fields.map(read),
2120
+ };
2121
+ break;
2122
+ case "closure.make":
2123
+ value = {
2124
+ kind: "closure",
2125
+ entries: this.closureAt(op.closure).entries,
2126
+ specialization: null,
2127
+ environment: {
2128
+ kind: "record",
2129
+ typeId: "",
2130
+ fields: op.fields.map(read),
2131
+ },
2132
+ };
2133
+ break;
2134
+ case "dyn.make":
2135
+ value = { kind: "dyn", vtable: op.vtable, value: read(op.value) };
2136
+ break;
2137
+ case "generator.make": {
2138
+ const row = this.managed.row(type);
2139
+ if (row.kind !== "generator")
2140
+ throw new Error("generator lacks its payload descriptor");
2141
+ const callable = read(op.callable);
2142
+ budget.checkPayload(BigInt(row.payload_bytes), [
2143
+ { value: callable, type: this.managed.type(callable) },
2144
+ ]);
2145
+ return;
2146
+ }
2147
+ case "task.spawn":
2148
+ case "task.invoke":
2149
+ case "task.race": {
2150
+ const row = this.managed.row(type);
2151
+ if (row.kind !== "provider" || row.provider !== "task")
2152
+ throw new Error("checked task lacks its canonical provider descriptor");
2153
+ const carrier = op.op === "task.race"
2154
+ ? BigInt(op.member_slot_bytes) * BigInt(op.members.length)
2155
+ : BigInt(row.invocation_slot_bytes);
2156
+ const children = [];
2157
+ if (op.op === "task.invoke") {
2158
+ const operands = [...op.args];
2159
+ if ("receiver" in op.callee)
2160
+ operands.push(op.callee.receiver);
2161
+ for (const operand of operands) {
2162
+ const type = act.managedTypes.get(operand.value);
2163
+ if (type == null)
2164
+ throw new Error("task input lacks its canonical descriptor");
2165
+ const value = read(operand.value);
2166
+ children.push({
2167
+ value: operand.decision === "copy" || operand.decision === "snapshot"
2168
+ ? this.managed.copy(value, type)
2169
+ : value,
2170
+ type,
2171
+ });
2172
+ }
2173
+ }
2174
+ budget.checkPayload(this.managed.width(row.children[0]) + carrier, children);
2175
+ return;
2176
+ }
2177
+ default:
2178
+ throw new Error(`unexpected managed materialization ${op.op}`);
2179
+ }
2180
+ budget.check([{ value, type }]);
2181
+ }
1428
2182
  takeEdge(act, edge) {
1429
2183
  // Read every argument before rebinding, so a block that branches to itself
1430
2184
  // with permuted parameters binds the incoming values, not the rebound ones.
1431
2185
  const args = edge.args.map((id) => this.refOf(act, id));
1432
2186
  const target = this.blockAt(act.fn, edge.to);
2187
+ for (const lease of act.managedInputs.splice(0))
2188
+ lease.release();
1433
2189
  for (const [at, param] of target.params.entries()) {
1434
2190
  act.env.set(param.value, args[at] ?? ownedRef(UNIT));
1435
2191
  }
@@ -1459,9 +2215,53 @@ class ExecutorV1 {
1459
2215
  throw error;
1460
2216
  });
1461
2217
  }
1462
- /// Run a runtime or extern shim. A fault it raises takes the call's `unwind`
1463
- /// edge so the cleanup the package names runs before the program ends.
2218
+ /** Reserve typed runtime payload before construction or replacement. Refusal
2219
+ * follows the call's existing input ownership and authored cleanup edge. */
2220
+ checkRuntimePayload(act, term, bytes, children) {
2221
+ const budget = act.context.actor?.budget;
2222
+ if (!budget)
2223
+ return true;
2224
+ try {
2225
+ budget.checkPayload(bytes, children);
2226
+ return true;
2227
+ }
2228
+ catch (error) {
2229
+ if (!(error instanceof HeapExceeded))
2230
+ throw error;
2231
+ const fault = { kind: "trap", trap: "heap_exceeded" };
2232
+ if (this.keepsInputs(term)) {
2233
+ // No new payload exists and none of the consuming inputs has moved.
2234
+ this.raiseFault(act, fault, term.unwind);
2235
+ return false;
2236
+ }
2237
+ const consumed = term.args
2238
+ .filter((operand) => operand.decision === "move")
2239
+ .map((operand) => this.boundary(act, operand));
2240
+ this.running = false;
2241
+ this.closeValueAsync({ kind: "record", typeId: "", fields: consumed }, error.message, (cleanupFault) => {
2242
+ this.raiseFault(act, combineFaults(fault, cleanupFault), term.unwind);
2243
+ this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2244
+ }, act.context.actor, act.rcOwner, act);
2245
+ return false;
2246
+ }
2247
+ }
2248
+ /// Run a runtime or extern shim through its authored fault continuation.
1464
2249
  callShim(act, term, shim) {
2250
+ const family = term.op === "runtime.call"
2251
+ ? this.pkg.runtime_families[term.family]?.family
2252
+ : undefined;
2253
+ const budget = act.context.actor?.budget;
2254
+ if (budget &&
2255
+ (family === "SinkFromEncoder" || family === "StreamFromGenerator")) {
2256
+ const children = term.args.map((operand) => {
2257
+ const type = act.managedTypes.get(operand.value);
2258
+ if (type == null)
2259
+ throw new Error("stream adapter input lacks its typed descriptor");
2260
+ return { value: this.read(act, operand.value), type };
2261
+ });
2262
+ if (!this.checkRuntimePayload(act, term, children.reduce((bytes, child) => bytes + this.managed.width(child.type), 0n), children))
2263
+ return;
2264
+ }
1465
2265
  const args = term.args.map((operand) => this.boundary(act, operand));
1466
2266
  // A call that hands back its updated receiver names its value's shape as
1467
2267
  // the pair's second member.
@@ -1477,9 +2277,13 @@ class ExecutorV1 {
1477
2277
  value = shim(this.host, args, shape, aggregate);
1478
2278
  }
1479
2279
  catch (error) {
1480
- if (error instanceof ShimFault) {
2280
+ if (error instanceof ShimFault || error instanceof HeapExceeded) {
1481
2281
  this.keepInputs(act, term, args);
1482
- this.raiseFault(act, { kind: "trap", trap: error.trap, message: error.message }, term.unwind);
2282
+ this.raiseFault(act, {
2283
+ kind: "trap",
2284
+ trap: error instanceof HeapExceeded ? "heap_exceeded" : error.trap,
2285
+ message: error.message,
2286
+ }, term.unwind);
1483
2287
  return;
1484
2288
  }
1485
2289
  throw error;
@@ -1523,16 +2327,21 @@ class ExecutorV1 {
1523
2327
  this.keepInputs(act, term, args);
1524
2328
  this.raiseFault(act, fault, term.unwind);
1525
2329
  resume();
1526
- }, act.context.actor, act.rcOwner);
2330
+ }, act.context.actor, act.rcOwner, act);
1527
2331
  };
1528
2332
  const next = (value = UNIT) => {
1529
2333
  let step;
1530
2334
  try {
1531
2335
  step = iterator.next(value);
2336
+ act.context.actor?.budget?.changed();
1532
2337
  }
1533
2338
  catch (error) {
1534
- if (error instanceof ShimFault) {
1535
- fail({ kind: "trap", trap: error.trap, message: error.message });
2339
+ if (error instanceof ShimFault || error instanceof HeapExceeded) {
2340
+ fail({
2341
+ kind: "trap",
2342
+ trap: error instanceof HeapExceeded ? "heap_exceeded" : error.trap,
2343
+ message: error.message,
2344
+ });
1536
2345
  return;
1537
2346
  }
1538
2347
  throw error;
@@ -1546,7 +2355,16 @@ class ExecutorV1 {
1546
2355
  }
1547
2356
  else if (step.value.kind === "release") {
1548
2357
  const request = step.value;
2358
+ const lease = act.context.actor?.budget ||
2359
+ act.context.generator ||
2360
+ act.context.retainsTransferableFrame
2361
+ ? this.retainCleanup(act, (request.retained ?? [request.value]).map((value) => ({
2362
+ value,
2363
+ type: request.retainedType ?? this.managed.type(value),
2364
+ })))
2365
+ : undefined;
1549
2366
  this.closeValueAsync(request.value, this.pipeFault(act), (fault) => {
2367
+ lease?.release();
1550
2368
  if (fault) {
1551
2369
  if (term.op !== "runtime.call" || !term.releases_contents) {
1552
2370
  fail(fault, request.onFault);
@@ -1557,11 +2375,11 @@ class ExecutorV1 {
1557
2375
  act.fault ??= fault;
1558
2376
  }
1559
2377
  next();
1560
- }, act.context.actor, act.rcOwner);
2378
+ }, act.context.actor, act.rcOwner, act);
1561
2379
  }
1562
2380
  else {
1563
2381
  const request = step.value;
1564
- const child = this.invokeFrame(act.context.actor, request.callee, request.args, next, fail, act.rcOwner);
2382
+ const child = this.invokeFrame(act.context.actor, request.callee, request.args, next, fail, act.rcOwner, false, act);
1565
2383
  act.context.cancel = (fault) => child.cancel?.(fault);
1566
2384
  }
1567
2385
  };
@@ -1575,13 +2393,13 @@ class ExecutorV1 {
1575
2393
  if (operation === "Len") {
1576
2394
  this.completeShim(act, term, {
1577
2395
  kind: "i64",
1578
- value: BigInt(view.items.length),
2396
+ value: BigInt(vectorLength(view)),
1579
2397
  });
1580
2398
  return;
1581
2399
  }
1582
2400
  if (index?.kind !== "i64")
1583
2401
  throw new Error("pool operation has no index");
1584
- const value = view.items[Number(index.value)];
2402
+ const value = vectorElement(view, Number(index.value));
1585
2403
  if (operation === "Get") {
1586
2404
  this.completeShim(act, term, this.variant(term.result_shape, value ? "OptionSome" : "OptionNone", value ? [value] : []));
1587
2405
  return;
@@ -1618,7 +2436,7 @@ class ExecutorV1 {
1618
2436
  }, (fault) => {
1619
2437
  this.raiseFault(act, fault, term.unwind);
1620
2438
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
1621
- });
2439
+ }, act.context.actor?.rcOwner ?? null, false, undefined, act);
1622
2440
  act.context.cancel = (fault) => callback.cancel?.(fault);
1623
2441
  };
1624
2442
  continueRendering();
@@ -1629,7 +2447,17 @@ class ExecutorV1 {
1629
2447
  throw new Error(`${term.op} with a "never" result cannot return in the sandbox`);
1630
2448
  }
1631
2449
  if (term.result && term.result !== "never") {
1632
- this.define(act, term.result.value, value);
2450
+ try {
2451
+ this.define(act, term.result.value, value);
2452
+ }
2453
+ catch (error) {
2454
+ if (!(error instanceof HeapExceeded))
2455
+ throw error;
2456
+ this.closeValueAsync(value, error.message, (fault) => {
2457
+ this.raiseFault(act, combineFaults({ kind: "trap", trap: "heap_exceeded" }, fault), "unwind" in term ? term.unwind : null);
2458
+ }, act.context.actor, act.rcOwner);
2459
+ return;
2460
+ }
1633
2461
  }
1634
2462
  this.takeEdge(act, term.normal);
1635
2463
  }
@@ -1675,9 +2503,20 @@ class ExecutorV1 {
1675
2503
  }
1676
2504
  const cell = item.value;
1677
2505
  const payload = cell.value;
2506
+ let payloadLease;
2507
+ if (actor?.budget && cell.managedPayload != null)
2508
+ payloadLease = actor.budget.retain({
2509
+ value: payload,
2510
+ type: cell.managedPayload,
2511
+ slots: true,
2512
+ identity: cell,
2513
+ });
2514
+ cell.managedLease?.release();
2515
+ cell.managedLease = undefined;
1678
2516
  cell.value = UNIT;
1679
2517
  blocks.delete(cell);
1680
2518
  this.closeValueAsync(payload, this.faultText(firstFault), (fault) => {
2519
+ payloadLease?.release();
1681
2520
  firstFault = combineFaults(firstFault, fault);
1682
2521
  release();
1683
2522
  }, actor, owner);
@@ -1724,7 +2563,39 @@ class ExecutorV1 {
1724
2563
  const caller = act.caller;
1725
2564
  if (!caller) {
1726
2565
  this.running = false;
1727
- act.context.returned(value);
2566
+ let reply;
2567
+ if (act.context.reply &&
2568
+ act.context.actor?.budget &&
2569
+ act.block.term.op === "return" &&
2570
+ act.block.term.value) {
2571
+ const type = act.managedTypes.get(act.block.term.value.value);
2572
+ if (type == null)
2573
+ throw new Error("actor reply lacks its managed descriptor");
2574
+ try {
2575
+ if (type != null)
2576
+ reply = act.context.actor.budget.retain({
2577
+ value,
2578
+ type,
2579
+ slots: true,
2580
+ });
2581
+ }
2582
+ catch (error) {
2583
+ if (!(error instanceof HeapExceeded))
2584
+ throw error;
2585
+ this.closeValueAsync(value, error.message, (fault) => {
2586
+ for (const lease of act.managedInputs.splice(0))
2587
+ lease.release();
2588
+ act.context.failed(combineFaults({ kind: "trap", trap: "heap_exceeded" }, fault));
2589
+ }, act.context.actor, act.rcOwner);
2590
+ return;
2591
+ }
2592
+ }
2593
+ const inputs = act.managedInputs.splice(0);
2594
+ act.context.returned(value, () => {
2595
+ reply?.release();
2596
+ for (const lease of inputs)
2597
+ lease.release();
2598
+ });
1728
2599
  return;
1729
2600
  }
1730
2601
  if (!act.normal) {
@@ -1744,6 +2615,8 @@ class ExecutorV1 {
1744
2615
  }
1745
2616
  this.current = caller;
1746
2617
  this.takeEdge(caller, act.normal);
2618
+ for (const lease of act.managedInputs.splice(0))
2619
+ lease.release();
1747
2620
  }
1748
2621
  dynamicEntry(receiver, slot) {
1749
2622
  const table = this.pkg.vtables[receiver.vtable] ??
@@ -1792,6 +2665,8 @@ class ExecutorV1 {
1792
2665
  if (!caller) {
1793
2666
  this.running = false;
1794
2667
  act.context.failed(fault, handback);
2668
+ for (const lease of act.managedInputs.splice(0))
2669
+ lease.release();
1795
2670
  return;
1796
2671
  }
1797
2672
  if (!act.unwind) {
@@ -1811,6 +2686,8 @@ class ExecutorV1 {
1811
2686
  caller.fault = fault;
1812
2687
  this.current = caller;
1813
2688
  this.takeEdge(caller, act.unwind);
2689
+ for (const lease of act.managedInputs.splice(0))
2690
+ lease.release();
1814
2691
  }
1815
2692
  finishConsumedCallable(act, value, done) {
1816
2693
  const callable = act.consumedCallable;
@@ -1850,6 +2727,7 @@ class ExecutorV1 {
1850
2727
  const generator = act.context.generator;
1851
2728
  if (!generator || !generator.receive)
1852
2729
  throw new Error("yield has no generator consumer");
2730
+ generator.frame = act;
1853
2731
  const value = this.boundary(act, term.inputs[0]);
1854
2732
  const wake = this.park(act, term);
1855
2733
  generator.resume = () => wake(UNIT);
@@ -1895,9 +2773,27 @@ class ExecutorV1 {
1895
2773
  case "StreamSend": {
1896
2774
  const sink = this.boundary(act, term.inputs[0]);
1897
2775
  const value = this.boundary(act, term.inputs[1]);
2776
+ const type = act.managedTypes.get(term.inputs[1].value);
2777
+ if (type == null)
2778
+ throw new Error("stream write lacks its typed input descriptor");
2779
+ // The native operation owns one aligned input slot until accepted or
2780
+ // terminal cleanup, independently of the receiving queue's slot.
1898
2781
  const wake = this.park(act, term);
2782
+ try {
2783
+ act.managedInputs.push(this.retainRoots([{ value, type, slots: true, identity: {} }], act.context.actor?.budget));
2784
+ }
2785
+ catch (error) {
2786
+ if (!(error instanceof HeapExceeded))
2787
+ throw error;
2788
+ const refused = { kind: "trap", trap: "heap_exceeded" };
2789
+ this.closeValueAsync(value, error.message, (fault) => wake(UNIT, combineFaults(refused, fault)), act.context.actor, act.rcOwner, act);
2790
+ return;
2791
+ }
1899
2792
  const cancel = act.context.cancel;
1900
2793
  const stop = this.sendStream(sink, value, term.detail.park, act.context.actor, (status, fault) => {
2794
+ if (status === 0)
2795
+ for (const lease of act.managedInputs.splice(0))
2796
+ lease.release();
1901
2797
  wake(UNIT, fault, status);
1902
2798
  }, act.rcOwner);
1903
2799
  act.context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
@@ -1915,8 +2811,10 @@ class ExecutorV1 {
1915
2811
  return;
1916
2812
  }
1917
2813
  let cancelWait = () => { };
1918
- const wake = this.park(act, term, () => cancelWait());
1919
- cancelWait = this.pipes.receive(stream, term.detail.park, (value, fault) => {
2814
+ let releaseEnvelope = () => { };
2815
+ const wake = this.park(act, term, () => cancelWait(), undefined, () => releaseEnvelope());
2816
+ cancelWait = this.pipes.receive(stream, term.detail.park, (value, fault, released) => {
2817
+ releaseEnvelope = released ?? (() => { });
1920
2818
  if (fault !== null)
1921
2819
  wake(UNIT, { kind: "panic", message: fault });
1922
2820
  else
@@ -1925,9 +2823,17 @@ class ExecutorV1 {
1925
2823
  return;
1926
2824
  }
1927
2825
  case "Await": {
1928
- const task = this.taskFor(this.boundary(act, term.inputs[0]));
2826
+ const handle = this.boundary(act, term.inputs[0]);
2827
+ const task = this.taskFor(handle);
1929
2828
  let unwatch = () => { };
1930
- const wake = this.park(act, term, () => unwatch());
2829
+ let resultLease;
2830
+ const wake = this.park(act, term, () => unwatch(), undefined, () => {
2831
+ resultLease?.release();
2832
+ task.handles.delete(handle);
2833
+ this.managed.retire(handle);
2834
+ task.budget?.changed();
2835
+ this.releaseTaskState(task);
2836
+ });
1931
2837
  const ready = () => {
1932
2838
  if (!task.done) {
1933
2839
  this.pendingActorWait(act, [task.actorTarget], false, "ask", (fault) => wake(UNIT, fault));
@@ -1937,7 +2843,9 @@ class ExecutorV1 {
1937
2843
  wake(UNIT, task.fault);
1938
2844
  else {
1939
2845
  const value = task.value;
2846
+ resultLease = this.retainTaskResult(task, act.context.actor?.budget);
1940
2847
  task.value = UNIT;
2848
+ task.taken = true;
1941
2849
  wake(value);
1942
2850
  }
1943
2851
  };
@@ -1997,12 +2905,28 @@ class ExecutorV1 {
1997
2905
  const wake = this.park(act, term, () => unwatch.forEach((remove) => remove()));
1998
2906
  let cancelTimer;
1999
2907
  const choose = () => {
2000
- const ready = sources.flatMap((value, index) => {
2001
- if (value.kind === "stream")
2002
- return this.pipes.readable(value) ? [{ index, order: 0 }] : [];
2003
- const task = this.taskFor(value);
2004
- return task.done ? [{ index, order: task.completedAt }] : [];
2005
- });
2908
+ const ready = [];
2909
+ for (const [index, value] of sources.entries()) {
2910
+ if (value.kind === "stream") {
2911
+ const adapter = this.generatorStreams.get(value.channelId);
2912
+ if (adapter)
2913
+ this.pollGeneratorStream(adapter);
2914
+ const readable = adapter
2915
+ ? adapter.pending !== null ||
2916
+ adapter.generator.done ||
2917
+ adapter.closed
2918
+ : this.pipes.readable(value);
2919
+ if (readable)
2920
+ ready.push({ index, order: 0 });
2921
+ }
2922
+ else {
2923
+ const task = this.taskFor(value);
2924
+ if (task.done)
2925
+ ready.push({ index, order: task.completedAt });
2926
+ }
2927
+ if (ready.length && term.detail.order !== "completion")
2928
+ break;
2929
+ }
2006
2930
  if (term.detail.order === "completion")
2007
2931
  ready.sort((a, b) => a.order - b.order);
2008
2932
  if (ready[0]) {
@@ -2022,8 +2946,15 @@ class ExecutorV1 {
2022
2946
  cancelTimer = this.scheduler.after(duration.value, () => wake({ kind: "i64", value: BigInt(sources.length) }));
2023
2947
  }
2024
2948
  sources.forEach((value) => {
2025
- if (value.kind === "stream")
2026
- unwatch.push(this.pipes.observe(value, choose));
2949
+ if (value.kind === "stream") {
2950
+ const adapter = this.generatorStreams.get(value.channelId);
2951
+ if (adapter) {
2952
+ adapter.observers.add(choose);
2953
+ unwatch.push(() => adapter.observers.delete(choose));
2954
+ }
2955
+ else
2956
+ unwatch.push(this.pipes.observe(value, choose));
2957
+ }
2027
2958
  else {
2028
2959
  const task = this.taskFor(value);
2029
2960
  task.changed.push(choose);
@@ -2118,6 +3049,16 @@ class ExecutorV1 {
2118
3049
  return;
2119
3050
  }
2120
3051
  case "Safepoint": {
3052
+ // A function's entry safepoint is its stack check (HEW-SPEC-2026
3053
+ // §2.2): past the frame limit the call fails with `StackOverflow`
3054
+ // and its parameters follow the cancellation cleanup.
3055
+ if (term.cancel &&
3056
+ act.depth > MAX_FRAME_DEPTH &&
3057
+ act.block.id === act.fn.entry) {
3058
+ act.fault = { kind: "trap", trap: "stack_overflow" };
3059
+ this.takeEdge(act, term.cancel);
3060
+ return;
3061
+ }
2121
3062
  if (act.context.pollSuppression) {
2122
3063
  this.takeEdge(act, term.resumes[0]);
2123
3064
  return;
@@ -2178,6 +3119,10 @@ class ExecutorV1 {
2178
3119
  let cancelTimer = () => { };
2179
3120
  const wake = this.park(act, term, () => cancelTimer());
2180
3121
  cancelTimer = this.scheduler.after(nanos, () => wake(UNIT));
3122
+ // Admission can publish a fault synchronously before this sleep parks.
3123
+ // Deliver that latch after arming, so cancellation also removes the timer.
3124
+ if (act.context.pendingCancellation)
3125
+ act.context.cancel?.(act.context.pendingCancellation);
2181
3126
  return;
2182
3127
  }
2183
3128
  default: {
@@ -2203,13 +3148,30 @@ class ExecutorV1 {
2203
3148
  message: `local actor wait cycle at ${operation}: ${path.map((actor) => actor.identity).join(" -> ")}`,
2204
3149
  });
2205
3150
  }
2206
- park(act, term, cleanup = () => { }, accepted = () => { }) {
3151
+ park(act, term, cleanup = () => { }, accepted = () => { }, drained = () => { }) {
2207
3152
  this.running = false;
2208
3153
  act.context.parked = true;
2209
3154
  let pending = null;
3155
+ let pendingLease;
2210
3156
  const resume = (value, fault = null, edge = 0) => {
2211
3157
  if (pending)
2212
3158
  return false;
3159
+ if (!fault &&
3160
+ act.context.actor?.budget &&
3161
+ term.result &&
3162
+ term.result !== "never") {
3163
+ const type = term.result.managed_type;
3164
+ if (type == null)
3165
+ throw new Error("suspended result lacks its managed descriptor");
3166
+ try {
3167
+ pendingLease = act.context.actor.budget.retain({ value, type });
3168
+ }
3169
+ catch (error) {
3170
+ if (!(error instanceof HeapExceeded))
3171
+ throw error;
3172
+ fault = { kind: "trap", trap: "heap_exceeded" };
3173
+ }
3174
+ }
2213
3175
  pending = { value, fault, edge };
2214
3176
  act.context.parked = false;
2215
3177
  if (act.context.actor?.active === act.context)
@@ -2236,6 +3198,8 @@ class ExecutorV1 {
2236
3198
  throw new Error(`${term.kind} has no resume edge`);
2237
3199
  this.takeEdge(act, edge);
2238
3200
  }
3201
+ pendingLease?.release();
3202
+ drained();
2239
3203
  this.runFrame(act);
2240
3204
  };
2241
3205
  // A close can arrive after readiness but before this frame runs. Keep
@@ -2440,7 +3404,16 @@ class ExecutorV1 {
2440
3404
  adapter.waiting = undefined;
2441
3405
  this.closeGenerator(generator, (closeFault) => cancel?.(closeFault ?? fault));
2442
3406
  };
2443
- if (adapter.receiving)
3407
+ this.pollGeneratorStream(adapter);
3408
+ }
3409
+ /** Readiness drives the same producer as recv but leaves a published item
3410
+ * retained until the winning receive. Losing observations only detach. */
3411
+ pollGeneratorStream(adapter) {
3412
+ const generator = adapter.generator;
3413
+ if (adapter.receiving ||
3414
+ adapter.pending !== null ||
3415
+ generator.done ||
3416
+ adapter.closed)
2444
3417
  return;
2445
3418
  adapter.receiving = true;
2446
3419
  generator.receive = (value, fault) => {
@@ -2457,6 +3430,8 @@ class ExecutorV1 {
2457
3430
  waiting(value, fault);
2458
3431
  else
2459
3432
  adapter.pending = value;
3433
+ for (const changed of [...adapter.observers])
3434
+ changed();
2460
3435
  };
2461
3436
  if (generator.started) {
2462
3437
  const resume = generator.resume;
@@ -2499,7 +3474,7 @@ class ExecutorV1 {
2499
3474
  act.fault = combineFaults(act.fault, fault);
2500
3475
  if (!synchronous)
2501
3476
  this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
2502
- }, act.context.actor, act.rcOwner);
3477
+ }, act.context.actor, act.rcOwner, act);
2503
3478
  synchronous = false;
2504
3479
  if (!completed)
2505
3480
  this.running = false;
@@ -2537,7 +3512,7 @@ class ExecutorV1 {
2537
3512
  else
2538
3513
  generator.frame.context.cancel?.();
2539
3514
  }
2540
- closeValueAsync(value, fault, done, actor = this.current.context.actor, rcOwner = actor?.rcOwner ?? this.current.rcOwner) {
3515
+ closeValueAsync(value, fault, done, actor = this.current.context.actor, rcOwner = actor?.rcOwner ?? this.current.rcOwner, retainedBy) {
2541
3516
  const pending = [value];
2542
3517
  let firstFault = null;
2543
3518
  const next = (failure = null) => {
@@ -2546,10 +3521,38 @@ class ExecutorV1 {
2546
3521
  // must disclose that failure instead of publishing a clean end.
2547
3522
  const currentFault = fault ?? this.faultText(actor?.crashing ?? firstFault);
2548
3523
  while (pending.length) {
2549
- const value = pending.pop();
3524
+ const entry = pending.pop();
3525
+ if ("managedRelease" in entry) {
3526
+ entry.managedRelease.release();
3527
+ continue;
3528
+ }
3529
+ if ("taskTaken" in entry) {
3530
+ entry.taskTaken.value = UNIT;
3531
+ entry.taskTaken.taken = true;
3532
+ entry.taskTaken.managedResult = undefined;
3533
+ entry.taskTaken.managedReply = undefined;
3534
+ continue;
3535
+ }
3536
+ if ("retire" in entry) {
3537
+ if (entry.retire.kind === "stream" &&
3538
+ !this.generatorStreams.has(entry.retire.channelId))
3539
+ this.pipes.finishClose(entry.retire);
3540
+ this.managed.retire(entry.retire);
3541
+ if (entry.retire.kind === "stream")
3542
+ this.generatorStreams.delete(entry.retire.channelId);
3543
+ actor?.budget?.changed();
3544
+ if (entry.retire.kind === "task") {
3545
+ const task = this.tasks.get(entry.retire.id);
3546
+ if (task)
3547
+ this.releaseTaskState(task);
3548
+ }
3549
+ continue;
3550
+ }
3551
+ const value = entry;
3552
+ pending.push({ retire: value });
2550
3553
  switch (value.kind) {
2551
3554
  case "encoding":
2552
- value.value = null;
3555
+ releaseEncoding(value);
2553
3556
  break;
2554
3557
  case "string":
2555
3558
  value.release();
@@ -2558,6 +3561,21 @@ class ExecutorV1 {
2558
3561
  if (!value.closed) {
2559
3562
  value.closed = true;
2560
3563
  if (value.cell && !value.cell.dying && --value.cell.refs === 0) {
3564
+ if (actor?.budget) {
3565
+ const row = this.managed.row(this.managed.type(value));
3566
+ if (row.kind !== "shared")
3567
+ throw new Error("Rc has no canonical payload descriptor");
3568
+ pending.push({
3569
+ managedRelease: actor.budget.retain({
3570
+ value: value.cell.value,
3571
+ type: row.payload,
3572
+ slots: true,
3573
+ identity: value.cell,
3574
+ }),
3575
+ });
3576
+ }
3577
+ value.cell.managedLease?.release();
3578
+ value.cell.managedLease = undefined;
2561
3579
  value.cell.owner?.blocks.delete(value.cell);
2562
3580
  value.cell.owner = null;
2563
3581
  pending.push(value.cell.value);
@@ -2586,17 +3604,18 @@ class ExecutorV1 {
2586
3604
  const resource = this.pkg.resources?.[value.resource];
2587
3605
  if (resource?.kind !== "opaque")
2588
3606
  throw new Error("opaque owner lost its selected close");
2589
- this.invokeFrame(actor, resource.close, [value], () => next(), next, rcOwner);
3607
+ this.invokeFrame(actor, resource.close, [value], () => next(), next, rcOwner, false, retainedBy);
2590
3608
  return;
2591
3609
  }
2592
3610
  case "task": {
2593
3611
  // Dropping a handle does not cancel scoped work. Join drains the
2594
3612
  // task, and scope close releases any result that was never taken.
2595
3613
  const task = this.tasks.get(value.id);
2596
- if (task?.done) {
3614
+ const owned = task?.handles.delete(value);
3615
+ if (task?.done && (!owned || !task.scopeAlive)) {
2597
3616
  const result = task.value;
2598
- task.value = UNIT;
2599
- this.closeValueAsync(result, currentFault, (fault) => next(fault ?? (task.close ? task.fault : null)), actor, rcOwner);
3617
+ pending.push({ taskTaken: task });
3618
+ this.closeValueAsync(result, currentFault, (fault) => next(fault ?? (task.close ? task.fault : null)), actor, rcOwner, retainedBy);
2600
3619
  return;
2601
3620
  }
2602
3621
  if (task?.close) {
@@ -2624,7 +3643,7 @@ class ExecutorV1 {
2624
3643
  if (this.consumedResources.has(value))
2625
3644
  break;
2626
3645
  if (!this.closingResources.has(value)) {
2627
- this.invokeFrame(actor, resource.close, [value], () => next(), next, rcOwner);
3646
+ this.invokeFrame(actor, resource.close, [value], () => next(), next, rcOwner, false, retainedBy);
2628
3647
  return;
2629
3648
  }
2630
3649
  this.consumedResources.add(value);
@@ -2636,12 +3655,11 @@ class ExecutorV1 {
2636
3655
  pending.push(...[...value.payload].reverse());
2637
3656
  break;
2638
3657
  case "vector":
2639
- pending.push(...[...value.items].reverse());
3658
+ pending.push(...releaseVector(value).reverse());
2640
3659
  break;
2641
3660
  case "stream": {
2642
3661
  const adapter = this.generatorStreams.get(value.channelId);
2643
3662
  if (adapter) {
2644
- this.generatorStreams.delete(value.channelId);
2645
3663
  adapter.closed = true;
2646
3664
  if (adapter.pending !== null)
2647
3665
  pending.push(adapter.pending);
@@ -2687,7 +3705,7 @@ class ExecutorV1 {
2687
3705
  }
2688
3706
  closeValue(value, fault) {
2689
3707
  if (value.kind === "encoding")
2690
- value.value = null;
3708
+ releaseEncoding(value);
2691
3709
  if (value.kind === "host_resource")
2692
3710
  this.closeHostResource(value);
2693
3711
  if (value.kind === "string")
@@ -2703,11 +3721,34 @@ class ExecutorV1 {
2703
3721
  for (const field of value.payload)
2704
3722
  this.closeValue(field, fault);
2705
3723
  else if (value.kind === "vector")
2706
- for (const item of value.items)
3724
+ for (const item of releaseVector(value))
2707
3725
  this.closeValue(item, fault);
3726
+ else if (value.kind === "rc" && !value.closed) {
3727
+ value.closed = true;
3728
+ const cell = value.cell;
3729
+ if (cell && !cell.dying && --cell.refs === 0) {
3730
+ cell.dying = true;
3731
+ this.closeValue(cell.value, fault);
3732
+ cell.value = UNIT;
3733
+ cell.managedLease?.release();
3734
+ cell.managedLease = undefined;
3735
+ cell.owner?.blocks.delete(cell);
3736
+ cell.owner = null;
3737
+ }
3738
+ }
3739
+ else if (value.kind === "weak" && !value.closed) {
3740
+ value.closed = true;
3741
+ value.cell.weak--;
3742
+ }
3743
+ this.managed.retire(value);
2708
3744
  }
2709
- newTask() {
3745
+ newTask(budget, type, memberBytes = 0n, scopeAlive = false) {
2710
3746
  const task = {
3747
+ handles: new Set(),
3748
+ scopeAlive,
3749
+ budget,
3750
+ memberBytes,
3751
+ managedType: type,
2711
3752
  id: this.ids.task(),
2712
3753
  grouped: false,
2713
3754
  done: false,
@@ -2720,8 +3761,44 @@ class ExecutorV1 {
2720
3761
  changed: [],
2721
3762
  };
2722
3763
  this.tasks.set(task.id, task);
3764
+ if (budget) {
3765
+ if (type == null)
3766
+ throw new Error("task has no managed result descriptor");
3767
+ const row = this.managed.row(type);
3768
+ if (row.kind === "provider" && row.provider === "task")
3769
+ task.managedLease = budget.retain({
3770
+ value: { kind: "task", id: task.id },
3771
+ type,
3772
+ });
3773
+ }
2723
3774
  return task;
2724
3775
  }
3776
+ releaseTaskState(task) {
3777
+ if (task.scopeAlive || task.handles.size || !this.tasks.has(task.id))
3778
+ return;
3779
+ this.releaseRaceMembers(task);
3780
+ task.managedLease?.release();
3781
+ task.managedLease = undefined;
3782
+ this.tasks.delete(task.id);
3783
+ task.budget?.changed();
3784
+ }
3785
+ releaseRaceMembers(task) {
3786
+ const members = task.members;
3787
+ if (!members)
3788
+ return;
3789
+ task.members = undefined;
3790
+ task.memberBytes = 0n;
3791
+ task.budget?.changed();
3792
+ for (const value of members) {
3793
+ if (value.kind !== "task")
3794
+ continue;
3795
+ const member = this.tasks.get(value.id);
3796
+ member?.handles.delete(value);
3797
+ this.managed.retire(value);
3798
+ if (member)
3799
+ this.releaseTaskState(member);
3800
+ }
3801
+ }
2725
3802
  taskFor(value) {
2726
3803
  if (value.kind !== "task")
2727
3804
  throw new Error(`expected a task, got ${value.kind}`);
@@ -2734,21 +3811,80 @@ class ExecutorV1 {
2734
3811
  if (task.done)
2735
3812
  return;
2736
3813
  task.done = true;
3814
+ task.resultInitialized = true;
2737
3815
  task.value = value;
2738
3816
  task.fault = fault;
3817
+ task.budget?.changed();
2739
3818
  task.completedAt = ++this.completedTasks;
2740
3819
  for (const changed of [...task.changed])
2741
3820
  changed();
2742
3821
  }
2743
- invokeFrame(actor, callable, args, returned, failed, rcOwner = actor?.rcOwner ?? null, inline = false) {
3822
+ retainTaskResult(task, budget) {
3823
+ if (!budget)
3824
+ return undefined;
3825
+ if (task.fault || (task.actorError && task.actorError !== "Failed"))
3826
+ return undefined;
3827
+ const row = task.managedType == null ? undefined : this.managed.row(task.managedType);
3828
+ if (row?.kind === "actor_call")
3829
+ return budget.retain({
3830
+ value: task.managedResult,
3831
+ type: row.completion,
3832
+ });
3833
+ if (!row || row.kind !== "provider" || row.children[0] == null)
3834
+ throw new Error("task result lacks its canonical payload descriptor");
3835
+ return budget.retain({
3836
+ value: task.managedResult ?? task.value,
3837
+ type: row.children[0],
3838
+ });
3839
+ }
3840
+ managedCompletion(task, value, error, envelope) {
3841
+ if (task.managedType == null ||
3842
+ value === null ||
3843
+ (error !== null && error !== "Failed"))
3844
+ return undefined;
3845
+ const provider = this.managed.row(task.managedType);
3846
+ if (provider.kind !== "actor_call")
3847
+ throw new Error("actor completion has no canonical provider descriptor");
3848
+ const type = provider.completion;
3849
+ const result = this.managed.row(type);
3850
+ if (result.kind !== "variant")
3851
+ throw new Error("actor completion has no canonical result cases");
3852
+ const err = runtimeTag(this.pkg.variants[result.shape], "ResultErr");
3853
+ const failure = this.managed.row(result.cases[err][0]);
3854
+ if (failure.kind !== "variant")
3855
+ throw new Error("actor completion has no canonical error cases");
3856
+ if (!envelope)
3857
+ throw new Error("actor completion lost its actual handler envelope");
3858
+ return {
3859
+ value: this.completion(result.shape, failure.shape, value, error),
3860
+ reply: { value: envelope, type: provider.reply },
3861
+ };
3862
+ }
3863
+ invokeFrame(actor, callable, args, returned, failed, rcOwner = actor?.rcOwner ?? null, inline = false, retainedBy,
3864
+ // The activation this frame runs on behalf of. Its depth carries on, so a
3865
+ // recursion through runtime callbacks reaches the frame limit as a direct
3866
+ // one does.
3867
+ invoker = retainedBy) {
2744
3868
  const context = {
2745
3869
  id: actor?.id ?? "actor:root",
2746
3870
  actor,
3871
+ retainsTransferableFrame: !!retainedBy?.context.generator ||
3872
+ retainedBy?.context.retainsTransferableFrame,
2747
3873
  rcOwner,
2748
- returned,
2749
- failed: (fault) => failed(fault),
3874
+ returned: (value, released) => {
3875
+ retainedBy?.managedChildren.delete(frame);
3876
+ returned(value);
3877
+ released();
3878
+ },
3879
+ failed: (fault) => {
3880
+ retainedBy?.managedChildren.delete(frame);
3881
+ failed(fault);
3882
+ },
2750
3883
  };
2751
3884
  const frame = this.activate(this.functionAt(callable), args, null, null, undefined, null, context);
3885
+ if (invoker)
3886
+ frame.depth = invoker.depth + 1;
3887
+ retainedBy?.managedChildren.add(frame);
2752
3888
  let cancelled = null;
2753
3889
  context.cancel = (fault = { kind: "panic", message: "task cancelled", cancelled: true }) => {
2754
3890
  cancelled = fault;
@@ -2840,6 +3976,7 @@ class ExecutorV1 {
2840
3976
  mailbox: [],
2841
3977
  timers: new Set(),
2842
3978
  disposals: [],
3979
+ disposalLeases: [],
2843
3980
  admission: [],
2844
3981
  busy: true,
2845
3982
  constructing: true,
@@ -2883,6 +4020,8 @@ class ExecutorV1 {
2883
4020
  const retained = combineFaults(primary, stateFault);
2884
4021
  this.sweepRcOwner(actor.rcOwner, actor, (sweepFault) => {
2885
4022
  actor.completed = true;
4023
+ actor.stateLease?.release();
4024
+ actor.stateLease = undefined;
2886
4025
  for (const closed of actor.closed.splice(0))
2887
4026
  closed();
2888
4027
  this.raiseFault(act, combineFaults(retained, sweepFault), term.unwind);
@@ -2919,6 +4058,24 @@ class ExecutorV1 {
2919
4058
  else
2920
4059
  started();
2921
4060
  };
4061
+ if (layout.max_heap_bytes != null) {
4062
+ actor.budget = new ManagedBudget(this.managed, BigInt(layout.max_heap_bytes), (root) => this.adoptManaged(root, actor));
4063
+ if (layout.state_type == null)
4064
+ throw new Error("limited actor state lacks a managed descriptor");
4065
+ try {
4066
+ actor.stateLease = actor.budget.retain({
4067
+ value: actor.state,
4068
+ type: layout.state_type,
4069
+ slots: true,
4070
+ });
4071
+ }
4072
+ catch (error) {
4073
+ if (!(error instanceof HeapExceeded))
4074
+ throw error;
4075
+ failed({ kind: "trap", trap: "heap_exceeded" });
4076
+ return;
4077
+ }
4078
+ }
2922
4079
  if (layout.init != null)
2923
4080
  actor.active = this.invokeFrame(actor, layout.init, [actor.state, ...args.slice(supplied)], start, failed, actor.rcOwner, true);
2924
4081
  else
@@ -3026,7 +4183,9 @@ class ExecutorV1 {
3026
4183
  complete: (_value, _error, drained) => drained?.(null),
3027
4184
  };
3028
4185
  const status = this.admitActorMessage(actor, message);
3029
- if (status === "full" || status === "closed") {
4186
+ if (status === "full" ||
4187
+ status === "closed" ||
4188
+ status === "trapped") {
3030
4189
  actor.disposals.push(payload);
3031
4190
  timer.discarded();
3032
4191
  this.dispatchActor(actor);
@@ -3064,7 +4223,21 @@ class ExecutorV1 {
3064
4223
  const handler = actor.layout.handlers.find((handler) => handler.message_id === operation.message && handler.streams);
3065
4224
  if (!handler)
3066
4225
  throw new Error("stream request has no producer handler");
4226
+ let managedLease;
4227
+ try {
4228
+ managedLease = this.acquireActorMessage(actor, handler, fields);
4229
+ }
4230
+ catch (error) {
4231
+ if (!(error instanceof HeapExceeded))
4232
+ throw error;
4233
+ this.crashActor(actor, { kind: "trap", trap: "heap_exceeded" }, handler);
4234
+ this.closeValue(payload, error.message);
4235
+ this.completeShim(act, term, UNIT);
4236
+ this.scheduler.enqueue(act.context.id, () => this.runFrame(act));
4237
+ return;
4238
+ }
3067
4239
  actor.mailbox.push({
4240
+ managedLease,
3068
4241
  caller,
3069
4242
  handler,
3070
4243
  payload: fields,
@@ -3085,8 +4258,29 @@ class ExecutorV1 {
3085
4258
  return;
3086
4259
  }
3087
4260
  case "call_start": {
3088
- const task = this.newTask();
3089
- const cancelRequest = this.ask(operation.protocol, args, (value, error, closeFault) => {
4261
+ const task = this.newTask(act.context.actor?.budget, term.result && term.result !== "never"
4262
+ ? (term.result.managed_type ?? undefined)
4263
+ : undefined);
4264
+ const cancelRequest = this.ask(operation.protocol, args, (value, error, closeFault, envelope) => {
4265
+ const reply = this.managedCompletion(task, value, error, envelope);
4266
+ if (reply && !closeFault && act.context.actor?.budget) {
4267
+ try {
4268
+ act.context.actor.budget.checkPayload(this.managed.width(reply.reply.type), [reply.reply]);
4269
+ }
4270
+ catch (failure) {
4271
+ if (!(failure instanceof HeapExceeded))
4272
+ throw failure;
4273
+ task.actorError = "Trapped";
4274
+ this.settleTask(task, UNIT, {
4275
+ kind: "trap",
4276
+ trap: "heap_exceeded",
4277
+ });
4278
+ this.failActorBudget(act.context.actor);
4279
+ return false;
4280
+ }
4281
+ }
4282
+ task.managedResult = reply?.value;
4283
+ task.managedReply = reply?.reply.value;
3090
4284
  task.actorError = error;
3091
4285
  this.settleTask(task, value ?? UNIT, closeFault ?? null);
3092
4286
  return true;
@@ -3108,8 +4302,20 @@ class ExecutorV1 {
3108
4302
  throw new Error("actor completion taken before readiness");
3109
4303
  act.fault ??= task.fault;
3110
4304
  const value = task.value;
4305
+ const lease = this.retainTaskResult(task, act.context.actor?.budget);
4306
+ if (lease)
4307
+ act.managedInputs.push(lease);
3111
4308
  task.value = UNIT;
3112
- this.completeShim(act, term, this.completion(term.result_shape, term.error_shape, value, task.actorError));
4309
+ task.taken = true;
4310
+ const result = task.managedResult ??
4311
+ this.completion(term.result_shape, term.error_shape, value, task.actorError);
4312
+ task.managedResult = undefined;
4313
+ task.managedReply = undefined;
4314
+ this.completeShim(act, term, result);
4315
+ task.handles.delete(args[0]);
4316
+ this.managed.retire(args[0]);
4317
+ task.budget?.changed();
4318
+ this.releaseTaskState(task);
3113
4319
  return;
3114
4320
  }
3115
4321
  case "submit": {
@@ -3162,6 +4368,15 @@ class ExecutorV1 {
3162
4368
  complete: (_value, _error, drained) => drained?.(null),
3163
4369
  };
3164
4370
  const status = this.admitActorMessage(actor, message);
4371
+ if (status === "trapped") {
4372
+ this.closeValueAsync(payload, "actor heap limit exceeded", (fault) => {
4373
+ act.fault ??= fault;
4374
+ finish(this.variant(term.result_shape, "ResultErr", [
4375
+ this.variant(term.error_shape, "ActorErrorTrapped"),
4376
+ ]));
4377
+ }, act.context.actor, act.rcOwner);
4378
+ return;
4379
+ }
3165
4380
  if (status === "full") {
3166
4381
  if (operation.policy === "drop_newest") {
3167
4382
  let settled = false;
@@ -3209,27 +4424,32 @@ class ExecutorV1 {
3209
4424
  if (isPool && count?.kind !== "i64")
3210
4425
  throw new Error("pool construction has no count");
3211
4426
  const configured = isPool ? args.slice(0, -1) : args;
3212
- const initial = layout.backoff
3213
- ? configured[configured.length - 2]
3214
- : undefined;
3215
- const maximum = layout.backoff
3216
- ? configured[configured.length - 1]
3217
- : undefined;
3218
- const backoff = initial?.kind === "i64" && maximum?.kind === "i64"
3219
- ? { initial: initial.value, maximum: maximum.value }
3220
- : null;
3221
- const validBackoff = !layout.backoff ||
3222
- (backoff !== null &&
3223
- backoff.initial > 0n &&
3224
- backoff.maximum >= backoff.initial);
3225
- const config = layout.backoff ? configured.slice(0, -2) : configured;
4427
+ // Config, then each backoff child's `(initial, maximum)` pair in
4428
+ // declaration order.
4429
+ const pairs = layout.children.filter((child) => child.backoff).length;
4430
+ const config = configured.slice(0, configured.length - 2 * pairs);
4431
+ let pair = config.length;
4432
+ const declared = layout.children.map((child) => {
4433
+ if (!child.backoff)
4434
+ return null;
4435
+ const initial = configured[pair];
4436
+ const maximum = configured[pair + 1];
4437
+ pair += 2;
4438
+ return initial?.kind === "i64" && maximum?.kind === "i64"
4439
+ ? { initial: initial.value, maximum: maximum.value }
4440
+ : undefined;
4441
+ });
4442
+ const validBackoff = declared.every((backoff) => backoff === null ||
4443
+ (backoff !== undefined &&
4444
+ ((backoff.initial === 0n && backoff.maximum === 0n) ||
4445
+ (backoff.initial > 0n && backoff.maximum >= backoff.initial))));
3226
4446
  if (!validBackoff ||
3227
4447
  (count?.kind === "i64" &&
3228
4448
  (count.value <= 0n || count.value > 2147483647n))) {
3229
4449
  const fault = {
3230
4450
  kind: "panic",
3231
4451
  message: !validBackoff
3232
- ? "supervisor backoff requires positive initial and maximum >= initial"
4452
+ ? "supervisor backoff requires positive initial and maximum >= initial, or both zero"
3233
4453
  : "supervisor pool count must be between 1 and 2147483647",
3234
4454
  };
3235
4455
  this.running = false;
@@ -3258,7 +4478,7 @@ class ExecutorV1 {
3258
4478
  teardownChild: null,
3259
4479
  teardownCancelTimer: null,
3260
4480
  restartTimes: [],
3261
- backoff,
4481
+ backoffs: declared.flatMap((backoff) => Array.from({ length: members }, () => backoff ?? null)),
3262
4482
  restartDelays: new Map(),
3263
4483
  restartTimers: new Map(),
3264
4484
  closed: [],
@@ -3463,6 +4683,7 @@ class ExecutorV1 {
3463
4683
  let admitted = false;
3464
4684
  let closing = false;
3465
4685
  const owner = this.current.context.actor;
4686
+ const sender = this.current;
3466
4687
  const rcOwner = this.current.rcOwner;
3467
4688
  const deadline = this.requestDeadline(this.current);
3468
4689
  let cancelTimer;
@@ -3471,11 +4692,12 @@ class ExecutorV1 {
3471
4692
  ? { kind: "Actor", identity: owner.identity }
3472
4693
  : { kind: "Task" };
3473
4694
  let receiver = null;
3474
- const finish = (value, error, drained) => {
4695
+ const finish = (value, error, drained, envelope) => {
3475
4696
  const alreadySettled = settled;
3476
4697
  settled = true;
3477
4698
  cancelTimer?.();
3478
- if (alreadySettled || complete(value, error) === false) {
4699
+ if (alreadySettled ||
4700
+ complete(value, error, undefined, envelope) === false) {
3479
4701
  // The finishing receiver still owns a reply that its caller no
3480
4702
  // longer accepts. Its next turn must wait for this release.
3481
4703
  this.closeValueAsync(value ?? UNIT, null, (fault) => {
@@ -3582,9 +4804,23 @@ class ExecutorV1 {
3582
4804
  }
3583
4805
  return;
3584
4806
  }
4807
+ let managedLease;
4808
+ try {
4809
+ managedLease = this.acquireActorMessage(actor, handler, payload);
4810
+ }
4811
+ catch (error) {
4812
+ if (!(error instanceof HeapExceeded))
4813
+ throw error;
4814
+ this.failActorBudget(actor);
4815
+ reject("Trapped");
4816
+ return;
4817
+ }
3585
4818
  admitted = true;
4819
+ for (const lease of sender.managedInputs.splice(0))
4820
+ lease.release();
3586
4821
  receiver = actor;
3587
4822
  message = {
4823
+ managedLease,
3588
4824
  caller,
3589
4825
  handler,
3590
4826
  payload,
@@ -3607,6 +4843,8 @@ class ExecutorV1 {
3607
4843
  if (index < 0)
3608
4844
  return;
3609
4845
  receiver.mailbox.splice(index, 1);
4846
+ message.managedLease?.release();
4847
+ message.managedLease = undefined;
3610
4848
  admitted = false;
3611
4849
  for (const admit of receiver.admission.splice(0))
3612
4850
  admit();
@@ -3634,13 +4872,56 @@ class ExecutorV1 {
3634
4872
  closeRequest(done);
3635
4873
  };
3636
4874
  }
4875
+ acquireActorMessage(actor, handler, payload) {
4876
+ const params = this.functionAt(handler.callable).params.slice(1);
4877
+ if (params.length !== payload.length)
4878
+ throw new Error("typed message payload differs from its handler");
4879
+ if (!actor.budget &&
4880
+ !this.pipes.hasManagedReceivers &&
4881
+ params.every((param) => param.managed_type == null ||
4882
+ !this.managed.needsAdoption(param.managed_type)))
4883
+ return undefined;
4884
+ const roots = params.flatMap((param, index) => {
4885
+ if (param.managed_type == null) {
4886
+ if (!actor.budget)
4887
+ return [];
4888
+ throw new Error("message parameter lacks its managed descriptor");
4889
+ }
4890
+ return [
4891
+ {
4892
+ value: payload[index],
4893
+ type: param.managed_type,
4894
+ slots: true,
4895
+ },
4896
+ ];
4897
+ });
4898
+ if (actor.budget)
4899
+ return actor.budget.retainMany(roots);
4900
+ for (const root of roots)
4901
+ this.adoptManaged(root, actor);
4902
+ return undefined;
4903
+ }
3637
4904
  admitActorMessage(actor, message) {
3638
4905
  if (!actor.alive || actor.closing)
3639
4906
  return "closed";
3640
4907
  const full = actor.layout.mailbox_capacity != null &&
3641
4908
  actor.mailbox.length >= actor.layout.mailbox_capacity;
3642
4909
  let declared = full ? actor.layout.overflow : null;
4910
+ const acquire = () => {
4911
+ try {
4912
+ message.managedLease = this.acquireActorMessage(actor, message.handler, message.payload);
4913
+ }
4914
+ catch (error) {
4915
+ if (!(error instanceof HeapExceeded))
4916
+ throw error;
4917
+ this.failActorBudget(actor);
4918
+ return false;
4919
+ }
4920
+ return true;
4921
+ };
3643
4922
  const discard = (lost) => {
4923
+ if (lost.managedLease)
4924
+ actor.disposalLeases.push(lost.managedLease);
3644
4925
  actor.disposals.push({
3645
4926
  kind: "record",
3646
4927
  typeId: "",
@@ -3658,6 +4939,8 @@ class ExecutorV1 {
3658
4939
  equals(queued.payload[key.param], message.payload[key.param]))
3659
4940
  : -1;
3660
4941
  if (queued >= 0) {
4942
+ if (!acquire())
4943
+ return "trapped";
3661
4944
  const superseded = actor.mailbox[queued];
3662
4945
  actor.mailbox[queued] = message;
3663
4946
  discard(superseded);
@@ -3673,6 +4956,8 @@ class ExecutorV1 {
3673
4956
  ? actor.mailbox.findIndex((queued) => !queued.obligated)
3674
4957
  : -1;
3675
4958
  if (oldest >= 0) {
4959
+ if (!acquire())
4960
+ return "trapped";
3676
4961
  const [evicted] = actor.mailbox.splice(oldest, 1);
3677
4962
  actor.mailbox.push(message);
3678
4963
  discard(evicted);
@@ -3680,6 +4965,8 @@ class ExecutorV1 {
3680
4965
  }
3681
4966
  if (full)
3682
4967
  return "full";
4968
+ if (!acquire())
4969
+ return "trapped";
3683
4970
  actor.mailbox.push(message);
3684
4971
  this.trace.snapshot("actor.send", {
3685
4972
  actor_id: actor.id,
@@ -3772,7 +5059,9 @@ class ExecutorV1 {
3772
5059
  if (actor.disposals.length > 0) {
3773
5060
  actor.busy = true;
3774
5061
  const values = actor.disposals.splice(0);
5062
+ const leases = actor.disposalLeases.splice(0);
3775
5063
  this.closeValueAsync({ kind: "vector", elementType: "message", items: values }, this.faultText(actor.crashing), (fault) => {
5064
+ leases.forEach((lease) => lease.release());
3776
5065
  actor.busy = false;
3777
5066
  if (fault)
3778
5067
  this.crashActor(actor, fault);
@@ -3816,6 +5105,8 @@ class ExecutorV1 {
3816
5105
  return;
3817
5106
  }
3818
5107
  actor.mailbox.shift();
5108
+ message.managedLease?.release();
5109
+ message.managedLease = undefined;
3819
5110
  for (const admit of actor.admission.splice(0))
3820
5111
  admit();
3821
5112
  if (message.requestDeadline !== undefined &&
@@ -3841,10 +5132,21 @@ class ExecutorV1 {
3841
5132
  caller: message.caller,
3842
5133
  requestDeadline: message.requestDeadline,
3843
5134
  reply: message.reply,
3844
- returned: (value) => {
5135
+ returned: (value, released) => {
3845
5136
  unwatchStream();
3846
5137
  actor.active = null;
5138
+ if (actor.crashing?.kind === "trap" &&
5139
+ actor.crashing.trap === "heap_exceeded") {
5140
+ const fault = actor.crashing;
5141
+ this.closeValueAsync(value, this.faultText(fault), (closeFault) => {
5142
+ released();
5143
+ this.crashActor(actor, combineFaults(fault, closeFault), message.handler);
5144
+ message.complete(null, "Trapped");
5145
+ }, actor, actor.rcOwner);
5146
+ return;
5147
+ }
3847
5148
  const drained = (fault) => {
5149
+ released();
3848
5150
  actor.busy = false;
3849
5151
  if (fault)
3850
5152
  this.crashActor(actor, fault, message.handler);
@@ -3859,7 +5161,7 @@ class ExecutorV1 {
3859
5161
  const shape = this.pkg.variants[message.handler.result_shape];
3860
5162
  if (shape?.cases[value.tag]?.name === "Err") {
3861
5163
  if (message.reply)
3862
- message.complete(value.payload[0] ?? UNIT, "Failed", drained);
5164
+ message.complete(value.payload[0] ?? UNIT, "Failed", drained, value);
3863
5165
  else {
3864
5166
  this.crashActor(actor, {
3865
5167
  kind: "panic",
@@ -3870,11 +5172,11 @@ class ExecutorV1 {
3870
5172
  }
3871
5173
  }
3872
5174
  else {
3873
- message.complete(value.payload[0] ?? UNIT, null, drained);
5175
+ message.complete(value.payload[0] ?? UNIT, null, drained, value);
3874
5176
  }
3875
5177
  }
3876
5178
  else {
3877
- message.complete(value, null, drained);
5179
+ message.complete(value, null, drained, value);
3878
5180
  }
3879
5181
  },
3880
5182
  failed: (fault) => {
@@ -3950,6 +5252,8 @@ class ExecutorV1 {
3950
5252
  this.reportFault(actor.closingFault);
3951
5253
  }
3952
5254
  actor.completed = true;
5255
+ actor.stateLease?.release();
5256
+ actor.stateLease = undefined;
3953
5257
  for (const wake of actor.closed.splice(0))
3954
5258
  wake();
3955
5259
  for (const admit of actor.admission.splice(0))
@@ -3983,11 +5287,15 @@ class ExecutorV1 {
3983
5287
  kind: "enum",
3984
5288
  typeId: this.variantName(actor.layout.crash_kind),
3985
5289
  shape: actor.layout.crash_kind,
3986
- tag: runtimeTag(this.pkg.variants[actor.layout.crash_kind], code === 214
3987
- ? "CrashKindTimerOverdue"
3988
- : code === 213
3989
- ? "CrashKindHandlerFailed"
3990
- : "CrashKindFault"),
5290
+ tag: runtimeTag(this.pkg.variants[actor.layout.crash_kind], code === 200
5291
+ ? "CrashKindHeapExceeded"
5292
+ : code === 214
5293
+ ? "CrashKindTimerOverdue"
5294
+ : code === 213
5295
+ ? "CrashKindHandlerFailed"
5296
+ : code === 215
5297
+ ? "CrashKindStackOverflow"
5298
+ : "CrashKindFault"),
3991
5299
  payload: [],
3992
5300
  },
3993
5301
  stringValue(String(message)),
@@ -4169,24 +5477,38 @@ class ExecutorV1 {
4169
5477
  return;
4170
5478
  }
4171
5479
  if (owner.restartBusy) {
4172
- owner.pendingRestarts.push({ child, debts });
5480
+ owner.pendingRestarts.push({
5481
+ child,
5482
+ debts,
5483
+ incarnation: owner.children[child] ?? null,
5484
+ });
4173
5485
  return;
4174
5486
  }
4175
5487
  const now = this.trace.virtualTimeMs;
4176
- if (owner.backoff && debts.length > 0 && !delayElapsed) {
5488
+ const backoff = owner.backoffs[child];
5489
+ if (backoff && backoff.initial > 0n && debts.length > 0 && !delayElapsed) {
4177
5490
  const previous = owner.restartDelays.get(child);
4178
5491
  const healthy = previous?.publishedAt != null &&
4179
5492
  now - previous.publishedAt >= owner.layout.window_secs * 1000;
4180
- const doubled = previous ? previous.delay * 2n : owner.backoff.initial;
5493
+ const doubled = previous ? previous.delay * 2n : backoff.initial;
4181
5494
  const delay = !previous || healthy
4182
- ? owner.backoff.initial
4183
- : doubled < owner.backoff.maximum
5495
+ ? backoff.initial
5496
+ : doubled < backoff.maximum
4184
5497
  ? doubled
4185
- : owner.backoff.maximum;
5498
+ : backoff.maximum;
4186
5499
  owner.restartDelays.set(child, { delay, publishedAt: null });
4187
5500
  owner.recovering.add(child);
4188
5501
  owner.restartTimers.set(child, this.scheduler.after(delay, () => {
4189
5502
  owner.restartTimers.delete(child);
5503
+ // A group restart that already replaced this role recovered its
5504
+ // fault; restarting again would tear down a healthy group and
5505
+ // charge the budget twice.
5506
+ if (owner.restartDelays.get(child)?.publishedAt != null) {
5507
+ for (const debt of debts)
5508
+ this.faultDebts.delete(debt);
5509
+ this.wakeRoles();
5510
+ return;
5511
+ }
4190
5512
  this.restartChild(owner, child, debts, true);
4191
5513
  }));
4192
5514
  return;
@@ -4233,11 +5555,16 @@ class ExecutorV1 {
4233
5555
  if (index === undefined) {
4234
5556
  for (const debt of debts)
4235
5557
  this.faultDebts.delete(debt);
4236
- for (const pending of owner.pendingRestarts.filter((pending) => affected.includes(pending.child))) {
5558
+ // This group replaced a queued crash's incarnation, which recovers it.
5559
+ // A replacement that crashed after its own publication still needs
5560
+ // its restart.
5561
+ const replaced = (pending) => affected.includes(pending.child) &&
5562
+ owner.children[pending.child] !== pending.incarnation;
5563
+ for (const pending of owner.pendingRestarts.filter(replaced)) {
4237
5564
  for (const debt of pending.debts)
4238
5565
  this.faultDebts.delete(debt);
4239
5566
  }
4240
- owner.pendingRestarts = owner.pendingRestarts.filter((pending) => !affected.includes(pending.child));
5567
+ owner.pendingRestarts = owner.pendingRestarts.filter((pending) => !replaced(pending));
4241
5568
  owner.restartBusy = false;
4242
5569
  this.wakeRoles();
4243
5570
  const pending = owner.pendingRestarts.shift();
@@ -4249,6 +5576,12 @@ class ExecutorV1 {
4249
5576
  const delay = owner.restartDelays.get(index);
4250
5577
  if (delay)
4251
5578
  delay.publishedAt = this.trace.virtualTimeMs;
5579
+ // The crashed role's replacement is the recovery that settles its
5580
+ // fault, as natively; a later sibling's cancelled start does not
5581
+ // reopen it.
5582
+ if (index === child)
5583
+ for (const debt of debts)
5584
+ this.faultDebts.delete(debt);
4252
5585
  start();
4253
5586
  }, () => {
4254
5587
  owner.restartBusy = false;
@@ -4372,6 +5705,10 @@ class ExecutorV1 {
4372
5705
  }
4373
5706
  drainMailbox(actor, fault, done) {
4374
5707
  const messages = actor.mailbox.splice(0);
5708
+ const leases = [
5709
+ ...actor.disposalLeases.splice(0),
5710
+ ...messages.flatMap((message) => message.managedLease ? [message.managedLease] : []),
5711
+ ];
4375
5712
  actor.drainingMailbox = true;
4376
5713
  this.closeValueAsync({
4377
5714
  kind: "vector",
@@ -4385,6 +5722,7 @@ class ExecutorV1 {
4385
5722
  })),
4386
5723
  ],
4387
5724
  }, fault, (closeFault) => {
5725
+ leases.forEach((lease) => lease.release());
4388
5726
  actor.drainingMailbox = false;
4389
5727
  for (const message of messages)
4390
5728
  message.complete(null, "Dead");
@@ -4464,6 +5802,8 @@ class ExecutorV1 {
4464
5802
  }
4465
5803
  actor.completed = true;
4466
5804
  actor.alive = false;
5805
+ actor.stateLease?.release();
5806
+ actor.stateLease = undefined;
4467
5807
  actor.busy = false;
4468
5808
  this.trace.snapshot(actor.terminating ? "actor.terminate" : "actor.stop", { actor_id: actor.id });
4469
5809
  this.settleSupervisedEnd(actor, actor.closingFault
@@ -4570,14 +5910,19 @@ function faultInfo(fault) {
4570
5910
  return [-2, "Deadline"];
4571
5911
  return [212, "UserPanic"];
4572
5912
  }
4573
- return {
4574
- integer_overflow: [201, "IntegerOverflow"],
4575
- divide_by_zero: [202, "DivideByZero"],
4576
- shift_out_of_range: [204, "ShiftOutOfRange"],
4577
- vector_bounds: [205, "IndexOutOfBounds"],
4578
- actor_refused: [-4, "Refused"],
4579
- }[fault.trap];
5913
+ return TRAP_FAULTS[fault.trap];
4580
5914
  }
5915
+ /// The native exit reason each package trap reports, as code and name.
5916
+ const TRAP_FAULTS = {
5917
+ integer_overflow: [201, "IntegerOverflow"],
5918
+ divide_by_zero: [202, "DivideByZero"],
5919
+ signed_min_div_neg_one: [203, "SignedMinDivNegOne"],
5920
+ shift_out_of_range: [204, "ShiftOutOfRange"],
5921
+ vector_bounds: [205, "IndexOutOfBounds"],
5922
+ actor_refused: [-4, "Refused"],
5923
+ heap_exceeded: [200, "HeapExceeded"],
5924
+ stack_overflow: [215, "StackOverflow"],
5925
+ };
4581
5926
  function faultApplicationMessage(fault) {
4582
5927
  return fault.kind === "panic" &&
4583
5928
  (fault.cancelled || fault.deadline || fault.timerOverdue)
@@ -4606,12 +5951,30 @@ function faultReport(fault, secondary = false) {
4606
5951
  (fault.secondary ?? []).map((next) => faultReport(next, true)).join(""));
4607
5952
  }
4608
5953
  // ── refs ────────────────────────────────────────────────────────────────────
4609
- function ownedRef(value) {
4610
- return { kind: "cell", cell: { value, valid: true } };
5954
+ function ownedRef(value, budget, type) {
5955
+ if (budget && type == null)
5956
+ throw new Error("owned value has no managed descriptor");
5957
+ return {
5958
+ kind: "cell",
5959
+ cell: {
5960
+ value,
5961
+ valid: true,
5962
+ budget,
5963
+ type,
5964
+ lease: budget?.retain({ value, type: type }),
5965
+ },
5966
+ };
4611
5967
  }
4612
5968
  function writeRef(ref, value) {
4613
5969
  switch (ref.kind) {
4614
5970
  case "cell":
5971
+ if (ref.cell.budget) {
5972
+ if (ref.cell.type == null)
5973
+ throw new Error("owned place has no managed descriptor");
5974
+ const lease = ref.cell.budget.retain({ value, type: ref.cell.type });
5975
+ ref.cell.lease?.release();
5976
+ ref.cell.lease = lease;
5977
+ }
4615
5978
  ref.cell.value = value;
4616
5979
  ref.cell.valid = true;
4617
5980
  return;
@@ -4626,8 +5989,11 @@ function writeRef(ref, value) {
4626
5989
  /// Only a cell of its own can be ended; a projection lives as long as the
4627
5990
  /// storage it reaches through.
4628
5991
  function invalidateRef(ref) {
4629
- if (ref.kind === "cell")
5992
+ if (ref.kind === "cell") {
4630
5993
  ref.cell.valid = false;
5994
+ ref.cell.lease?.release();
5995
+ ref.cell.lease = undefined;
5996
+ }
4631
5997
  else
4632
5998
  writeRef(ref, UNIT);
4633
5999
  }
@@ -4734,11 +6100,7 @@ function truth(value) {
4734
6100
  : value.kind === "i64" && value.value !== 0n;
4735
6101
  }
4736
6102
  function bytesValue(bytes) {
4737
- return {
4738
- kind: "vector",
4739
- elementType: "u8",
4740
- items: bytes.map((byte) => ({ kind: "i64", value: BigInt(byte) })),
4741
- };
6103
+ return byteVectorValue(bytes.map((byte) => ({ kind: "i64", value: BigInt(byte) })));
4742
6104
  }
4743
6105
  function renderMessage(value) {
4744
6106
  return value.kind === "string" ? value.value : canonical(value);
@@ -4758,7 +6120,7 @@ function toComparable(value) {
4758
6120
  case "enum":
4759
6121
  return [value.tag, value.payload.map(toComparable)];
4760
6122
  case "vector":
4761
- return value.items.map(toComparable);
6123
+ return vectorItems(value).map(toComparable);
4762
6124
  case "rc":
4763
6125
  return "Rc";
4764
6126
  case "weak":
@@ -4798,12 +6160,18 @@ function trapMessage(trap) {
4798
6160
  return "integer overflow";
4799
6161
  case "divide_by_zero":
4800
6162
  return "divide by zero";
6163
+ case "signed_min_div_neg_one":
6164
+ return "signed minimum divided by -1";
4801
6165
  case "shift_out_of_range":
4802
6166
  return "shift amount out of range";
4803
6167
  case "vector_bounds":
4804
6168
  return "index out of bounds";
4805
6169
  case "actor_refused":
4806
6170
  return "delivery refused by interceptor";
6171
+ case "heap_exceeded":
6172
+ return "actor heap limit exceeded";
6173
+ case "stack_overflow":
6174
+ return "stack overflow";
4807
6175
  }
4808
6176
  }
4809
6177
  /// The operand type the package records. `isize` and `usize` are 64-bit in the
@@ -4853,8 +6221,16 @@ function checkedFailure(op, lhs, rhs, ty) {
4853
6221
  return null;
4854
6222
  }
4855
6223
  const scalar = intTy(ty);
4856
- if ((op === "Divide" || op === "Modulo") && rhs.value === 0n) {
4857
- return "divide_by_zero";
6224
+ if (op === "Divide" || op === "Modulo") {
6225
+ if (rhs.value === 0n)
6226
+ return "divide_by_zero";
6227
+ // Both the quotient and the remainder of the signed minimum by -1 trap,
6228
+ // although the remainder alone would be representable.
6229
+ if (scalar.signed &&
6230
+ rhs.value === -1n &&
6231
+ lhs.value === -(1n << (scalar.bits - 1n))) {
6232
+ return "signed_min_div_neg_one";
6233
+ }
4858
6234
  }
4859
6235
  if ((op === "Shl" || op === "Shr") &&
4860
6236
  (rhs.value < 0n || rhs.value >= scalar.bits)) {
@@ -5038,8 +6414,8 @@ function equals(lhs, rhs) {
5038
6414
  lhs.payload.every((field, at) => equals(field, rhs.payload[at])));
5039
6415
  }
5040
6416
  if (lhs.kind === "vector" && rhs.kind === "vector") {
5041
- return (lhs.items.length === rhs.items.length &&
5042
- lhs.items.every((item, at) => equals(item, rhs.items[at])));
6417
+ return (vectorLength(lhs) === vectorLength(rhs) &&
6418
+ vectorItems(lhs).every((item, at) => equals(item, vectorElement(rhs, at))));
5043
6419
  }
5044
6420
  return canonical(lhs) === canonical(rhs);
5045
6421
  }