libpetri 5.1.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,22 +1,32 @@
1
1
  import {
2
- SmtVerifier,
2
+ ComposeBindings,
3
+ FusionSet,
4
+ FusionSetBuilder,
5
+ Instance,
6
+ Interface,
7
+ InterfaceBuilder,
8
+ PetriNet,
9
+ PetriNetBuilder,
10
+ SubnetDef,
11
+ SubnetDefBuilder,
12
+ Transition,
13
+ TransitionBuilder,
3
14
  all,
4
15
  allPlaces,
5
- alwaysAvailable,
6
16
  and,
7
17
  andPlaces,
8
18
  atLeast,
9
19
  consumptionCount,
10
20
  enumerateBranches,
21
+ environmentPlace,
11
22
  exactly,
12
23
  fork,
13
24
  forwardInput,
14
25
  isPassthrough,
15
- isProven,
16
- isViolated,
17
26
  one,
18
27
  outPlace,
19
28
  passthrough,
29
+ place,
20
30
  produce,
21
31
  requireOutputProducingActions,
22
32
  requiredCount,
@@ -28,11 +38,11 @@ import {
28
38
  withTimeout,
29
39
  xor,
30
40
  xorPlaces
31
- } from "./chunk-EL4E6LVO.js";
41
+ } from "./chunk-MQZ6IM63.js";
32
42
  import {
33
43
  eventTransitionName,
34
44
  isFailureEvent
35
- } from "./chunk-SXK2Z45Z.js";
45
+ } from "./chunk-H2KAMPGN.js";
36
46
  import {
37
47
  keyForPlace,
38
48
  matchCorrelates,
@@ -69,14 +79,6 @@ function isUnit(token) {
69
79
  return token === UNIT_TOKEN;
70
80
  }
71
81
 
72
- // src/core/place.ts
73
- function place(name) {
74
- return { name };
75
- }
76
- function environmentPlace(name) {
77
- return { place: place(name) };
78
- }
79
-
80
82
  // src/core/arc.ts
81
83
  function inputArc(place2) {
82
84
  return { type: "input", place: place2 };
@@ -221,2160 +223,225 @@ var TransitionContext = class {
221
223
  `Place '${actual.name}' consumed ${values.length} tokens, use inputs() for batched access`
222
224
  );
223
225
  }
224
- return values[0];
225
- }
226
- /** Get all consumed input values for a place. */
227
- inputs(place2) {
228
- const actual = this.resolve(place2);
229
- this.requireInput(actual);
230
- return this.rawInput.values(actual);
231
- }
232
- /** Get consumed input token with metadata. */
233
- inputToken(place2) {
234
- const actual = this.resolve(place2);
235
- this.requireInput(actual);
236
- return this.rawInput.get(actual);
237
- }
238
- /** Returns declared input places (consumed). */
239
- inputPlaces() {
240
- return this._inputPlaces;
241
- }
242
- requireInput(place2) {
243
- if (!this.allowedInputs.has(place2.name)) {
244
- throw new Error(
245
- `Place '${place2.name}' not in declared inputs: [${[...this.allowedInputs].join(", ")}]`
246
- );
247
- }
248
- }
249
- // ==================== Read Access (not consumed) ====================
250
- /** Get read-only context value. Throws if place not declared as read. */
251
- read(place2) {
252
- const actual = this.resolve(place2);
253
- this.requireRead(actual);
254
- return this.rawInput.value(actual);
255
- }
256
- /** Get all read-only context values for a place. */
257
- reads(place2) {
258
- const actual = this.resolve(place2);
259
- this.requireRead(actual);
260
- return this.rawInput.values(actual);
261
- }
262
- /** Returns declared read places (context, not consumed). */
263
- readPlaces() {
264
- return this._readPlaces;
265
- }
266
- requireRead(place2) {
267
- if (!this.allowedReads.has(place2.name)) {
268
- throw new Error(
269
- `Place '${place2.name}' not in declared reads: [${[...this.allowedReads].join(", ")}]`
270
- );
271
- }
272
- }
273
- // ==================== Output Access ====================
274
- /**
275
- * Add one or more output values to the same place in a single call.
276
- *
277
- * Validates the place once, then appends each value to the output
278
- * collector. Calling with zero values is a no-op.
279
- *
280
- * @example
281
- * ctx.output(outPlace, 'a', 'b', 'c');
282
- * ctx.output(outPlace, ...someArray);
283
- *
284
- * @throws if place not declared as output.
285
- */
286
- output(place2, ...values) {
287
- const actual = this.resolve(place2);
288
- this.requireOutput(actual);
289
- for (const value of values) {
290
- this.writeTarget.add(actual, value);
291
- }
292
- return this;
293
- }
294
- /**
295
- * Add one or more pre-built output tokens to the same place in a single call.
296
- *
297
- * Validates the place once, then appends each token. Calling with zero
298
- * tokens is a no-op.
299
- *
300
- * @throws if place not declared as output.
301
- */
302
- outputToken(place2, ...tokens) {
303
- const actual = this.resolve(place2);
304
- this.requireOutput(actual);
305
- for (const token of tokens) {
306
- this.writeTarget.addToken(actual, token);
307
- }
308
- return this;
309
- }
310
- /** Returns declared output places. */
311
- outputPlaces() {
312
- return this._outputPlaces;
313
- }
314
- requireOutput(place2) {
315
- if (!this.allowedOutputs.has(place2.name)) {
316
- throw new Error(
317
- `Place '${place2.name}' not in declared outputs: [${[...this.allowedOutputs].join(", ")}]`
318
- );
319
- }
320
- }
321
- /**
322
- * Severs the action's output from the marking, for use when a firing times out.
323
- *
324
- * After this call the action's `output(...)` writes are dropped, and the executor
325
- * harvests a fresh collector the action holds no reference to. Anything the action
326
- * wrote *before* the timeout is discarded with it: a partial result merged with the
327
- * timeout branch would violate the transition's own output spec.
328
- *
329
- * This is the only isolation available — libpetri does not own the promise the action
330
- * runs on, so it cannot stop the work, only stop the result from landing.
331
- *
332
- * @internal Executor machinery — must be called by the executor, never by an action.
333
- */
334
- detachForTimeout() {
335
- this.writeTarget.detach();
336
- this._rawOutput = new TokenOutput();
337
- }
338
- /**
339
- * Produces into the executor's harvest collector rather than the action's write target.
340
- *
341
- * Identical to {@link output} before {@link detachForTimeout}; after it, this is the
342
- * only route that still reaches the marking. Used by the executor to deposit the
343
- * timeout branch.
344
- *
345
- * @internal Executor machinery — must be called by the executor, never by an action.
346
- */
347
- outputToHarvest(place2, value) {
348
- const actual = this.resolve(place2);
349
- this.requireOutput(actual);
350
- this._rawOutput.add(actual, value);
351
- return this;
352
- }
353
- // ==================== ν-name minting (NU-010) ====================
354
- /**
355
- * @internal Installs the ν-name minter. Wired by the executor at firing time
356
- * so names minted by {@link freshName} are monotonic across the run and
357
- * instance-prefixed (spec NU-010, NU-030).
358
- */
359
- setFreshNameSupplier(supplier) {
360
- this._freshNameSupplier = supplier;
361
- }
362
- /**
363
- * Mints a fresh ν-name (the ν-binder primitive — spec NU-010).
364
- *
365
- * An action calls this on the fork side to create a correlation id, then
366
- * writes it into the sibling output payloads; a later join correlates those
367
- * siblings via a {@link import('./match-spec.js').MatchSpec}. Uses the
368
- * executor-installed minter when present; otherwise falls back to a
369
- * process-global counter prefixed by the transition name.
370
- */
371
- freshName() {
372
- if (this._freshNameSupplier) return this._freshNameSupplier();
373
- return nameId(`${this._transitionName}#${GLOBAL_FRESH_NAME_COUNTER++}`);
374
- }
375
- // ==================== Structure Info ====================
376
- /** Returns the transition name. */
377
- transitionName() {
378
- return this._transitionName;
379
- }
380
- // ==================== Execution Context ====================
381
- /** Retrieves an execution context object by key. */
382
- executionContext(key) {
383
- return this.executionCtx.get(key);
384
- }
385
- /** Checks if an execution context object of the given key is present. */
386
- hasExecutionContext(key) {
387
- return this.executionCtx.has(key);
388
- }
389
- // ==================== Logging ====================
390
- /** Emits a structured log message into the event store. */
391
- log(level, message, error) {
392
- this._logFn?.(level, message, error);
393
- }
394
- // ==================== Internal ====================
395
- /** @internal Used by BitmapNetExecutor to collect outputs after action completion. */
396
- rawOutput() {
397
- return this._rawOutput;
398
- }
399
- };
400
-
401
- // src/core/token-input.ts
402
- var TokenInput = class {
403
- tokens = /* @__PURE__ */ new Map();
404
- /** Add a token (used by executor when firing transition). */
405
- add(place2, token) {
406
- const existing = this.tokens.get(place2.name);
407
- if (existing) {
408
- existing.push(token);
409
- } else {
410
- this.tokens.set(place2.name, [token]);
411
- }
412
- return this;
413
- }
414
- /** Get all tokens for a place. */
415
- getAll(place2) {
416
- return this.tokens.get(place2.name) ?? [];
417
- }
418
- /** Get the first token for a place. Throws if no tokens. */
419
- get(place2) {
420
- const list = this.tokens.get(place2.name);
421
- if (!list || list.length === 0) {
422
- throw new Error(`No token for place: ${place2.name}`);
423
- }
424
- return list[0];
425
- }
426
- /** Get the first token's value for a place. Throws if no tokens. */
427
- value(place2) {
428
- return this.get(place2).value;
429
- }
430
- /** Get all token values for a place. */
431
- values(place2) {
432
- return this.getAll(place2).map((t) => t.value);
433
- }
434
- /** Get token count for a place. */
435
- count(place2) {
436
- return this.getAll(place2).length;
437
- }
438
- /** Check if any tokens exist for a place. */
439
- has(place2) {
440
- return this.count(place2) > 0;
441
- }
442
- };
443
-
444
- // src/core/transition.ts
445
- var TRANSITION_KEY = /* @__PURE__ */ Symbol("Transition.internal");
446
- var EMPTY_PLACE_ALIAS = /* @__PURE__ */ new Map();
447
- var Transition = class {
448
- name;
449
- inputSpecs;
450
- outputSpec;
451
- inhibitors;
452
- reads;
453
- resets;
454
- timing;
455
- actionTimeout;
456
- action;
457
- priority;
458
- /**
459
- * ν-net join correlation: a subset of `inputSpecs` that must be correlated by
460
- * name equality on firing (spec NU-020). `null` for ordinary transitions.
461
- */
462
- matchSpec;
463
- /**
464
- * Per-transition **declared → actual** place correspondence (per
465
- * **MOD-031**), keyed by the author-original declared place **name** →
466
- * actual composed place. Empty for a hand-written or directly-composed
467
- * ([MOD-025]) transition (identity). Populated by the subnet rewriter after
468
- * instantiation ([MOD-010]) / port binding ([MOD-020]) so an action that
469
- * hardcodes a declared place constant resolves to the composed place via
470
- * {@link import('./transition-context.js').TransitionContext}. Consumed only
471
- * by the action-facing context I/O — never by enablement, firing, the
472
- * verifier, the exporter, or events (so [MOD-023] is unaffected).
473
- */
474
- placeAlias;
475
- _inputPlaces;
476
- _readPlaces;
477
- _outputPlaces;
478
- /** @internal Use {@link Transition.builder} to create instances. */
479
- constructor(key, name, inputSpecs, outputSpec, inhibitors, reads, resets, timing, action, priority, placeAlias = EMPTY_PLACE_ALIAS, matchSpec2 = null) {
480
- if (key !== TRANSITION_KEY) throw new Error("Use Transition.builder() to create instances");
481
- this.name = name;
482
- this.inputSpecs = inputSpecs;
483
- this.outputSpec = outputSpec;
484
- this.inhibitors = inhibitors;
485
- this.reads = reads;
486
- this.resets = resets;
487
- this.timing = timing;
488
- this.actionTimeout = findTimeout(outputSpec);
489
- this.action = action;
490
- this.priority = priority;
491
- this.placeAlias = placeAlias.size === 0 ? EMPTY_PLACE_ALIAS : placeAlias;
492
- this.matchSpec = matchSpec2;
493
- const inputPlaces = /* @__PURE__ */ new Set();
494
- for (const spec of inputSpecs) {
495
- inputPlaces.add(spec.place);
496
- }
497
- this._inputPlaces = inputPlaces;
498
- const readPlaces = /* @__PURE__ */ new Set();
499
- for (const r of reads) {
500
- readPlaces.add(r.place);
501
- }
502
- this._readPlaces = readPlaces;
503
- const outputPlaces = /* @__PURE__ */ new Set();
504
- if (outputSpec !== null) {
505
- for (const p of allPlaces(outputSpec)) {
506
- outputPlaces.add(p);
507
- }
508
- }
509
- this._outputPlaces = outputPlaces;
510
- }
511
- /** Returns set of input places — consumed tokens. */
512
- inputPlaces() {
513
- return this._inputPlaces;
514
- }
515
- /** Returns set of read places — context tokens, not consumed. */
516
- readPlaces() {
517
- return this._readPlaces;
518
- }
519
- /** Returns set of output places — where tokens are produced. */
520
- outputPlaces() {
521
- return this._outputPlaces;
522
- }
523
- /** Returns true if this transition has an action timeout. */
524
- hasActionTimeout() {
525
- return this.actionTimeout !== null;
526
- }
527
- toString() {
528
- return `Transition[${this.name}]`;
529
- }
530
- static builder(name) {
531
- return new TransitionBuilder(name);
532
- }
533
- };
534
- var TransitionBuilder = class {
535
- _name;
536
- _inputSpecs = [];
537
- _outputSpec = null;
538
- _inhibitors = [];
539
- _reads = [];
540
- _resets = [];
541
- _timing = immediate();
542
- _action = passthrough();
543
- _priority = 0;
544
- _placeAlias = EMPTY_PLACE_ALIAS;
545
- _matchSpec = null;
546
- constructor(name) {
547
- this._name = name;
548
- }
549
- /** Add input specifications with cardinality. */
550
- inputs(...specs) {
551
- this._inputSpecs.push(...specs);
552
- return this;
553
- }
554
- /** Set the output specification (composite AND/XOR structure). */
555
- outputs(spec) {
556
- this._outputSpec = spec;
557
- return this;
558
- }
559
- /** Add inhibitor arc. */
560
- inhibitor(place2) {
561
- this._inhibitors.push({ type: "inhibitor", place: place2 });
562
- return this;
563
- }
564
- /** Add inhibitor arcs. */
565
- inhibitors(...places) {
566
- for (const p of places) {
567
- this._inhibitors.push({ type: "inhibitor", place: p });
568
- }
569
- return this;
570
- }
571
- /** Add read arc. */
572
- read(place2) {
573
- this._reads.push({ type: "read", place: place2 });
574
- return this;
575
- }
576
- /** Add read arcs. */
577
- reads(...places) {
578
- for (const p of places) {
579
- this._reads.push({ type: "read", place: p });
580
- }
581
- return this;
582
- }
583
- /** Add reset arc. */
584
- reset(place2) {
585
- this._resets.push({ type: "reset", place: place2 });
586
- return this;
587
- }
588
- /** Add reset arcs. */
589
- resets(...places) {
590
- for (const p of places) {
591
- this._resets.push({ type: "reset", place: p });
592
- }
593
- return this;
594
- }
595
- /** Set timing specification. */
596
- timing(timing) {
597
- this._timing = timing;
598
- return this;
599
- }
600
- /** Set the transition action. */
601
- action(action) {
602
- this._action = action;
603
- return this;
604
- }
605
- /** Set the priority (higher fires first). */
606
- priority(priority) {
607
- this._priority = priority;
608
- return this;
609
- }
610
- /**
611
- * Sets the ν-net join correlation spec: the named input places must be
612
- * correlated by name equality on firing (spec NU-020). Every place referenced
613
- * by the spec must also be declared as an input.
614
- */
615
- match(spec) {
616
- this._matchSpec = spec;
617
- return this;
618
- }
619
- /**
620
- * Sets the per-transition declared→actual place correspondence (per
621
- * **MOD-031**). Populated by the subnet rewriter during the compose-time
622
- * rewrite; not normally called by hand-written nets, whose correspondence is
623
- * the identity (empty map).
624
- */
625
- placeAlias(alias) {
626
- this._placeAlias = alias;
627
- return this;
628
- }
629
- build() {
630
- if (this._outputSpec !== null) {
631
- const inputPlaceNames = new Set(this._inputSpecs.map((s) => s.place.name));
632
- for (const fi of findForwardInputs(this._outputSpec)) {
633
- if (!inputPlaceNames.has(fi.from.name)) {
634
- throw new Error(
635
- `Transition '${this._name}': ForwardInput references non-input place '${fi.from.name}'`
636
- );
637
- }
638
- }
639
- }
640
- if (this._matchSpec !== null) {
641
- const inputPlaceNames = new Set(this._inputSpecs.map((s) => s.place.name));
642
- for (const k of this._matchSpec.keys) {
643
- if (!inputPlaceNames.has(k.place.name)) {
644
- throw new Error(
645
- `Transition '${this._name}': MatchSpec correlates non-input place '${k.place.name}'`
646
- );
647
- }
648
- }
649
- }
650
- return new Transition(
651
- TRANSITION_KEY,
652
- this._name,
653
- [...this._inputSpecs],
654
- this._outputSpec,
655
- [...this._inhibitors],
656
- [...this._reads],
657
- [...this._resets],
658
- this._timing,
659
- this._action,
660
- this._priority,
661
- this._placeAlias,
662
- this._matchSpec
663
- );
664
- }
665
- };
666
- function findTimeout(out) {
667
- if (out === null) return null;
668
- switch (out.type) {
669
- case "timeout":
670
- return out;
671
- case "and":
672
- case "xor":
673
- for (const child of out.children) {
674
- const found = findTimeout(child);
675
- if (found !== null) return found;
676
- }
677
- return null;
678
- case "place":
679
- case "forward-input":
680
- return null;
681
- }
682
- }
683
- function findForwardInputs(out) {
684
- switch (out.type) {
685
- case "forward-input":
686
- return [{ from: out.from, to: out.to }];
687
- case "and":
688
- case "xor":
689
- return out.children.flatMap(findForwardInputs);
690
- case "timeout":
691
- return findForwardInputs(out.child);
692
- case "place":
693
- return [];
694
- }
695
- }
696
-
697
- // src/core/interface.ts
698
- var INTERFACE_KEY = /* @__PURE__ */ Symbol("Interface.internal");
699
- var Interface = class {
700
- ports;
701
- channels;
702
- /** @internal Use {@link Interface.builder} to create instances. */
703
- constructor(key, ports, channels) {
704
- if (key !== INTERFACE_KEY) throw new Error("Use Interface.builder() to create instances");
705
- this.ports = ports;
706
- this.channels = channels;
707
- }
708
- /** Looks up a port by name. Returns undefined when absent. */
709
- port(name) {
710
- return this.ports.get(name);
711
- }
712
- /** Looks up a channel by name. Returns undefined when absent. */
713
- channel(name) {
714
- return this.channels.get(name);
715
- }
716
- /**
717
- * Looks up a port by name and returns its underlying place, narrowed to
718
- * `Place<T>`. Returns undefined when the port does not exist.
719
- *
720
- * Note: TypeScript erases generics at runtime, so unlike Java's
721
- * `portPlaceAs(name, Class<T>)` this method cannot verify the token type at
722
- * runtime — the caller is trusted to supply the correct `T`. Per **MOD-022**,
723
- * type compatibility is enforced at compile time only in TypeScript.
724
- */
725
- placeAs(name) {
726
- const p = this.ports.get(name);
727
- if (p === void 0) return void 0;
728
- return p.place;
729
- }
730
- static builder() {
731
- return new InterfaceBuilder();
732
- }
733
- };
734
- var InterfaceBuilder = class {
735
- _ports = /* @__PURE__ */ new Map();
736
- _channels = /* @__PURE__ */ new Map();
737
- /** Add a pre-built port (rejects duplicate names). */
738
- port(port) {
739
- if (this._ports.has(port.name)) {
740
- throw new Error(`Duplicate port name: '${port.name}'`);
741
- }
742
- this._ports.set(port.name, port);
743
- return this;
744
- }
745
- /** Add an input port (advisory direction). */
746
- inputPort(name, place2) {
747
- return this.port({ name, direction: "input", place: place2 });
748
- }
749
- /** Add an output port (advisory direction). */
750
- outputPort(name, place2) {
751
- return this.port({ name, direction: "output", place: place2 });
752
- }
753
- /** Add an in-out port (advisory direction). */
754
- inoutPort(name, place2) {
755
- return this.port({ name, direction: "inout", place: place2 });
756
- }
757
- channel(channelOrName, transition) {
758
- const ch = typeof channelOrName === "string" ? { name: channelOrName, transition } : channelOrName;
759
- if (this._channels.has(ch.name)) {
760
- throw new Error(`Duplicate channel name: '${ch.name}'`);
761
- }
762
- this._channels.set(ch.name, ch);
763
- return this;
764
- }
765
- /** @internal Bulk-add ports already validated by the caller. */
766
- portsAll(ports) {
767
- for (const p of ports) this.port(p);
768
- return this;
769
- }
770
- /** @internal Bulk-add channels already validated by the caller. */
771
- channelsAll(channels) {
772
- for (const c of channels) this.channel(c);
773
- return this;
774
- }
775
- build() {
776
- return new Interface(
777
- INTERFACE_KEY,
778
- new Map(this._ports),
779
- new Map(this._channels)
780
- );
781
- }
782
- };
783
-
784
- // src/core/instance.ts
785
- var INSTANCE_KEY = /* @__PURE__ */ Symbol("Instance.internal");
786
- var Instance = class {
787
- prefix;
788
- def;
789
- renamedBody;
790
- portHandles;
791
- channelHandles;
792
- params;
793
- /**
794
- * @internal Use {@link SubnetDef.instantiate} (or the internal factory
795
- * {@link __createInstance}) to create instances.
796
- */
797
- constructor(key, prefix, def, renamedBody, portHandles, channelHandles, params) {
798
- if (key !== INSTANCE_KEY) {
799
- throw new Error("Use SubnetDef.instantiate() to create Instance values");
800
- }
801
- this.prefix = prefix;
802
- this.def = def;
803
- this.renamedBody = renamedBody;
804
- this.portHandles = portHandles;
805
- this.channelHandles = channelHandles;
806
- this.params = params;
807
- }
808
- /**
809
- * Returns the renamed {@link Place} corresponding to the named port.
810
- *
811
- * The port name is the **original** (pre-prefix) name as declared in the
812
- * subnet's `Interface`. Per **MOD-022**, TypeScript enforces token-type
813
- * compatibility at compile time only; this method does not validate `T`
814
- * at runtime. A missing name raises an `Error`.
815
- *
816
- * @throws when the port name is unknown
817
- */
818
- port(name) {
819
- const p = this.portHandles.get(name);
820
- if (p === void 0) {
821
- throw new Error(`No port named '${name}' in instance '${this.prefix}'`);
822
- }
823
- return p;
824
- }
825
- /**
826
- * Returns the renamed {@link Transition} corresponding to the named channel.
827
- *
828
- * @throws when the channel name is unknown
829
- */
830
- channel(name) {
831
- const t = this.channelHandles.get(name);
832
- if (t === void 0) {
833
- throw new Error(`No channel named '${name}' in instance '${this.prefix}'`);
834
- }
835
- return t;
836
- }
837
- /**
838
- * Returns the debug-UI descriptor for this instance per **MOD-041**.
839
- */
840
- descriptor() {
841
- const transitions = [];
842
- for (const t of this.renamedBody.transitions) transitions.push(t.name);
843
- const exposedPlaces = [];
844
- for (const p of this.portHandles.values()) exposedPlaces.push(p.name);
845
- return {
846
- prefix: this.prefix,
847
- defName: this.def.name,
848
- transitions,
849
- exposedPlaces,
850
- params: this.params,
851
- parentPrefix: null
852
- };
853
- }
854
- /**
855
- * Produces a derived instance whose specified transitions (named by their
856
- * **original**, pre-prefix names) carry the supplied actions per **MOD-030**.
857
- *
858
- * For each entry `[originalName, action]`, the renamed body is searched for
859
- * the transition whose name equals `prefix + "/" + originalName`. The
860
- * resulting instance shares the original `def`, `prefix`, `params`, and
861
- * port/channel handle topology — only the renamed body is rebuilt with new
862
- * actions. Per **MOD-030**, calling `bindActions` on one instance does NOT
863
- * affect the actions held by other instances of the same `def`.
864
- *
865
- * Unrecognised original names raise an `Error` so typos surface eagerly.
866
- */
867
- bindActions(actionsByOriginalName) {
868
- const prefixedByName = /* @__PURE__ */ new Map();
869
- const renamedNames = /* @__PURE__ */ new Set();
870
- for (const t of this.renamedBody.transitions) {
871
- renamedNames.add(t.name);
872
- }
873
- for (const originalName of Object.keys(actionsByOriginalName)) {
874
- const prefixed = this.prefix + "/" + originalName;
875
- if (!renamedNames.has(prefixed)) {
876
- throw new Error(
877
- `Instance.bindActions: no transition '${originalName}' (resolved as '${prefixed}') in instance '${this.prefix}' of subnet '${this.def.name}'`
878
- );
879
- }
880
- prefixedByName.set(prefixed, actionsByOriginalName[originalName]);
881
- }
882
- const reboundBody = this.renamedBody.bindActionsWithResolver((name) => {
883
- const action = prefixedByName.get(name);
884
- if (action !== void 0) return action;
885
- for (const t of this.renamedBody.transitions) {
886
- if (t.name === name) return t.action;
887
- }
888
- throw new Error(`Instance.bindActions: resolver invoked with unknown name '${name}'`);
889
- });
890
- const reboundChannelHandles = /* @__PURE__ */ new Map();
891
- if (this.channelHandles.size > 0) {
892
- const byName = /* @__PURE__ */ new Map();
893
- for (const t of reboundBody.transitions) {
894
- byName.set(t.name, t);
895
- }
896
- for (const [name, oldT] of this.channelHandles) {
897
- const refreshed = byName.get(oldT.name);
898
- if (refreshed === void 0) {
899
- throw new Error(
900
- `Instance.bindActions: channel '${name}' transition '${oldT.name}' not found in rebound body`
901
- );
902
- }
903
- reboundChannelHandles.set(name, refreshed);
904
- }
905
- }
906
- return __createInstance(
907
- this.prefix,
908
- this.def,
909
- reboundBody,
910
- this.portHandles,
911
- reboundChannelHandles,
912
- this.params
913
- );
914
- }
915
- };
916
- function __createInstance(prefix, def, renamedBody, portHandles, channelHandles, params) {
917
- return new Instance(
918
- INSTANCE_KEY,
919
- prefix,
920
- def,
921
- renamedBody,
922
- portHandles,
923
- channelHandles,
924
- params
925
- );
926
- }
927
-
928
- // src/core/internal/subnet-rewriter.ts
929
- function renamePlace(orig, prefix) {
930
- return place(prefix + "/" + orig.name);
931
- }
932
- function renameNet(body, prefix, placeRemap, transitionRemap) {
933
- placeRemap.clear();
934
- transitionRemap.clear();
935
- for (const orig of body.places) {
936
- placeRemap.set(orig.name, renamePlace(orig, prefix));
937
- }
938
- const builder = PetriNet.builder(prefix + "/" + body.name);
939
- for (const renamedPlace of placeRemap.values()) {
940
- builder.place(renamedPlace);
941
- }
942
- for (const t of body.transitions) {
943
- const renamed = rewriteTransition(t, prefix, placeRemap);
944
- transitionRemap.set(t.name, renamed);
945
- builder.transition(renamed);
946
- }
947
- return builder.build();
948
- }
949
- function rewriteTransition(t, prefix, placeRemap) {
950
- return rebuildWithName(t, prefix + "/" + t.name, placeRemap);
951
- }
952
- function substitutePlaces(t, remap) {
953
- return rebuildWithName(t, t.name, remap);
954
- }
955
- function rebuildWithName(t, name, remap, normalizeInputs) {
956
- const builder = Transition.builder(name).timing(t.timing).priority(t.priority).action(t.action);
957
- const alias = buildPlaceAlias(t, remap);
958
- if (alias.size > 0) {
959
- builder.placeAlias(alias);
960
- }
961
- if (t.inputSpecs.length > 0) {
962
- const rewrittenInputs = new Array(t.inputSpecs.length);
963
- for (let i = 0; i < t.inputSpecs.length; i++) {
964
- rewrittenInputs[i] = rewriteIn(t.inputSpecs[i], remap);
965
- }
966
- builder.inputs(...normalizeInputs !== void 0 ? normalizeInputs(rewrittenInputs) : rewrittenInputs);
967
- }
968
- if (t.outputSpec !== null) {
969
- builder.outputs(rewriteOut(t.outputSpec, remap));
970
- }
971
- for (let i = 0; i < t.inhibitors.length; i++) {
972
- builder.inhibitor(rewriteInhibitor(t.inhibitors[i], remap).place);
973
- }
974
- for (let i = 0; i < t.reads.length; i++) {
975
- builder.read(rewriteRead(t.reads[i], remap).place);
976
- }
977
- for (let i = 0; i < t.resets.length; i++) {
978
- builder.reset(rewriteReset(t.resets[i], remap).place);
979
- }
980
- if (t.matchSpec !== null) {
981
- builder.match({
982
- keys: t.matchSpec.keys.map((k) => ({
983
- place: remap.get(k.place.name) ?? k.place,
984
- key: k.key
985
- }))
986
- });
987
- }
988
- return builder.build();
989
- }
990
- function buildPlaceAlias(t, remap) {
991
- const prev = t.placeAlias;
992
- if (remap.size === 0 && prev.size === 0) {
993
- return EMPTY_ALIAS2;
994
- }
995
- const alias = /* @__PURE__ */ new Map();
996
- if (prev.size > 0) {
997
- for (const [declaredName, prevActual] of prev) {
998
- const replaced = remap.get(prevActual.name);
999
- const finalActual = replaced !== void 0 ? replaced : prevActual;
1000
- if (finalActual.name !== declaredName) {
1001
- alias.set(declaredName, finalActual);
1002
- }
1003
- }
1004
- return alias;
1005
- }
1006
- const record = (p) => {
1007
- if (alias.has(p.name)) return;
1008
- const replaced = remap.get(p.name);
1009
- if (replaced !== void 0 && replaced.name !== p.name) {
1010
- alias.set(p.name, replaced);
1011
- }
1012
- };
1013
- for (const spec of t.inputSpecs) record(spec.place);
1014
- for (const rd of t.reads) record(rd.place);
1015
- for (const inh of t.inhibitors) record(inh.place);
1016
- for (const rs of t.resets) record(rs.place);
1017
- if (t.outputSpec !== null) {
1018
- for (const p of allPlaces(t.outputSpec)) record(p);
1019
- }
1020
- return alias;
1021
- }
1022
- var EMPTY_ALIAS2 = /* @__PURE__ */ new Map();
1023
- function rewriteIn(spec, remap) {
1024
- switch (spec.type) {
1025
- case "one":
1026
- return one(resolve(spec.place, remap));
1027
- case "exactly":
1028
- return exactly(spec.count, resolve(spec.place, remap));
1029
- case "all":
1030
- return all(resolve(spec.place, remap));
1031
- case "at-least":
1032
- return atLeast(spec.minimum, resolve(spec.place, remap));
1033
- }
1034
- }
1035
- function rewriteOut(out, remap) {
1036
- switch (out.type) {
1037
- case "place":
1038
- return outPlace(resolve(out.place, remap));
1039
- case "forward-input":
1040
- return forwardInput(resolve(out.from, remap), resolve(out.to, remap));
1041
- case "and": {
1042
- const children = out.children;
1043
- const rewritten = new Array(children.length);
1044
- for (let i = 0; i < children.length; i++) {
1045
- rewritten[i] = rewriteOut(children[i], remap);
1046
- }
1047
- return { type: "and", children: rewritten };
1048
- }
1049
- case "xor": {
1050
- const children = out.children;
1051
- const rewritten = new Array(children.length);
1052
- for (let i = 0; i < children.length; i++) {
1053
- rewritten[i] = rewriteOut(children[i], remap);
1054
- }
1055
- return { type: "xor", children: rewritten };
1056
- }
1057
- case "timeout":
1058
- return timeout(out.afterMs, rewriteOut(out.child, remap));
1059
- }
1060
- }
1061
- function rewriteInhibitor(inh, remap) {
1062
- return { type: "inhibitor", place: resolve(inh.place, remap) };
1063
- }
1064
- function rewriteRead(rd, remap) {
1065
- return { type: "read", place: resolve(rd.place, remap) };
1066
- }
1067
- function rewriteReset(rs, remap) {
1068
- return { type: "reset", place: resolve(rs.place, remap) };
1069
- }
1070
- function resolve(p, remap) {
1071
- const replaced = remap.get(p.name);
1072
- return replaced !== void 0 ? replaced : p;
1073
- }
1074
- function mergeTransitions(caller, instance, mergedName) {
1075
- if (mergedName === void 0 || mergedName === null || mergedName.length === 0) {
1076
- throw new Error("mergeTransitions: mergedName must be a non-empty string");
1077
- }
1078
- const mergedTiming = mergeTimings(caller.timing, instance.timing, mergedName);
1079
- const mergedPriority = pickPriority(caller.priority, instance.priority);
1080
- const mergedAction = composeActions(caller.action, instance.action);
1081
- const mergedMatch = mergeMatchSpecs(caller.matchSpec, instance.matchSpec, mergedName);
1082
- const mergedAlias = mergePlaceAlias(caller.placeAlias, instance.placeAlias, mergedName);
1083
- const builder = Transition.builder(mergedName).timing(mergedTiming).priority(mergedPriority);
1084
- if (mergedAction !== void 0) {
1085
- builder.action(mergedAction);
1086
- }
1087
- rejectCrossSideKindConflicts(caller, instance, mergedName);
1088
- const unionedInputs = normalizeInputArcs(
1089
- [...caller.inputSpecs, ...instance.inputSpecs],
1090
- () => `Channel composition '${mergedName}'`
1091
- );
1092
- if (unionedInputs.length > 0) {
1093
- builder.inputs(...unionedInputs);
1094
- }
1095
- const mergedOutput = mergeOutputs(caller.outputSpec, instance.outputSpec);
1096
- if (mergedOutput !== null) {
1097
- builder.outputs(mergedOutput);
1098
- }
1099
- for (const inh of unionArcs(
1100
- caller.inhibitors,
1101
- instance.inhibitors,
1102
- keyOfInhibitor
1103
- )) {
1104
- builder.inhibitor(inh.place);
1105
- }
1106
- for (const rd of unionArcs(caller.reads, instance.reads, keyOfRead)) {
1107
- builder.read(rd.place);
1108
- }
1109
- for (const rs of unionArcs(caller.resets, instance.resets, keyOfReset)) {
1110
- builder.reset(rs.place);
1111
- }
1112
- if (mergedMatch !== null) {
1113
- builder.match(mergedMatch);
1114
- }
1115
- if (mergedAlias.size > 0) {
1116
- builder.placeAlias(mergedAlias);
1117
- }
1118
- return builder.build();
1119
- }
1120
- function mergeMatchSpecs(caller, instance, channelName) {
1121
- if (caller === null) return instance;
1122
- if (instance === null) return caller;
1123
- throw new Error(
1124
- `Channel composition '${channelName}': both the caller-side and instance-side transition carry a \u03BD-net match \u2014 refusing to fuse two independent correlations into one transition (NU-060). Resolve explicitly by keeping the match on a single side.`
1125
- );
1126
- }
1127
- function mergePlaceAlias(caller, instance, channelName) {
1128
- if (caller.size === 0) return instance;
1129
- if (instance.size === 0) return caller;
1130
- const merged = new Map(caller);
1131
- for (const [declared, actual] of instance) {
1132
- const existing = merged.get(declared);
1133
- if (existing !== void 0 && existing.name !== actual.name) {
1134
- throw new Error(
1135
- `Channel composition '${channelName}': conflicting declared\u2192actual place alias for declared place '${declared}' \u2014 caller-side maps to '${existing.name}', instance-side to '${actual.name}' (MOD-031). Resolve explicitly.`
1136
- );
1137
- }
1138
- merged.set(declared, actual);
1139
- }
1140
- return merged;
1141
- }
1142
- function mergeTimings(caller, instance, channelName) {
1143
- if (caller.type === "immediate" && instance.type === "immediate") {
1144
- return { type: "immediate" };
1145
- }
1146
- if (caller.type === "immediate") return instance;
1147
- if (instance.type === "immediate") return caller;
1148
- if (timingsEqual(caller, instance)) return caller;
1149
- throw new Error(
1150
- `Channel composition '${channelName}': conflicting non-Immediate timings \u2014 caller-side ${describeTiming(caller)} vs instance-side ${describeTiming(instance)}. Resolve explicitly by aligning the timings on either side (MOD-021).`
1151
- );
1152
- }
1153
- function pickPriority(callerPriority, _instancePriority) {
1154
- return callerPriority;
1155
- }
1156
- function composeActions(caller, instance) {
1157
- const callerIsPassthrough = caller === void 0 || isPassthrough(caller);
1158
- const instanceIsPassthrough = instance === void 0 || isPassthrough(instance);
1159
- if (callerIsPassthrough && instanceIsPassthrough) return void 0;
1160
- if (callerIsPassthrough) return instance;
1161
- if (instanceIsPassthrough) return caller;
1162
- return async (ctx) => {
1163
- await caller(ctx);
1164
- await instance(ctx);
1165
- };
1166
- }
1167
- function mergeOutputs(caller, instance) {
1168
- if (caller === null && instance === null) return null;
1169
- if (caller === null) return instance;
1170
- if (instance === null) return caller;
1171
- return and(caller, instance);
1172
- }
1173
- function unionArcs(caller, instance, keyOf) {
1174
- if (caller.length === 0 && instance.length === 0) return [];
1175
- const seen = /* @__PURE__ */ new Map();
1176
- for (let i2 = 0; i2 < caller.length; i2++) {
1177
- const arc = caller[i2];
1178
- const key = keyOf(arc);
1179
- if (!seen.has(key)) seen.set(key, arc);
1180
- }
1181
- for (let i2 = 0; i2 < instance.length; i2++) {
1182
- const arc = instance[i2];
1183
- const key = keyOf(arc);
1184
- if (!seen.has(key)) seen.set(key, arc);
1185
- }
1186
- const result = new Array(seen.size);
1187
- let i = 0;
1188
- for (const arc of seen.values()) result[i++] = arc;
1189
- return result;
1190
- }
1191
- function timingsEqual(a, b) {
1192
- if (a.type !== b.type) return false;
1193
- switch (a.type) {
1194
- case "immediate":
1195
- return true;
1196
- case "deadline":
1197
- return a.byMs === b.byMs;
1198
- case "delayed":
1199
- return a.afterMs === b.afterMs;
1200
- case "window": {
1201
- const w = b;
1202
- return a.earliestMs === w.earliestMs && a.latestMs === w.latestMs;
1203
- }
1204
- case "exact":
1205
- return a.atMs === b.atMs;
1206
- }
1207
- }
1208
- function describeTiming(t) {
1209
- switch (t.type) {
1210
- case "immediate":
1211
- return "Immediate";
1212
- case "deadline":
1213
- return `Deadline(byMs=${t.byMs})`;
1214
- case "delayed":
1215
- return `Delayed(afterMs=${t.afterMs})`;
1216
- case "window":
1217
- return `Window(earliestMs=${t.earliestMs}, latestMs=${t.latestMs})`;
1218
- case "exact":
1219
- return `Exact(atMs=${t.atMs})`;
1220
- }
1221
- }
1222
- function keyOfInhibitor(arc) {
1223
- return `inh|${arc.place.name}`;
1224
- }
1225
- function keyOfRead(arc) {
1226
- return `read|${arc.place.name}`;
1227
- }
1228
- function keyOfReset(arc) {
1229
- return `reset|${arc.place.name}`;
1230
- }
1231
- function normalizeInputArcs(arcs, seamOf) {
1232
- if (arcs.length < 2) return arcs;
1233
- const byPlace = /* @__PURE__ */ new Map();
1234
- let collided = false;
1235
- for (let i2 = 0; i2 < arcs.length; i2++) {
1236
- const arc = arcs[i2];
1237
- const name = arc.place.name;
1238
- const prior = byPlace.get(name);
1239
- if (prior === void 0) {
1240
- byPlace.set(name, arc);
1241
- } else {
1242
- collided = true;
1243
- byPlace.set(name, mergeInPair(prior, arc, seamOf(name)));
1244
- }
1245
- }
1246
- if (!collided) return arcs;
1247
- const result = new Array(byPlace.size);
1248
- let i = 0;
1249
- for (const arc of byPlace.values()) result[i++] = arc;
1250
- return result;
1251
- }
1252
- function mergeInPair(a, b, seam) {
1253
- if (a.type === "all" && b.type === "all") return a;
1254
- if (a.type === "at-least" && b.type === "at-least") {
1255
- return a.minimum >= b.minimum ? a : b;
1256
- }
1257
- const countA = summableCount(a);
1258
- const countB = summableCount(b);
1259
- if (countA !== -1 && countB !== -1) {
1260
- return exactly(countA + countB, a.place);
1261
- }
1262
- throw new Error(
1263
- `${seam}: input arcs ${describeIn(a)} and ${describeIn(b)} collide on place '${a.place.name}' and have no additive merge (MOD-021 rule (c)). Use a single arc with exactly(n) / atLeast(n).`
1264
- );
1265
- }
1266
- function summableCount(arc) {
1267
- switch (arc.type) {
1268
- case "one":
1269
- return 1;
1270
- case "exactly":
1271
- return arc.count;
1272
- default:
1273
- return -1;
1274
- }
1275
- }
1276
- function describeIn(arc) {
1277
- switch (arc.type) {
1278
- case "one":
1279
- return "one()";
1280
- case "exactly":
1281
- return `exactly(${arc.count})`;
1282
- case "all":
1283
- return "all()";
1284
- case "at-least":
1285
- return `atLeast(${arc.minimum})`;
1286
- }
1287
- }
1288
- function rejectCrossSideKindConflicts(caller, instance, mergedName) {
1289
- const callerKinds = arcKindsByPlace(caller);
1290
- if (callerKinds.size === 0) return;
1291
- for (const [place2, instanceSet] of arcKindsByPlace(instance)) {
1292
- const callerSet = callerKinds.get(place2);
1293
- if (callerSet !== void 0 && !sameKindSet(callerSet, instanceSet)) {
1294
- throw new Error(
1295
- `Channel composition '${mergedName}': conflicting arc kinds on place '${place2}' \u2014 caller-side ${describeKinds(callerSet)} vs instance-side ${describeKinds(instanceSet)}. Different arc types on one place cannot be merged (MOD-021 rule (d)). Resolve explicitly.`
1296
- );
1297
- }
1298
- }
1299
- }
1300
- function arcKindsByPlace(t) {
1301
- const kinds = /* @__PURE__ */ new Map();
1302
- const add = (name, kind) => {
1303
- let set = kinds.get(name);
1304
- if (set === void 0) {
1305
- set = /* @__PURE__ */ new Set();
1306
- kinds.set(name, set);
1307
- }
1308
- set.add(kind);
1309
- };
1310
- for (const s of t.inputSpecs) add(s.place.name, "input");
1311
- for (const a of t.inhibitors) add(a.place.name, "inhibitor");
1312
- for (const a of t.reads) add(a.place.name, "read");
1313
- for (const a of t.resets) add(a.place.name, "reset");
1314
- return kinds;
1315
- }
1316
- function describeKinds(kinds) {
1317
- return `[${[...kinds].sort().join(", ")}]`;
1318
- }
1319
- function sameKindSet(a, b) {
1320
- if (a.size !== b.size) return false;
1321
- for (const k of a) if (!b.has(k)) return false;
1322
- return true;
1323
- }
1324
- function applyFusion(transitions, fusionMap, seamOf) {
1325
- const normalizeInputs = (inputs) => normalizeInputArcs(inputs, seamOf);
1326
- const rewritten = /* @__PURE__ */ new Set();
1327
- for (const t of transitions) {
1328
- rewritten.add(rebuildWithName(
1329
- t,
1330
- t.name,
1331
- fusionMap,
1332
- fusionTouchesInputs(t, fusionMap) ? normalizeInputs : void 0
1333
- ));
1334
- }
1335
- return rewritten;
1336
- }
1337
- function fusionTouchesInputs(t, fusionMap) {
1338
- if (fusionMap.size === 0) return false;
1339
- for (let i = 0; i < t.inputSpecs.length; i++) {
1340
- if (fusionMap.has(t.inputSpecs[i].place.name)) return true;
1341
- }
1342
- return false;
1343
- }
1344
-
1345
- // src/verification/verification-harness.ts
1346
- function buildVerificationResult(syntheticNet, perProperty) {
1347
- const frozen = new Map(perProperty);
1348
- return {
1349
- syntheticNet,
1350
- perProperty: frozen,
1351
- allProven() {
1352
- for (const r of frozen.values()) {
1353
- if (!isProven(r)) return false;
1354
- }
1355
- return true;
1356
- },
1357
- anyViolated() {
1358
- for (const r of frozen.values()) {
1359
- if (isViolated(r)) return true;
1360
- }
1361
- return false;
1362
- }
1363
- };
1364
- }
1365
- function normaliseGenerators(generators) {
1366
- if (generators instanceof Map) return new Map(generators);
1367
- return new Map(Object.entries(generators));
1368
- }
1369
- function normaliseProperties(properties) {
1370
- if (Array.isArray(properties)) return [...properties];
1371
- return [...properties];
1372
- }
1373
-
1374
- // src/core/subnet-def.ts
1375
- var SUBNET_DEF_KEY = /* @__PURE__ */ Symbol("SubnetDef.internal");
1376
- var SubnetDef = class _SubnetDef {
1377
- name;
1378
- body;
1379
- iface;
1380
- /** @internal Use {@link SubnetDef.builder} or {@link SubnetDef.fromNet} to create instances. */
1381
- constructor(key, name, body, iface) {
1382
- if (key !== SUBNET_DEF_KEY) {
1383
- throw new Error("Use SubnetDef.builder() or SubnetDef.fromNet() to create instances");
1384
- }
1385
- this.name = name;
1386
- this.body = body;
1387
- this.iface = iface;
1388
- }
1389
- /**
1390
- * Produces a renamed module instance per **MOD-010**, **MOD-011**, **MOD-012**,
1391
- * and **MOD-030**.
1392
- *
1393
- * The rename pass walks every place and transition of the body net,
1394
- * substituting each name with `prefix + "/" + originalName`, and rebuilds
1395
- * every arc with rewritten place references. Transition timing, priority,
1396
- * and action are carried through by reference (action sharing per
1397
- * [MOD-030]). The renamed body is itself a structurally valid `PetriNet`
1398
- * per [CORE-040]; per-instance state isolation per [MOD-012] is a
1399
- * structural consequence of distinct prefixed names.
1400
- *
1401
- * ## Prefix validation
1402
- *
1403
- * The `"/"` character is reserved as the prefix separator (per [MOD-010]).
1404
- * User-supplied prefixes MUST NOT contain `"/"`; nested instantiation is
1405
- * performed by the future `PetriNetBuilder.compose(...)` mechanism (per
1406
- * [MOD-013]). A prefix containing `"/"` raises an `Error`.
1407
- *
1408
- * @param prefix the rename prefix (non-empty, must not contain `"/"`)
1409
- * @param params the parameter value carried through to `Instance.params`
1410
- * (may be omitted when `P` is `void`)
1411
- * @throws when `prefix` is empty or contains `"/"`
1412
- */
1413
- instantiate(prefix, params) {
1414
- validatePrefix(prefix);
1415
- const placeRemap = /* @__PURE__ */ new Map();
1416
- const transitionRemap = /* @__PURE__ */ new Map();
1417
- const renamedBody = renameNet(this.body, prefix, placeRemap, transitionRemap);
1418
- const portHandles = /* @__PURE__ */ new Map();
1419
- for (const port of this.iface.ports.values()) {
1420
- const renamed = placeRemap.get(port.place.name);
1421
- if (renamed === void 0) {
1422
- throw new Error(
1423
- `Port '${port.name}' references place '${port.place.name}' that was not found in the renamed body. This indicates a SubnetDef invariant violation.`
1424
- );
1425
- }
1426
- portHandles.set(port.name, renamed);
1427
- }
1428
- const channelHandles = /* @__PURE__ */ new Map();
1429
- for (const channel of this.iface.channels.values()) {
1430
- const renamed = transitionRemap.get(channel.transition.name);
1431
- if (renamed === void 0) {
1432
- throw new Error(
1433
- `Channel '${channel.name}' references transition '${channel.transition.name}' that was not found in the renamed body. This indicates a SubnetDef invariant violation.`
1434
- );
1435
- }
1436
- channelHandles.set(channel.name, renamed);
1437
- }
1438
- return __createInstance(
1439
- prefix,
1440
- this,
1441
- renamedBody,
1442
- portHandles,
1443
- channelHandles,
1444
- params
1445
- );
1446
- }
1447
- /**
1448
- * Verifies safety properties of this subnet definition **in isolation** per
1449
- * **MOD-051**, by wrapping it in a synthetic enclosing net where each input
1450
- * port is fed by an {@link EnvironmentPlace} (token-source per the harness
1451
- * generator) and each output port is observed via a synthetic place. The
1452
- * standard {@link SmtVerifier} (per [MOD-050]) is invoked once per property
1453
- * declared in the harness; the resulting per-property outcomes are
1454
- * aggregated into a {@link VerificationResult}.
1455
- *
1456
- * ## Synthetic-net construction
1457
- *
1458
- * The synthetic enclosing net is built by:
1459
- * 1. Instantiating this `SubnetDef` with the prefix `"sut"` (system-under-test)
1460
- * and `harness.params`.
1461
- * 2. For each input or in-out port on the interface, looking up the
1462
- * harness generator by port name, allocating a synthetic
1463
- * {@link Place}`<unknown>` of the same conceptual token type as the
1464
- * port, wrapping it in an {@link EnvironmentPlace}, and binding the
1465
- * port to that synthetic place via
1466
- * {@link import('./petri-net.js').PetriNetBuilder.compose}. The supplier
1467
- * is invoked once at construction time to materialize the seed token —
1468
- * its presence is what bounds the input behavior under analysis. **If
1469
- * the harness map is missing a generator for a required input or in-out
1470
- * port, an `Error` is thrown.**
1471
- * 3. For each output or in-out port, allocating a synthetic observation
1472
- * {@link Place}`<unknown>` and binding the port to it via the same
1473
- * `compose(...)` call. The verifier inspects this place's reachability
1474
- * / marking through the standard property APIs ({@link SmtProperty}).
1475
- * 4. Building the resulting flat {@link PetriNet} per [MOD-023] (the
1476
- * verifier is composition-unaware per [MOD-050]).
1477
- *
1478
- * ## Per-property invocation
1479
- *
1480
- * Each {@link SmtProperty} in the harness is verified independently against
1481
- * the same synthetic net. The synthetic environment places are passed
1482
- * through to the verifier so that places driven by the harness generators
1483
- * are treated under the verifier's environment-analysis semantics rather
1484
- * than as ordinary sink places.
1485
- *
1486
- * @param harness the verification harness — supplies parameters, input-port
1487
- * token generators, and the property set
1488
- * @returns a {@link VerificationResult} aggregating per-property
1489
- * {@link SmtVerificationResult}s
1490
- * @throws when an input or in-out port is missing a harness generator
1491
- */
1492
- async verify(harness, environmentMode = alwaysAvailable()) {
1493
- if (harness === null || harness === void 0) {
1494
- throw new Error("SubnetDef.verify: harness must not be null/undefined");
1495
- }
1496
- const sut = this.instantiate("sut", harness.params);
1497
- const generators = normaliseGenerators(harness.portInputGenerators);
1498
- const properties = normaliseProperties(harness.properties);
1499
- const envPlaces = [];
1500
- const portMappings = /* @__PURE__ */ new Map();
1501
- for (const port of this.iface.ports.values()) {
1502
- const portName = port.name;
1503
- switch (port.direction) {
1504
- case "input": {
1505
- const generator = generators.get(portName);
1506
- if (generator === void 0) {
1507
- throw new Error(
1508
- `verify: harness is missing an input generator for port '${portName}' on subnet '${this.name}' (MOD-051)`
1509
- );
1510
- }
1511
- const seed = generator();
1512
- if (seed === null || seed === void 0) {
1513
- throw new Error(
1514
- `verify: input generator for port '${portName}' on subnet '${this.name}' produced null/undefined`
1515
- );
1516
- }
1517
- const synth = place(`harness_in_${portName}`);
1518
- envPlaces.push(environmentPlace(synth.name));
1519
- portMappings.set(portName, synth);
1520
- break;
1521
- }
1522
- case "output": {
1523
- const synth = place(`harness_out_${portName}`);
1524
- portMappings.set(portName, synth);
1525
- break;
1526
- }
1527
- case "inout": {
1528
- const generator = generators.get(portName);
1529
- if (generator === void 0) {
1530
- throw new Error(
1531
- `verify: harness is missing an input generator for in-out port '${portName}' on subnet '${this.name}' (MOD-051)`
1532
- );
1533
- }
1534
- const seed = generator();
1535
- if (seed === null || seed === void 0) {
1536
- throw new Error(
1537
- `verify: input generator for in-out port '${portName}' on subnet '${this.name}' produced null/undefined`
1538
- );
1539
- }
1540
- const synth = place(`harness_io_${portName}`);
1541
- envPlaces.push(environmentPlace(synth.name));
1542
- portMappings.set(portName, synth);
1543
- break;
1544
- }
1545
- }
1546
- }
1547
- const syntheticNet = PetriNet.builder("verify_" + this.name).compose(sut, portMappings).build();
1548
- requireOutputProducingActions(syntheticNet);
1549
- const perProperty = /* @__PURE__ */ new Map();
1550
- for (const property of properties) {
1551
- const verifier = SmtVerifier.forNet(syntheticNet).property(property);
1552
- if (envPlaces.length > 0) {
1553
- verifier.environmentPlaces(...envPlaces);
1554
- verifier.environmentMode(environmentMode);
1555
- }
1556
- const result = await verifier.verify();
1557
- perProperty.set(property, result);
1558
- }
1559
- return buildVerificationResult(syntheticNet, perProperty);
1560
- }
1561
- // ============================================================
1562
- // Static factories
1563
- // ============================================================
1564
- static builder(name) {
1565
- return new SubnetDefBuilder(name);
1566
- }
1567
- /**
1568
- * Retrofit utility per **MOD-014**: wraps an existing closed {@link PetriNet}
1569
- * plus an {@link Interface} into an unparameterised `SubnetDef<void>`.
1570
- *
1571
- * Validation per **MOD-014** / **MOD-006** is enforced before the result is
1572
- * constructed:
1573
- * - Every port's underlying `Place` must be present in `net.places`.
1574
- * - Every channel's underlying `Transition` must be present in `net.transitions`.
1575
- * - Port and channel name uniqueness is re-validated defensively (the
1576
- * `Interface` builder already enforces this; hand-built `Interface`
1577
- * values bypass that path).
1578
- *
1579
- * The resulting subnet definition is unparameterised (parameter type is `void`).
1580
- *
1581
- * @throws when a port place is not in `net.places`, a channel transition is
1582
- * not in `net.transitions`, or port/channel names are not unique.
1583
- */
1584
- static fromNet(net, iface) {
1585
- const bodyPlaces = net.places;
1586
- const bodyTransitions = net.transitions;
1587
- const seenPortNames = /* @__PURE__ */ new Set();
1588
- for (const port of iface.ports.values()) {
1589
- if (seenPortNames.has(port.name)) {
1590
- throw new Error(
1591
- `fromNet: duplicate port name '${port.name}' on interface for net '${net.name}' (MOD-006)`
1592
- );
1593
- }
1594
- seenPortNames.add(port.name);
1595
- if (!bodyPlaces.has(port.place)) {
1596
- throw new Error(
1597
- `fromNet: port '${port.name}' references place '${port.place.name}' which is not in net '${net.name}' (MOD-014/MOD-006)`
1598
- );
1599
- }
1600
- }
1601
- const seenChannelNames = /* @__PURE__ */ new Set();
1602
- for (const channel of iface.channels.values()) {
1603
- if (seenChannelNames.has(channel.name)) {
1604
- throw new Error(
1605
- `fromNet: duplicate channel name '${channel.name}' on interface for net '${net.name}' (MOD-006)`
1606
- );
1607
- }
1608
- seenChannelNames.add(channel.name);
1609
- if (!bodyTransitions.has(channel.transition)) {
1610
- throw new Error(
1611
- `fromNet: channel '${channel.name}' references transition '${channel.transition.name}' which is not in net '${net.name}' (MOD-014/MOD-006)`
1612
- );
1613
- }
1614
- }
1615
- return new _SubnetDef(SUBNET_DEF_KEY, net.name, net, iface);
1616
- }
1617
- };
1618
- var SubnetDefBuilder = class {
1619
- _name;
1620
- _bodyBuilder;
1621
- _ports = [];
1622
- _channels = [];
1623
- constructor(name) {
1624
- this._name = name;
1625
- this._bodyBuilder = PetriNet.builder(name);
1626
- }
1627
- // -------- body construction (delegates to PetriNet.Builder) --------
1628
- transition(transition) {
1629
- this._bodyBuilder.transition(transition);
1630
- return this;
1631
- }
1632
- transitions(...transitions) {
1633
- this._bodyBuilder.transitions(...transitions);
1634
- return this;
1635
- }
1636
- place(place2) {
1637
- this._bodyBuilder.place(place2);
1638
- return this;
1639
- }
1640
- // -------- interface declarations --------
1641
- inputPort(name, place2) {
1642
- this._ports.push({ name, direction: "input", place: place2 });
1643
- return this;
1644
- }
1645
- outputPort(name, place2) {
1646
- this._ports.push({ name, direction: "output", place: place2 });
1647
- return this;
1648
- }
1649
- inoutPort(name, place2) {
1650
- this._ports.push({ name, direction: "inout", place: place2 });
1651
- return this;
1652
- }
1653
- channel(name, transition) {
1654
- this._channels.push({ name, transition });
1655
- return this;
1656
- }
1657
- // -------- build & validate (MOD-006) --------
1658
- build() {
1659
- const built = this._bodyBuilder.build();
1660
- const bodyPlaces = built.places;
1661
- const bodyTransitions = built.transitions;
1662
- const portNames = /* @__PURE__ */ new Set();
1663
- for (const port of this._ports) {
1664
- if (portNames.has(port.name)) {
1665
- throw new Error(`Subnet '${this._name}': duplicate port name '${port.name}'`);
1666
- }
1667
- portNames.add(port.name);
1668
- if (!bodyPlaces.has(port.place)) {
1669
- throw new Error(
1670
- `Subnet '${this._name}': port '${port.name}' references place '${port.place.name}' which is not in the body`
1671
- );
1672
- }
1673
- }
1674
- const channelNames = /* @__PURE__ */ new Set();
1675
- for (const channel of this._channels) {
1676
- if (channelNames.has(channel.name)) {
1677
- throw new Error(`Subnet '${this._name}': duplicate channel name '${channel.name}'`);
1678
- }
1679
- channelNames.add(channel.name);
1680
- if (!bodyTransitions.has(channel.transition)) {
1681
- throw new Error(
1682
- `Subnet '${this._name}': channel '${channel.name}' references transition '${channel.transition.name}' which is not in the body`
1683
- );
1684
- }
1685
- }
1686
- const ifaceBuilder = Interface.builder();
1687
- ifaceBuilder.portsAll(this._ports);
1688
- ifaceBuilder.channelsAll(this._channels);
1689
- const iface = ifaceBuilder.build();
1690
- return new SubnetDef(SUBNET_DEF_KEY, this._name, built, iface);
1691
- }
1692
- };
1693
- function validatePrefix(prefix) {
1694
- if (typeof prefix !== "string" || prefix.length === 0) {
1695
- throw new Error("SubnetDef.instantiate: prefix must be a non-empty string");
1696
- }
1697
- if (prefix.indexOf("/") >= 0) {
1698
- throw new Error(
1699
- `SubnetDef.instantiate: prefix must not contain '/' (reserved as the prefix separator per MOD-010); use compose(...) for nested instantiation. Got: '${prefix}'`
1700
- );
1701
- }
1702
- }
1703
-
1704
- // src/core/compose-bindings.ts
1705
- var COMPOSE_BINDINGS_KEY = /* @__PURE__ */ Symbol("ComposeBindings.internal");
1706
- var ComposeBindings = class {
1707
- _portBindings = /* @__PURE__ */ new Map();
1708
- _channelBindings = /* @__PURE__ */ new Map();
1709
- /**
1710
- * @internal Use {@link PetriNetBuilder.compose} — instances are produced
1711
- * by the host builder and supplied to the caller's callback.
1712
- */
1713
- constructor(key) {
1714
- if (key !== COMPOSE_BINDINGS_KEY) {
1715
- throw new Error(
1716
- "Use PetriNetBuilder.compose(instance, b => ...) \u2014 ComposeBindings is not directly constructible"
1717
- );
1718
- }
1719
- }
1720
- /**
1721
- * Binds the named interface port to the given caller place per **MOD-020**.
1722
- *
1723
- * The port name is the **original** (pre-prefix) name declared in the
1724
- * subnet's `Interface`. The typed `<T>` parameter ensures the caller
1725
- * place's token type matches the interface port's token type at compile
1726
- * time per [MOD-022]; TypeScript does not validate the type at runtime
1727
- * because generics are erased.
1728
- *
1729
- * @throws when `portName` is already bound on this builder
1730
- */
1731
- bindPort(portName, callerPlace) {
1732
- if (this._portBindings.has(portName)) {
1733
- throw new Error(`Port '${portName}' is already bound`);
1734
- }
1735
- this._portBindings.set(portName, callerPlace);
1736
- return this;
1737
- }
1738
- /**
1739
- * Records a synchronous channel binding per **MOD-021**: at compose time,
1740
- * the instance-side renamed channel transition is merged with
1741
- * `callerTransition` into a single transition in the resulting flat net.
1742
- *
1743
- * **Status**: channel composition is implemented in task #13. Recording a
1744
- * channel binding here is permitted, but `compose(...)` currently raises
1745
- * an `Error` when any channel binding is present.
1746
- *
1747
- * @throws when `channelName` is already bound on this builder
1748
- */
1749
- bindChannel(channelName, callerTransition) {
1750
- if (this._channelBindings.has(channelName)) {
1751
- throw new Error(`Channel '${channelName}' is already bound`);
1752
- }
1753
- this._channelBindings.set(channelName, callerTransition);
1754
- return this;
1755
- }
1756
- /** Returns an unmodifiable view of the recorded port bindings. */
1757
- portBindings() {
1758
- return this._portBindings;
1759
- }
1760
- /** Returns an unmodifiable view of the recorded channel bindings. */
1761
- channelBindings() {
1762
- return this._channelBindings;
1763
- }
1764
- };
1765
- function __createComposeBindings() {
1766
- return new ComposeBindings(COMPOSE_BINDINGS_KEY);
1767
- }
1768
-
1769
- // src/core/fusion-set.ts
1770
- var FUSION_SET_KEY = /* @__PURE__ */ Symbol("FusionSet.internal");
1771
- var FusionSet = class _FusionSet {
1772
- name;
1773
- members;
1774
- /** @internal Use {@link FusionSet.builder} or {@link FusionSet.of} to create instances. */
1775
- constructor(key, name, members) {
1776
- if (key !== FUSION_SET_KEY) {
1777
- throw new Error("Use FusionSet.builder() or FusionSet.of() to create instances");
1778
- }
1779
- this.name = name;
1780
- this.members = members;
1781
- }
1782
- /**
1783
- * Returns the canonical member — by convention, the first declared member.
1784
- * The canonical place's identity survives into the resulting flat net.
1785
- */
1786
- get canonical() {
1787
- return this.members[0];
1788
- }
1789
- /**
1790
- * Returns all members **except** the canonical member, in declaration order.
1791
- * These are the places that get substituted away at
1792
- * {@link import('./petri-net.js').PetriNetBuilder.build} time.
1793
- *
1794
- * For a single-member (degenerate) set, returns an empty array.
1795
- */
1796
- nonCanonical() {
1797
- if (this.members.length <= 1) return [];
1798
- return this.members.slice(1);
1799
- }
1800
- toString() {
1801
- return `FusionSet[${this.name}, canonical=${this.canonical.name}, members=${this.members.length}]`;
1802
- }
1803
- // ============================================================
1804
- // Static factories
1805
- // ============================================================
1806
- /** Returns a fresh {@link FusionSetBuilder} with the given human-readable name. */
1807
- static builder(name) {
1808
- return new FusionSetBuilder(name);
1809
- }
1810
- /**
1811
- * Convenience factory: builds a fusion set whose first member is `first`
1812
- * (the canonical) and whose remaining members are `rest`, all sharing the
1813
- * type `<T>`.
1814
- *
1815
- * The varargs form ensures static-type homogeneity at the call site (the
1816
- * TypeScript compiler checks every `rest` entry is a `Place<T>`).
1817
- */
1818
- static of(name, first, ...rest) {
1819
- const members = [first];
1820
- for (const p of rest) members.push(p);
1821
- return new _FusionSet(FUSION_SET_KEY, name, Object.freeze(members));
1822
- }
1823
- };
1824
- var FusionSetBuilder = class {
1825
- _name;
1826
- _members = [];
1827
- constructor(name) {
1828
- if (typeof name !== "string" || name.length === 0) {
1829
- throw new Error("FusionSet.builder: name must be a non-empty string");
1830
- }
1831
- this._name = name;
1832
- }
1833
- /**
1834
- * Appends a member to the fusion set. The first member becomes the canonical
1835
- * place per the convention documented on {@link FusionSet}.
1836
- *
1837
- * The `<T>` parameter is for compile-time guidance only: callers writing
1838
- * typed code at the same call site benefit from the compiler checking
1839
- * `Place<T>` at each member.
1840
- */
1841
- member(place2) {
1842
- if (place2 === void 0 || place2 === null) {
1843
- throw new Error(`FusionSet '${this._name}': member place must not be null`);
1844
- }
1845
- this._members.push(place2);
1846
- return this;
1847
- }
1848
- /**
1849
- * Builds the immutable {@link FusionSet}. Validates that the set has at
1850
- * least one member; the empty set is rejected as malformed per **MOD-060**.
1851
- * Single-member sets are accepted as a structurally degenerate no-op.
1852
- */
1853
- build() {
1854
- if (this._members.length === 0) {
1855
- throw new Error(
1856
- `FusionSet '${this._name}': must have at least one member (MOD-060)`
1857
- );
1858
- }
1859
- return new FusionSet(FUSION_SET_KEY, this._name, Object.freeze(this._members.slice()));
1860
- }
1861
- };
1862
-
1863
- // src/core/petri-net.ts
1864
- var PETRI_NET_KEY = /* @__PURE__ */ Symbol("PetriNet.internal");
1865
- var PetriNet = class _PetriNet {
1866
- name;
1867
- places;
1868
- transitions;
1869
- /**
1870
- * Subnet-membership metadata per **MOD-026**: maps each node name (place or
1871
- * transition) contributed by exactly one directly-composed subnet to that
1872
- * subnet's name. Shared places contributed by two or more subnets, and every
1873
- * node of a net not built via {@link PetriNetBuilder.compose}`(SubnetDef)`,
1874
- * are absent. Never null — an empty map when there is no metadata. The DOT
1875
- * exporter renders `subgraph cluster_*` blocks from this map.
1876
- *
1877
- * V8 `Map` is insertion-ordered, preserving compose order so cluster
1878
- * subgraphs render deterministically (cross-language byte-parity).
1879
- */
1880
- subnetMembership;
1881
- /** @internal Use {@link PetriNet.builder} to create instances. */
1882
- constructor(key, name, places, transitions, subnetMembership = /* @__PURE__ */ new Map()) {
1883
- if (key !== PETRI_NET_KEY) throw new Error("Use PetriNet.builder() to create instances");
1884
- this.name = name;
1885
- this.places = places;
1886
- this.transitions = transitions;
1887
- this.subnetMembership = subnetMembership;
1888
- }
1889
- /**
1890
- * Creates a new PetriNet with actions bound to transitions by name.
1891
- * Unbound transitions keep passthrough action.
1892
- */
1893
- bindActions(actionBindings) {
1894
- const bindings = actionBindings instanceof Map ? actionBindings : new Map(Object.entries(actionBindings));
1895
- return this.bindActionsWithResolver(
1896
- (name) => bindings.get(name) ?? passthrough()
1897
- );
1898
- }
1899
- /**
1900
- * Creates a new PetriNet with actions bound via a resolver function.
1901
- *
1902
- * The resolver is called once per transition with its name. Returning `null`
1903
- * defers that transition — it keeps whatever action it already carries — which
1904
- * is what makes staged binding (**MOD-024** AC7) work.
1905
- */
1906
- bindActionsWithResolver(actionResolver) {
1907
- const boundTransitions = /* @__PURE__ */ new Set();
1908
- for (const t of this.transitions) {
1909
- const action = actionResolver(t.name);
1910
- if (action !== null && action !== t.action) {
1911
- boundTransitions.add(rebuildWithAction(t, action));
1912
- } else {
1913
- boundTransitions.add(t);
1914
- }
1915
- }
1916
- return new _PetriNet(
1917
- PETRI_NET_KEY,
1918
- this.name,
1919
- this.places,
1920
- boundTransitions,
1921
- this.subnetMembership
1922
- );
226
+ return values[0];
1923
227
  }
1924
- static builder(name) {
1925
- return new PetriNetBuilder(name);
228
+ /** Get all consumed input values for a place. */
229
+ inputs(place2) {
230
+ const actual = this.resolve(place2);
231
+ this.requireInput(actual);
232
+ return this.rawInput.values(actual);
1926
233
  }
1927
- };
1928
- var PetriNetBuilder = class _PetriNetBuilder {
1929
- _name;
1930
- _places = /* @__PURE__ */ new Set();
1931
- _transitions = /* @__PURE__ */ new Set();
1932
- _fusionSets = [];
1933
- // MOD-026: node name -> set of subnet names that contributed it via
1934
- // compose(SubnetDef), in first-contribution order. Resolved to single-owner
1935
- // membership at build(). V8 Map/Set iterate in insertion order, preserving
1936
- // compose order for cross-language byte-parity.
1937
- _subnetContributions = /* @__PURE__ */ new Map();
1938
- constructor(name) {
1939
- this._name = name;
1940
- }
1941
- /** Add an explicit place. */
1942
- place(place2) {
1943
- this._places.add(place2);
1944
- return this;
234
+ /** Get consumed input token with metadata. */
235
+ inputToken(place2) {
236
+ const actual = this.resolve(place2);
237
+ this.requireInput(actual);
238
+ return this.rawInput.get(actual);
1945
239
  }
1946
- /** Add explicit places. */
1947
- places(...places) {
1948
- for (const p of places) this._places.add(p);
1949
- return this;
240
+ /** Returns declared input places (consumed). */
241
+ inputPlaces() {
242
+ return this._inputPlaces;
1950
243
  }
1951
- /** Add a transition (auto-collects places from arcs). */
1952
- transition(transition) {
1953
- this._transitions.add(transition);
1954
- for (const spec of transition.inputSpecs) {
1955
- this._places.add(spec.place);
1956
- }
1957
- for (const p of transition.outputPlaces()) {
1958
- this._places.add(p);
1959
- }
1960
- for (const inh of transition.inhibitors) {
1961
- this._places.add(inh.place);
1962
- }
1963
- for (const r of transition.reads) {
1964
- this._places.add(r.place);
1965
- }
1966
- for (const r of transition.resets) {
1967
- this._places.add(r.place);
244
+ requireInput(place2) {
245
+ if (!this.allowedInputs.has(place2.name)) {
246
+ throw new Error(
247
+ `Place '${place2.name}' not in declared inputs: [${[...this.allowedInputs].join(", ")}]`
248
+ );
1968
249
  }
1969
- return this;
1970
250
  }
1971
- /** Add transitions (auto-collects places from arcs). */
1972
- transitions(...transitions) {
1973
- for (const t of transitions) this.transition(t);
1974
- return this;
251
+ // ==================== Read Access (not consumed) ====================
252
+ /** Get read-only context value. Throws if place not declared as read. */
253
+ read(place2) {
254
+ const actual = this.resolve(place2);
255
+ this.requireRead(actual);
256
+ return this.rawInput.value(actual);
1975
257
  }
1976
- compose(instanceOrDef, arg) {
1977
- if (instanceOrDef instanceof SubnetDef) {
1978
- return this.composeDirect(instanceOrDef);
1979
- }
1980
- const instance = instanceOrDef;
1981
- if (arg === void 0) {
1982
- return this.composeAuto(instance);
1983
- }
1984
- if (typeof arg === "function") {
1985
- const bindings = __createComposeBindings();
1986
- arg(bindings);
1987
- return this.composeInternal(instance, bindings.portBindings(), bindings.channelBindings());
258
+ /** Get all read-only context values for a place. */
259
+ reads(place2) {
260
+ const actual = this.resolve(place2);
261
+ this.requireRead(actual);
262
+ return this.rawInput.values(actual);
263
+ }
264
+ /** Returns declared read places (context, not consumed). */
265
+ readPlaces() {
266
+ return this._readPlaces;
267
+ }
268
+ requireRead(place2) {
269
+ if (!this.allowedReads.has(place2.name)) {
270
+ throw new Error(
271
+ `Place '${place2.name}' not in declared reads: [${[...this.allowedReads].join(", ")}]`
272
+ );
1988
273
  }
1989
- const portMappings = arg instanceof Map ? arg : new Map(Object.entries(arg));
1990
- return this.composeInternal(instance, portMappings, /* @__PURE__ */ new Map());
1991
274
  }
275
+ // ==================== Output Access ====================
1992
276
  /**
1993
- * Composes a subnet {@link SubnetDef} **directly** into this builder per
1994
- * **MOD-025** — **without instantiation**, and without the prefix-renaming
1995
- * of {@link SubnetDef.instantiate}.
1996
- *
1997
- * Every body place and transition is added under its **original**
1998
- * (un-prefixed) name. Places merge into this builder by name: a body place
1999
- * whose name equals an enclosing-net place *is* that place in the composed
2000
- * flat net. This is the mode for wiring a subnet in as a single shared copy.
2001
- *
2002
- * Direct composition is **order-independent**: composing the same set of
2003
- * subnets in any order yields the same flat net, because merging is by
2004
- * place name and not by a probe of the builder's place set at call time —
2005
- * contrast the no-interface body-inference branch of {@link composeAuto}
2006
- * (MOD-024), which is order-sensitive.
2007
- *
2008
- * For multiple *independent* copies — each with isolated per-instance state
2009
- * per [MOD-012] — use {@link SubnetDef.instantiate} + the `compose(instance)`
2010
- * overload instead.
277
+ * Add one or more output values to the same place in a single call.
2011
278
  *
2012
- * Rejections: a body transition whose name already exists in this builder
2013
- * (use `instantiate(prefix)` for independent copies); a subnet whose
2014
- * interface declares any channel (direct composition does not bind
2015
- * channels — use `instantiate` + the channel-binding `compose` overload).
279
+ * Validates the place once, then appends each value to the output
280
+ * collector. Calling with zero values is a no-op.
2016
281
  *
2017
- * Token-type conflicts on a same-named place cannot be detected: TS
2018
- * {@link Place} equality is name-only at runtime (the documented carve-out,
2019
- * same as MOD-024).
282
+ * @example
283
+ * ctx.output(outPlace, 'a', 'b', 'c');
284
+ * ctx.output(outPlace, ...someArray);
2020
285
  *
2021
- * @throws when the subnet declares channels, or a body transition name
2022
- * collides with a transition already in this builder.
286
+ * @throws if place not declared as output.
2023
287
  */
2024
- composeDirect(def) {
2025
- const iface = def.iface;
2026
- if (iface.channels.size > 0) {
2027
- const channelNames = [];
2028
- for (const c of iface.channels.values()) channelNames.push(c.name);
2029
- channelNames.sort();
2030
- throw new Error(
2031
- `compose(SubnetDef): subnet '${def.name}' declares channels [${channelNames.join(", ")}]; direct composition does not bind channels. Use def.instantiate(prefix) + compose(instance, bind => bind.bindChannel(...)).`
2032
- );
2033
- }
2034
- const body = def.body;
2035
- const hostTransitionNames = /* @__PURE__ */ new Set();
2036
- for (const t of this._transitions) hostTransitionNames.add(t.name);
2037
- for (const t of body.transitions) {
2038
- if (hostTransitionNames.has(t.name)) {
2039
- throw new Error(
2040
- `compose(SubnetDef): transition '${t.name}' from subnet '${def.name}' collides with a transition already in net '${this._name}'. Direct composition merges by name; for independent copies use def.instantiate(prefix) + compose(instance).`
2041
- );
2042
- }
2043
- }
2044
- const hostByName = /* @__PURE__ */ new Map();
2045
- for (const p of this._places) hostByName.set(p.name, p);
2046
- const subnetName = def.name.replace(/\//g, "_");
2047
- const mergeMap = /* @__PURE__ */ new Map();
2048
- for (const p of body.places) {
2049
- const host = hostByName.get(p.name);
2050
- this.place(host ?? p);
2051
- if (host !== void 0) mergeMap.set(p.name, host);
2052
- this.recordContribution(p.name, subnetName);
2053
- }
2054
- for (const t of body.transitions) {
2055
- this.transition(substitutePlaces(t, mergeMap));
2056
- this.recordContribution(t.name, subnetName);
288
+ output(place2, ...values) {
289
+ const actual = this.resolve(place2);
290
+ this.requireOutput(actual);
291
+ for (const value of values) {
292
+ this.writeTarget.add(actual, value);
2057
293
  }
2058
294
  return this;
2059
295
  }
2060
- /** @internal MOD-026: records a node as contributed by the named subnet. */
2061
- recordContribution(nodeName, subnetName) {
2062
- let owners = this._subnetContributions.get(nodeName);
2063
- if (owners === void 0) {
2064
- owners = /* @__PURE__ */ new Set();
2065
- this._subnetContributions.set(nodeName, owners);
2066
- }
2067
- owners.add(subnetName);
2068
- }
2069
296
  /**
2070
- * Identity-default auto-compose per **MOD-024**.
2071
- *
2072
- * Each declared interface port auto-binds to its own `port.place` — the
2073
- * Place the SubnetDef builder declared via `.inputPort(name, hostPlace)`
2074
- * (or `outputPort` / `inoutPort`). If the host builder already declares
2075
- * the equal place, the two merge; if not, the place arrives implicitly
2076
- * via the rewritten transitions' arcs (same flow as explicit `bindPort`).
297
+ * Add one or more pre-built output tokens to the same place in a single call.
2077
298
  *
2078
- * If the subnet declares no interface ports at all, body places are
2079
- * checked against this builder's place set **by name** (matching the
2080
- * existing TS Place equality semantics; see [CORE-002] note in
2081
- * `spec/11-modular-composition.md` MOD-024). Body places that don't match
2082
- * stay private under their prefixed names per [MOD-010].
299
+ * Validates the place once, then appends each token. Calling with zero
300
+ * tokens is a no-op.
2083
301
  *
2084
- * Channels are NOT auto-bound transition identity is too delicate for
2085
- * inference. If the subnet declares any channel, this overload throws.
302
+ * @throws if place not declared as output.
2086
303
  */
2087
- composeAuto(instance) {
2088
- const iface = instance.def.iface;
2089
- if (iface.channels.size > 0) {
2090
- const channelNames = [];
2091
- for (const c of iface.channels.values()) channelNames.push(c.name);
2092
- channelNames.sort();
304
+ outputToken(place2, ...tokens) {
305
+ const actual = this.resolve(place2);
306
+ this.requireOutput(actual);
307
+ for (const token of tokens) {
308
+ this.writeTarget.addToken(actual, token);
309
+ }
310
+ return this;
311
+ }
312
+ /** Returns declared output places. */
313
+ outputPlaces() {
314
+ return this._outputPlaces;
315
+ }
316
+ requireOutput(place2) {
317
+ if (!this.allowedOutputs.has(place2.name)) {
2093
318
  throw new Error(
2094
- `compose(Instance): subnet '${instance.def.name}' (instance prefix '${instance.prefix}') declares channels [${channelNames.join(", ")}]; auto-compose does not bind channels. Use compose(instance, bind => bind.bindChannel(...)) with explicit channel bindings.`
319
+ `Place '${place2.name}' not in declared outputs: [${[...this.allowedOutputs].join(", ")}]`
2095
320
  );
2096
321
  }
2097
- const hostByName = /* @__PURE__ */ new Map();
2098
- for (const p of this._places) hostByName.set(p.name, p);
2099
- if (iface.ports.size > 0) {
2100
- const portMappings = /* @__PURE__ */ new Map();
2101
- for (const port of iface.ports.values()) {
2102
- const hostMatch = hostByName.get(port.place.name);
2103
- portMappings.set(port.name, hostMatch ?? port.place);
2104
- }
2105
- return this.composeInternal(instance, portMappings, /* @__PURE__ */ new Map());
2106
- }
2107
- const mergeMap = /* @__PURE__ */ new Map();
2108
- const prefix = instance.prefix + "/";
2109
- for (const renamed of instance.renamedBody.places) {
2110
- if (!renamed.name.startsWith(prefix)) continue;
2111
- const originalName = renamed.name.substring(prefix.length);
2112
- const hostMatch = hostByName.get(originalName);
2113
- if (hostMatch !== void 0) {
2114
- mergeMap.set(renamed.name, hostMatch);
2115
- }
2116
- }
2117
- return this.applyComposition(instance, mergeMap, /* @__PURE__ */ new Map());
2118
322
  }
2119
323
  /**
2120
- * @internal Shared compose implementation: validates port and channel
2121
- * bindings, builds the place-substitution map, walks every renamed-body
2122
- * transition through {@link substitutePlaces}, applies channel merges per
2123
- * [MOD-021], and adds the resulting transitions to this builder.
2124
- *
2125
- * ## Channel-merge flow ([MOD-021])
324
+ * Severs the action's output from the marking, for use when a firing times out.
2126
325
  *
2127
- * 1. Collect rewritten instance transitions into a working `Map<string,
2128
- * Transition>` keyed by prefixed transition name (deferred not yet
2129
- * added to the builder's transition set).
2130
- * 2. For each channel binding, resolve the renamed instance-side
2131
- * transition through `instance.channel(channelName)`, then look up its
2132
- * rewritten counterpart in the working map by name.
2133
- * 3. Replace the working-map entry with a {@link mergeTransitions} result
2134
- * that fuses caller-side + instance-side; remove the rewritten
2135
- * instance-side entry. Also replace (or add) the caller-side
2136
- * transition in this builder's transition set with the same merged
2137
- * result, indexed under the caller's name slot.
2138
- * 4. Add the surviving (un-merged) entries to this builder.
326
+ * After this call the action's `output(...)` writes are dropped, and the executor
327
+ * harvests a fresh collector the action holds no reference to. Anything the action
328
+ * wrote *before* the timeout is discarded with it: a partial result merged with the
329
+ * timeout branch would violate the transition's own output spec.
2139
330
  *
2140
- * The deferral matters: writing the rewritten instance transitions to the
2141
- * builder eagerly would force a second "remove-then-replace" pass to
2142
- * apply the channel merges, complicating the place-collection invariants.
2143
- * Collecting first and merging second keeps the builder's transition set
2144
- * finalized exactly once.
331
+ * This is the only isolation available libpetri does not own the promise the action
332
+ * runs on, so it cannot stop the work, only stop the result from landing.
2145
333
  *
2146
- * Keying the working map by prefixed transition name (rather than by
2147
- * Transition reference) is also robust against a prior
2148
- * {@link Instance.bindActions} call that may have rebuilt the renamed-body
2149
- * transitions, breaking identity equality between the body and the
2150
- * channel-handle map — but the prefixed names remain stable.
334
+ * @internal Executor machinery must be called by the executor, never by an action.
2151
335
  */
2152
- composeInternal(instance, portMappings, channelBindings) {
2153
- const iface = instance.def.iface;
2154
- const mergeMap = /* @__PURE__ */ new Map();
2155
- for (const [portName, callerPlace] of portMappings) {
2156
- const port = iface.port(portName);
2157
- if (port === void 0) {
2158
- const knownPorts = [];
2159
- for (const p of iface.ports.values()) knownPorts.push(p.name);
2160
- throw new Error(
2161
- `compose: no port named '${portName}' on subnet '${instance.def.name}' (instance prefix '${instance.prefix}'). Known ports: [${knownPorts.join(", ")}]`
2162
- );
2163
- }
2164
- const ifacePlace = instance.port(portName);
2165
- mergeMap.set(ifacePlace.name, callerPlace);
2166
- }
2167
- return this.applyComposition(instance, mergeMap, channelBindings);
336
+ detachForTimeout() {
337
+ this.writeTarget.detach();
338
+ this._rawOutput = new TokenOutput();
2168
339
  }
2169
340
  /**
2170
- * Shared post-mergeMap pipeline: rewrites renamed-body transitions
2171
- * through `mergeMap`, applies channel merges per **MOD-021**, and adds
2172
- * the surviving transitions to the builder.
341
+ * Produces into the executor's harvest collector rather than the action's write target.
342
+ *
343
+ * Identical to {@link output} before {@link detachForTimeout}; after it, this is the
344
+ * only route that still reaches the marking. Used by the executor to deposit the
345
+ * timeout branch.
2173
346
  *
2174
- * Used by both the explicit-binding path (`composeInternal`) and the
2175
- * auto-compose path (`composeAuto` per **MOD-024**). The two paths differ
2176
- * only in how the (renamed Place name → host Place) `mergeMap` is built.
347
+ * @internal Executor machinery — must be called by the executor, never by an action.
2177
348
  */
2178
- applyComposition(instance, mergeMap, channelBindings) {
2179
- const iface = instance.def.iface;
2180
- const rewrittenByName = /* @__PURE__ */ new Map();
2181
- for (const t of instance.renamedBody.transitions) {
2182
- rewrittenByName.set(t.name, substitutePlaces(t, mergeMap));
2183
- }
2184
- for (const [channelName, callerTrans] of channelBindings) {
2185
- let instanceRenamedChannel;
2186
- try {
2187
- instanceRenamedChannel = instance.channel(channelName);
2188
- } catch (cause) {
2189
- const knownChannels = [];
2190
- for (const c of iface.channels.values()) knownChannels.push(c.name);
2191
- const err = new Error(
2192
- `compose: no channel named '${channelName}' on subnet '${instance.def.name}' (instance prefix '${instance.prefix}'). Known channels: [${knownChannels.join(", ")}]`
2193
- );
2194
- err.cause = cause;
2195
- throw err;
2196
- }
2197
- const rewrittenInstanceChannel = rewrittenByName.get(instanceRenamedChannel.name);
2198
- if (rewrittenInstanceChannel === void 0) {
2199
- throw new Error(
2200
- `compose: channel '${channelName}' resolved to a transition '${instanceRenamedChannel.name}' that is not present in the renamed body. This indicates a SubnetDef invariant violation.`
2201
- );
2202
- }
2203
- const merged = mergeTransitions(callerTrans, rewrittenInstanceChannel, callerTrans.name);
2204
- rewrittenByName.delete(instanceRenamedChannel.name);
2205
- this._transitions.delete(callerTrans);
2206
- this.transition(merged);
2207
- }
2208
- for (const rewritten of rewrittenByName.values()) {
2209
- this.transition(rewritten);
2210
- }
2211
- return this;
2212
- }
2213
- fuse(...args) {
2214
- if (args.length === 1 && typeof args[0] === "function") {
2215
- const declarer = args[0];
2216
- const fb = FusionSet.builder(this._name + "-fusion");
2217
- declarer(fb);
2218
- this._fusionSets.push(fb.build());
2219
- return this;
2220
- }
2221
- for (const s of args) {
2222
- if (s === void 0 || s === null) {
2223
- throw new Error("fuse: fusion set must not be null");
2224
- }
2225
- this._fusionSets.push(s);
2226
- }
349
+ outputToHarvest(place2, value) {
350
+ const actual = this.resolve(place2);
351
+ this.requireOutput(actual);
352
+ this._rawOutput.add(actual, value);
2227
353
  return this;
2228
354
  }
355
+ // ==================== ν-name minting (NU-010) ====================
2229
356
  /**
2230
- * Builds the immutable {@link PetriNet}, applying fusion resolution (per
2231
- * **MOD-061**) AFTER all transition/composition accumulation:
2232
- *
2233
- * 1. Detect overlapping fusion sets — a single place declared in two sets
2234
- * is rejected with an `Error`.
2235
- * 2. Build the `non-canonical → canonical` substitution map across all
2236
- * sets, keyed by non-canonical place name (matching the rewriter's
2237
- * Map<string, Place<unknown>> convention — TypeScript Place identity is
2238
- * name-based per `runtime/compiled-net.ts`).
2239
- * 3. Walk every transition through {@link applyFusion} to rewrite arc place
2240
- * references; input arcs colliding on a canonical place merge per
2241
- * [MOD-021] (additive where summable, rejected otherwise).
2242
- * 4. Re-derive the place set from the rewritten transitions plus any
2243
- * caller-declared standalone places, dropping non-canonical members.
2244
- * Caller-declared standalone places that happen to be non-canonical
2245
- * members are also dropped.
2246
- *
2247
- * If no fusion sets were registered, the build is the trivial
2248
- * `new PetriNet(...)` — the fusion machinery has no per-build cost when
2249
- * unused.
2250
- *
2251
- * @throws when two fusion sets share a place
357
+ * @internal Installs the ν-name minter. Wired by the executor at firing time
358
+ * so names minted by {@link freshName} are monotonic across the run and
359
+ * instance-prefixed (spec NU-010, NU-030).
2252
360
  */
2253
- build() {
2254
- const membership = this.resolveSubnetMembership();
2255
- if (this._fusionSets.length === 0) {
2256
- return new PetriNet(
2257
- PETRI_NET_KEY,
2258
- this._name,
2259
- this._places,
2260
- this._transitions,
2261
- membership
2262
- );
2263
- }
2264
- return this.buildWithFusion(membership);
361
+ setFreshNameSupplier(supplier) {
362
+ this._freshNameSupplier = supplier;
2265
363
  }
2266
364
  /**
2267
- * @internal Resolves the per-compose contributions recorded by
2268
- * `composeDirect` into the final node-name → subnet-name membership map per
2269
- * **MOD-026**. A node contributed by exactly one subnet maps to that subnet;
2270
- * a place contributed by two or more subnets is a shared rendezvous place
2271
- * and is omitted (it renders top-level, outside any cluster). Returns an
2272
- * empty map the common case when no subnet was composed directly.
365
+ * Mints a fresh ν-name (the ν-binder primitive spec NU-010).
366
+ *
367
+ * An action calls this on the fork side to create a correlation id, then
368
+ * writes it into the sibling output payloads; a later join correlates those
369
+ * siblings via a {@link import('./match-spec.js').MatchSpec}. Uses the
370
+ * executor-installed minter when present; otherwise falls back to a
371
+ * process-global counter prefixed by the transition name.
2273
372
  */
2274
- resolveSubnetMembership() {
2275
- const resolved = /* @__PURE__ */ new Map();
2276
- for (const [nodeName, owners] of this._subnetContributions) {
2277
- if (owners.size === 1) {
2278
- resolved.set(nodeName, owners.values().next().value);
2279
- }
2280
- }
2281
- return resolved;
373
+ freshName() {
374
+ if (this._freshNameSupplier) return this._freshNameSupplier();
375
+ return nameId(`${this._transitionName}#${GLOBAL_FRESH_NAME_COUNTER++}`);
2282
376
  }
2283
- /**
2284
- * @internal Drops membership entries for places removed by fusion: a
2285
- * non-canonical fused member no longer exists in the net, so its
2286
- * `node-name → subnet` entry would dangle. The surviving canonical place
2287
- * keeps its own entry. Per **MOD-026**.
2288
- */
2289
- static filterFusedMembership(membership, nonCanonicalNames) {
2290
- if (membership.size === 0 || nonCanonicalNames.size === 0) {
2291
- return membership;
2292
- }
2293
- const filtered = /* @__PURE__ */ new Map();
2294
- for (const [key, value] of membership) {
2295
- if (!nonCanonicalNames.has(key)) {
2296
- filtered.set(key, value);
2297
- }
2298
- }
2299
- return filtered;
377
+ // ==================== Structure Info ====================
378
+ /** Returns the transition name. */
379
+ transitionName() {
380
+ return this._transitionName;
2300
381
  }
2301
- /**
2302
- * @internal Fusion-resolution pass per **MOD-061**. Split out from
2303
- * {@link build} so the no-fusion fast path stays trivial.
2304
- */
2305
- buildWithFusion(membership) {
2306
- const ownership = /* @__PURE__ */ new Map();
2307
- for (const set of this._fusionSets) {
2308
- for (const member of set.members) {
2309
- const prior = ownership.get(member.name);
2310
- if (prior !== void 0 && prior !== set) {
2311
- throw new Error(
2312
- `Fusion overlap: place '${member.name}' appears in two fusion sets ('${prior.name}' and '${set.name}'). A place may appear in at most one fusion set (MOD-060).`
2313
- );
2314
- }
2315
- ownership.set(member.name, set);
2316
- }
2317
- }
2318
- const fusionMap = /* @__PURE__ */ new Map();
2319
- const nonCanonicalNames = /* @__PURE__ */ new Set();
2320
- for (const set of this._fusionSets) {
2321
- const canonical = set.canonical;
2322
- for (const nc of set.nonCanonical()) {
2323
- fusionMap.set(nc.name, canonical);
2324
- nonCanonicalNames.add(nc.name);
2325
- }
2326
- }
2327
- const rewrittenTransitions = applyFusion(this._transitions, fusionMap, (canonicalName) => {
2328
- const owner = ownership.get(canonicalName);
2329
- return `Fusion set '${owner !== void 0 ? owner.name : canonicalName}'`;
2330
- });
2331
- const rebuiltPlaces = /* @__PURE__ */ new Set();
2332
- for (const p of this._places) {
2333
- if (!nonCanonicalNames.has(p.name)) {
2334
- rebuiltPlaces.add(p);
2335
- }
2336
- }
2337
- for (const t of rewrittenTransitions) {
2338
- for (const spec of t.inputSpecs) rebuiltPlaces.add(spec.place);
2339
- for (const p of t.outputPlaces()) rebuiltPlaces.add(p);
2340
- for (const inh of t.inhibitors) rebuiltPlaces.add(inh.place);
2341
- for (const r of t.reads) rebuiltPlaces.add(r.place);
2342
- for (const r of t.resets) rebuiltPlaces.add(r.place);
2343
- }
2344
- return new PetriNet(
2345
- PETRI_NET_KEY,
2346
- this._name,
2347
- rebuiltPlaces,
2348
- rewrittenTransitions,
2349
- _PetriNetBuilder.filterFusedMembership(membership, nonCanonicalNames)
2350
- );
382
+ // ==================== Execution Context ====================
383
+ /** Retrieves an execution context object by key. */
384
+ executionContext(key) {
385
+ return this.executionCtx.get(key);
386
+ }
387
+ /** Checks if an execution context object of the given key is present. */
388
+ hasExecutionContext(key) {
389
+ return this.executionCtx.has(key);
390
+ }
391
+ // ==================== Logging ====================
392
+ /** Emits a structured log message into the event store. */
393
+ log(level, message, error) {
394
+ this._logFn?.(level, message, error);
395
+ }
396
+ // ==================== Internal ====================
397
+ /** @internal Used by BitmapNetExecutor to collect outputs after action completion. */
398
+ rawOutput() {
399
+ return this._rawOutput;
2351
400
  }
2352
401
  };
2353
- function rebuildWithAction(t, action) {
2354
- const builder = Transition.builder(t.name).timing(t.timing).priority(t.priority).action(action);
2355
- if (t.placeAlias.size > 0) {
2356
- builder.placeAlias(t.placeAlias);
402
+
403
+ // src/core/token-input.ts
404
+ var TokenInput = class {
405
+ tokens = /* @__PURE__ */ new Map();
406
+ /** Add a token (used by executor when firing transition). */
407
+ add(place2, token) {
408
+ const existing = this.tokens.get(place2.name);
409
+ if (existing) {
410
+ existing.push(token);
411
+ } else {
412
+ this.tokens.set(place2.name, [token]);
413
+ }
414
+ return this;
2357
415
  }
2358
- if (t.inputSpecs.length > 0) {
2359
- builder.inputs(...t.inputSpecs);
416
+ /** Get all tokens for a place. */
417
+ getAll(place2) {
418
+ return this.tokens.get(place2.name) ?? [];
2360
419
  }
2361
- if (t.outputSpec !== null) {
2362
- builder.outputs(t.outputSpec);
420
+ /** Get the first token for a place. Throws if no tokens. */
421
+ get(place2) {
422
+ const list = this.tokens.get(place2.name);
423
+ if (!list || list.length === 0) {
424
+ throw new Error(`No token for place: ${place2.name}`);
425
+ }
426
+ return list[0];
2363
427
  }
2364
- for (const inh of t.inhibitors) {
2365
- builder.inhibitor(inh.place);
428
+ /** Get the first token's value for a place. Throws if no tokens. */
429
+ value(place2) {
430
+ return this.get(place2).value;
2366
431
  }
2367
- for (const r of t.reads) {
2368
- builder.read(r.place);
432
+ /** Get all token values for a place. */
433
+ values(place2) {
434
+ return this.getAll(place2).map((t) => t.value);
2369
435
  }
2370
- for (const r of t.resets) {
2371
- builder.reset(r.place);
436
+ /** Get token count for a place. */
437
+ count(place2) {
438
+ return this.getAll(place2).length;
2372
439
  }
2373
- if (t.matchSpec !== null) {
2374
- builder.match(t.matchSpec);
440
+ /** Check if any tokens exist for a place. */
441
+ has(place2) {
442
+ return this.count(place2) > 0;
2375
443
  }
2376
- return builder.build();
2377
- }
444
+ };
2378
445
 
2379
446
  // src/core/subnet.ts
2380
447
  function closedSubnet(net) {
@@ -2686,6 +753,35 @@ var CompiledNet = class _CompiledNet {
2686
753
  return true;
2687
754
  }
2688
755
  };
756
+ function restartThresholds(compiled) {
757
+ const thresholds = new Float64Array(compiled.placeCount);
758
+ const requirer = new Int32Array(compiled.placeCount).fill(-1);
759
+ const drained = new Uint8Array(compiled.placeCount);
760
+ for (let tid = 0; tid < compiled.transitionCount; tid++) {
761
+ const t = compiled.transition(tid);
762
+ const requires = (pid) => {
763
+ requirer[pid] = requirer[pid] === -1 || requirer[pid] === tid ? tid : -2;
764
+ };
765
+ for (const spec of t.inputSpecs) {
766
+ const pid = compiled.placeId(spec.place);
767
+ thresholds[pid] = Math.max(thresholds[pid], requiredCount(spec));
768
+ requires(pid);
769
+ }
770
+ for (const arc of t.reads) {
771
+ const pid = compiled.placeId(arc.place);
772
+ thresholds[pid] = Math.max(thresholds[pid], 1);
773
+ requires(pid);
774
+ }
775
+ for (const arc of t.resets) drained[compiled.placeId(arc.place)] = 1;
776
+ if (t.matchSpec !== null) {
777
+ for (const key of t.matchSpec.keys) thresholds[compiled.placeId(key.place)] = Infinity;
778
+ }
779
+ }
780
+ for (let pid = 0; pid < compiled.placeCount; pid++) {
781
+ if (requirer[pid] !== -2 && !drained[pid]) thresholds[pid] = 0;
782
+ }
783
+ return thresholds;
784
+ }
2689
785
  function setBit(arr, bit) {
2690
786
  arr[bit >>> WORD_SHIFT] |= 1 << (bit & BIT_MASK);
2691
787
  }
@@ -3246,8 +1342,16 @@ var BitmapNetExecutor = class {
3246
1342
  wakeUpResolve = null;
3247
1343
  // Pre-allocated buffer for fireReadyTransitions() to avoid per-cycle allocation
3248
1344
  readyBuffer = [];
3249
- // Pending reset places for clock-restart detection
3250
- pendingResetPlaces = /* @__PURE__ */ new Set();
1345
+ /**
1346
+ * Per transition, one bit: a firing left it disabled in the intermediate marking while it
1347
+ * was marked enabled (TIME-012), so the next dirty scan that finds it enabled restarts its
1348
+ * clock. Set at fire time, cleared at the end of every scan; `anyRestartPending` lets the
1349
+ * scan skip the clear when nothing was flagged.
1350
+ */
1351
+ restartPendingWords;
1352
+ anyRestartPending = false;
1353
+ /** Per place, the count a firing must leave it at to have disabled nothing through it. */
1354
+ restartThresholds;
3251
1355
  /**
3252
1356
  * Undeclared place names already reported (CORE-072 AC4). Keyed by name — TS
3253
1357
  * Place identity is name-based — so a hot loop warns once, not per token.
@@ -3255,7 +1359,6 @@ var BitmapNetExecutor = class {
3255
1359
  warnedUnknownPlaces = /* @__PURE__ */ new Set();
3256
1360
  /** Transitions already warned for writing several tokens to a place their spec names once (IO-016 AC4). */
3257
1361
  warnedMultiplicity = /* @__PURE__ */ new Set();
3258
- transitionInputPlaceNames;
3259
1362
  running = false;
3260
1363
  draining = false;
3261
1364
  closed = false;
@@ -3280,6 +1383,8 @@ var BitmapNetExecutor = class {
3280
1383
  const dirtyWords = this.compiled.transitionCount + BIT_MASK >>> WORD_SHIFT;
3281
1384
  this.dirtySet = new Uint32Array(dirtyWords);
3282
1385
  this.dirtySnapBuffer = new Uint32Array(dirtyWords);
1386
+ this.restartPendingWords = new Uint32Array(dirtyWords);
1387
+ this.restartThresholds = restartThresholds(this.compiled);
3283
1388
  this.enabledAtMs = new Float64Array(this.compiled.transitionCount);
3284
1389
  this.enabledAtMs.fill(-Infinity);
3285
1390
  this.inFlightFlags = new Uint8Array(this.compiled.transitionCount);
@@ -3304,12 +1409,6 @@ var BitmapNetExecutor = class {
3304
1409
  this.allImmediate = allImm;
3305
1410
  this.allSamePriority = samePrio;
3306
1411
  this.eventStoreEnabled = this.eventStore.isEnabled();
3307
- this.transitionInputPlaceNames = /* @__PURE__ */ new Map();
3308
- for (const t of net.transitions) {
3309
- const names = /* @__PURE__ */ new Set();
3310
- for (const spec of t.inputSpecs) names.add(spec.place.name);
3311
- this.transitionInputPlaceNames.set(t, names);
3312
- }
3313
1412
  for (const [place2, tokens] of initialTokens) {
3314
1413
  if (tokens.length > 0 && this.compiled.tryPlaceId(place2) === void 0) {
3315
1414
  this.warnUnknownPlace(place2, "");
@@ -3467,11 +1566,11 @@ var BitmapNetExecutor = class {
3467
1566
  throw new Error(`Place ${envPlace.place.name} is not registered as an environment place`);
3468
1567
  }
3469
1568
  if (this.closed || this.draining) return false;
3470
- return new Promise((resolve2, reject) => {
1569
+ return new Promise((resolve, reject) => {
3471
1570
  this.externalQueue.push({
3472
1571
  place: envPlace.place,
3473
1572
  token,
3474
- resolve: resolve2,
1573
+ resolve,
3475
1574
  reject
3476
1575
  });
3477
1576
  this.wakeUp();
@@ -3544,7 +1643,7 @@ var BitmapNetExecutor = class {
3544
1643
  this.enabledFlags[tid] = 0;
3545
1644
  this.enabledTransitionCount--;
3546
1645
  this.enabledAtMs[tid] = -Infinity;
3547
- } else if (canNow && wasEnabled && this.hasInputFromResetPlace(this.compiled.transition(tid))) {
1646
+ } else if (canNow && wasEnabled && this.anyRestartPending && (this.restartPendingWords[w] & 1 << bit) !== 0) {
3548
1647
  this.enabledAtMs[tid] = nowMs;
3549
1648
  this.emitEvent({
3550
1649
  type: "transition-clock-restarted",
@@ -3554,7 +1653,10 @@ var BitmapNetExecutor = class {
3554
1653
  }
3555
1654
  }
3556
1655
  }
3557
- this.pendingResetPlaces.clear();
1656
+ if (this.anyRestartPending) {
1657
+ this.restartPendingWords.fill(0);
1658
+ this.anyRestartPending = false;
1659
+ }
3558
1660
  }
3559
1661
  /**
3560
1662
  * Checks all enabled transitions with finite deadlines. If a transition has been
@@ -3608,14 +1710,26 @@ var BitmapNetExecutor = class {
3608
1710
  }
3609
1711
  return true;
3610
1712
  }
3611
- hasInputFromResetPlace(t) {
3612
- if (this.pendingResetPlaces.size === 0) return false;
3613
- const inputNames = this.transitionInputPlaceNames.get(t);
3614
- if (!inputNames) return false;
3615
- for (const name of this.pendingResetPlaces) {
3616
- if (inputNames.has(name)) return true;
1713
+ /**
1714
+ * Flags each other transition that firing `tid` has just disabled through `pid`
1715
+ * (TIME-012). Called from {@link updateBitmapAfterConsumption} on the intermediate marking
1716
+ * M - Pre(t) (inputs consumed and resets drained, outputs not yet deposited), and only for
1717
+ * a place the firing left below its restart threshold. Removing tokens never trips an
1718
+ * inhibitor arc, so the bit tests reject every other kind of neighbour cheaply.
1719
+ */
1720
+ flagIntermediateDisablements(tid, pid) {
1721
+ const affected = this.compiled.affectedTransitions(pid);
1722
+ for (let j = 0; j < affected.length; j++) {
1723
+ const other = affected[j];
1724
+ if (other === tid || !this.enabledFlags[other] || this.inFlightFlags[other]) continue;
1725
+ const w = other >>> WORD_SHIFT;
1726
+ const mask = 1 << (other & BIT_MASK);
1727
+ if ((this.restartPendingWords[w] & mask) !== 0) continue;
1728
+ if (!this.canEnable(other, this.markingBitmap)) {
1729
+ this.restartPendingWords[w] |= mask;
1730
+ this.anyRestartPending = true;
1731
+ }
3617
1732
  }
3618
- return false;
3619
1733
  }
3620
1734
  // ======================== Firing ========================
3621
1735
  fireReadyTransitions(nowMs) {
@@ -3779,7 +1893,6 @@ var BitmapNetExecutor = class {
3779
1893
  }
3780
1894
  for (const arc of t.resets) {
3781
1895
  const removed = this.marking.removeAll(arc.place);
3782
- this.pendingResetPlaces.add(arc.place.name);
3783
1896
  for (const token of removed) {
3784
1897
  consumed.push(token);
3785
1898
  this.emitEvent({
@@ -3882,10 +1995,12 @@ var BitmapNetExecutor = class {
3882
1995
  const pids = this.compiled.consumptionPlaceIds(tid);
3883
1996
  for (const pid of pids) {
3884
1997
  const place2 = this.compiled.place(pid);
3885
- if (!this.marking.hasTokens(place2)) {
1998
+ const left = this.marking.tokenCount(place2);
1999
+ if (left === 0) {
3886
2000
  clearBit(this.markingBitmap, pid);
3887
2001
  }
3888
2002
  this.markDirty(pid);
2003
+ if (left < this.restartThresholds[pid]) this.flagIntermediateDisablements(tid, pid);
3889
2004
  }
3890
2005
  }
3891
2006
  // ======================== Completion Processing ========================
@@ -4024,8 +2139,8 @@ var BitmapNetExecutor = class {
4024
2139
  if (!this.closed) {
4025
2140
  const timerMs = this.millisUntilNextTimedTransition();
4026
2141
  if (timerMs === 0 && promises.length === 0) return;
4027
- promises.push(new Promise((resolve2) => {
4028
- this.wakeUpResolve = resolve2;
2142
+ promises.push(new Promise((resolve) => {
2143
+ this.wakeUpResolve = resolve;
4029
2144
  }));
4030
2145
  if (timerMs > 0 && timerMs < Infinity) {
4031
2146
  promises.push(new Promise((r) => setTimeout(r, timerMs)));
@@ -4232,6 +2347,10 @@ var PrecompiledNet = class _PrecompiledNet {
4232
2347
  simpleOutputPlaceId;
4233
2348
  // ==================== Input Precomputation ====================
4234
2349
  inputPlaceCount;
2350
+ /**
2351
+ * @deprecated Not read by the executor. Kept so the exported shape of `PrecompiledNet`
2352
+ * stays stable; scheduled for removal in the next major release.
2353
+ */
4235
2354
  inputPlaceMaskWords;
4236
2355
  // ==================== Reverse Index ====================
4237
2356
  placeToTransitions;
@@ -4591,9 +2710,17 @@ var PrecompiledNetExecutor = class {
4591
2710
  inFlightResolves;
4592
2711
  inFlightErrors;
4593
2712
  inFlightCount = 0;
4594
- // ==================== Reset-Clock Detection ====================
4595
- pendingResetWords;
4596
- hasPendingResets = false;
2713
+ // ==================== Intermediate-Disablement Clock Restart ====================
2714
+ /**
2715
+ * Per transition, one bit: a firing left it disabled in the intermediate marking while it
2716
+ * was marked enabled (TIME-012), so the next dirty scan that finds it enabled restarts its
2717
+ * clock. Set at fire time, cleared at the end of every scan; `anyRestartPending` lets the
2718
+ * scan skip the clear when nothing was flagged.
2719
+ */
2720
+ restartPendingWords;
2721
+ anyRestartPending = false;
2722
+ /** Per place, the count a firing must leave it at to have disabled nothing through it. */
2723
+ restartThresholds;
4597
2724
  // ==================== Queues ====================
4598
2725
  completionQueue = [];
4599
2726
  externalQueue = [];
@@ -4661,7 +2788,8 @@ var PrecompiledNetExecutor = class {
4661
2788
  this.inFlightStartMs = new Float64Array(tc);
4662
2789
  this.inFlightResolves = new Array(tc).fill(null);
4663
2790
  this.inFlightErrors = new Array(tc).fill(null);
4664
- this.pendingResetWords = new Uint32Array(wc);
2791
+ this.restartPendingWords = new Uint32Array(this.transitionWords);
2792
+ this.restartThresholds = restartThresholds(prog.compiled);
4665
2793
  this.markingSnapBuffer = new Uint32Array(wc);
4666
2794
  this.firingSnapBuffer = new Uint32Array(wc);
4667
2795
  this.initMatchCaches();
@@ -4835,11 +2963,11 @@ var PrecompiledNetExecutor = class {
4835
2963
  throw new Error(`Place ${envPlace.place.name} is not registered as an environment place`);
4836
2964
  }
4837
2965
  if (this.closed || this.draining) return false;
4838
- return new Promise((resolve2, reject) => {
2966
+ return new Promise((resolve, reject) => {
4839
2967
  this.externalQueue.push({
4840
2968
  place: envPlace.place,
4841
2969
  token,
4842
- resolve: resolve2,
2970
+ resolve,
4843
2971
  reject
4844
2972
  });
4845
2973
  this.wakeUp();
@@ -4914,7 +3042,7 @@ var PrecompiledNetExecutor = class {
4914
3042
  this.enabledFlags[tid] = 0;
4915
3043
  this.enabledTransitionCount--;
4916
3044
  this.enabledAtMs[tid] = -Infinity;
4917
- } else if (canNow && wasEnabled && this.hasInputFromResetPlace(tid)) {
3045
+ } else if (canNow && wasEnabled && this.anyRestartPending && (this.restartPendingWords[w] & 1 << bit) !== 0) {
4918
3046
  this.enabledAtMs[tid] = nowMs;
4919
3047
  this.emitEvent({
4920
3048
  type: "transition-clock-restarted",
@@ -4924,9 +3052,9 @@ var PrecompiledNetExecutor = class {
4924
3052
  }
4925
3053
  }
4926
3054
  }
4927
- if (this.hasPendingResets) {
4928
- this.pendingResetWords.fill(0);
4929
- this.hasPendingResets = false;
3055
+ if (this.anyRestartPending) {
3056
+ this.restartPendingWords.fill(0);
3057
+ this.anyRestartPending = false;
4930
3058
  }
4931
3059
  }
4932
3060
  enforceDeadlines(nowMs) {
@@ -4988,13 +3116,25 @@ var PrecompiledNetExecutor = class {
4988
3116
  }
4989
3117
  return null;
4990
3118
  }
4991
- hasInputFromResetPlace(tid) {
4992
- if (!this.hasPendingResets) return false;
4993
- const inputMask = this.program.inputPlaceMaskWords[tid];
4994
- for (let w = 0; w < inputMask.length; w++) {
4995
- if ((inputMask[w] & this.pendingResetWords[w]) !== 0) return true;
3119
+ /**
3120
+ * Flags each other transition that firing `tid` has just disabled through `pid`
3121
+ * (TIME-012). Called from {@link updateBitmapAfterConsumption} on the intermediate marking,
3122
+ * only for a place the firing left below its restart threshold. Mirrors the bitmap
3123
+ * executor, which gives the rationale.
3124
+ */
3125
+ flagIntermediateDisablements(tid, pid) {
3126
+ const affected = this.program.placeToTransitions[pid];
3127
+ for (let j = 0; j < affected.length; j++) {
3128
+ const other = affected[j];
3129
+ if (other === tid || !this.enabledFlags[other] || this.inFlightFlags[other]) continue;
3130
+ const w = other >>> WORD_SHIFT;
3131
+ const mask = 1 << (other & BIT_MASK);
3132
+ if ((this.restartPendingWords[w] & mask) !== 0) continue;
3133
+ if (!this.canEnable(other, this.markingBitmap)) {
3134
+ this.restartPendingWords[w] |= mask;
3135
+ this.anyRestartPending = true;
3136
+ }
4996
3137
  }
4997
- return false;
4998
3138
  }
4999
3139
  // ======================== Firing ========================
5000
3140
  fireReadyTransitions(nowMs) {
@@ -5200,8 +3340,6 @@ var PrecompiledNetExecutor = class {
5200
3340
  const pid = ops[pc++];
5201
3341
  const place2 = prog.places[pid];
5202
3342
  const tokens = this.tokenQueues[pid].splice(0);
5203
- this.pendingResetWords[pid >>> WORD_SHIFT] |= 1 << (pid & BIT_MASK);
5204
- this.hasPendingResets = true;
5205
3343
  for (const token of tokens) {
5206
3344
  consumed.push(token);
5207
3345
  this.emitEvent({
@@ -5350,8 +3488,6 @@ var PrecompiledNetExecutor = class {
5350
3488
  for (const arc of t.resets) {
5351
3489
  const pid = prog.compiled.placeId(arc.place);
5352
3490
  const tokens = this.tokenQueues[pid].splice(0);
5353
- this.pendingResetWords[pid >>> WORD_SHIFT] |= 1 << (pid & BIT_MASK);
5354
- this.hasPendingResets = true;
5355
3491
  for (const token of tokens) {
5356
3492
  consumed.push(token);
5357
3493
  this.emitEvent({
@@ -5383,10 +3519,12 @@ var PrecompiledNetExecutor = class {
5383
3519
  const pids = this.program.consumptionPlaceIds[tid];
5384
3520
  for (let i = 0; i < pids.length; i++) {
5385
3521
  const pid = pids[i];
5386
- if (this.tokenQueues[pid].length === 0) {
3522
+ const left = this.tokenQueues[pid].length;
3523
+ if (left === 0) {
5387
3524
  this.clearMarkingBit(pid);
5388
3525
  }
5389
3526
  this.markDirty(pid);
3527
+ if (left < this.restartThresholds[pid]) this.flagIntermediateDisablements(tid, pid);
5390
3528
  }
5391
3529
  }
5392
3530
  // ======================== Completion Processing ========================
@@ -5562,8 +3700,8 @@ var PrecompiledNetExecutor = class {
5562
3700
  if (!this.closed) {
5563
3701
  const timerMs = this.millisUntilNextTimedTransition();
5564
3702
  if (timerMs === 0 && promises.length === 0) return;
5565
- promises.push(new Promise((resolve2) => {
5566
- this.wakeUpResolve = resolve2;
3703
+ promises.push(new Promise((resolve) => {
3704
+ this.wakeUpResolve = resolve;
5567
3705
  }));
5568
3706
  if (timerMs > 0 && timerMs < Infinity) {
5569
3707
  promises.push(new Promise((r) => setTimeout(r, timerMs)));