libpetri 2.7.1 → 2.9.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
@@ -12,11 +12,17 @@ import {
12
12
  timeoutPlace,
13
13
  xor,
14
14
  xorPlaces
15
- } from "./chunk-ETNHEAS6.js";
15
+ } from "./chunk-M3KT7BFO.js";
16
16
  import {
17
17
  eventTransitionName,
18
18
  isFailureEvent
19
19
  } from "./chunk-SXK2Z45Z.js";
20
+ import {
21
+ keyForPlace,
22
+ matchCorrelates,
23
+ matchKey,
24
+ matchSpec
25
+ } from "./chunk-E3ZWB645.js";
20
26
  import {
21
27
  MAX_DURATION_MS,
22
28
  deadline,
@@ -131,6 +137,11 @@ function consumptionCount(spec, available) {
131
137
  }
132
138
  }
133
139
 
140
+ // src/core/name.ts
141
+ function nameId(value) {
142
+ return value;
143
+ }
144
+
134
145
  // src/core/transition-action.ts
135
146
  function passthrough() {
136
147
  return PASSTHROUGH;
@@ -204,6 +215,7 @@ function withTimeout(action, timeoutMs, timeoutPlace2, timeoutValue) {
204
215
 
205
216
  // src/core/transition-context.ts
206
217
  var EMPTY_ALIAS = /* @__PURE__ */ new Map();
218
+ var GLOBAL_FRESH_NAME_COUNTER = 0;
207
219
  var TransitionContext = class {
208
220
  rawInput;
209
221
  _rawOutput;
@@ -217,6 +229,7 @@ var TransitionContext = class {
217
229
  executionCtx;
218
230
  _logFn;
219
231
  placeAlias;
232
+ _freshNameSupplier;
220
233
  constructor(transitionName, rawInput, rawOutput, inputPlaces, readPlaces, outputPlaces, executionContext, logFn, placeAlias) {
221
234
  this._transitionName = transitionName;
222
235
  this.rawInput = rawInput;
@@ -358,6 +371,28 @@ var TransitionContext = class {
358
371
  );
359
372
  }
360
373
  }
374
+ // ==================== ν-name minting (NU-010) ====================
375
+ /**
376
+ * @internal Installs the ν-name minter. Wired by the executor at firing time
377
+ * so names minted by {@link freshName} are monotonic across the run and
378
+ * instance-prefixed (spec NU-010, NU-030).
379
+ */
380
+ setFreshNameSupplier(supplier) {
381
+ this._freshNameSupplier = supplier;
382
+ }
383
+ /**
384
+ * Mints a fresh ν-name (the ν-binder primitive — spec NU-010).
385
+ *
386
+ * An action calls this on the fork side to create a correlation id, then
387
+ * writes it into the sibling output payloads; a later join correlates those
388
+ * siblings via a {@link import('./match-spec.js').MatchSpec}. Uses the
389
+ * executor-installed minter when present; otherwise falls back to a
390
+ * process-global counter prefixed by the transition name.
391
+ */
392
+ freshName() {
393
+ if (this._freshNameSupplier) return this._freshNameSupplier();
394
+ return nameId(`${this._transitionName}#${GLOBAL_FRESH_NAME_COUNTER++}`);
395
+ }
361
396
  // ==================== Structure Info ====================
362
397
  /** Returns the transition name. */
363
398
  transitionName() {
@@ -472,6 +507,11 @@ var Transition = class {
472
507
  actionTimeout;
473
508
  action;
474
509
  priority;
510
+ /**
511
+ * ν-net join correlation: a subset of `inputSpecs` that must be correlated by
512
+ * name equality on firing (spec NU-020). `null` for ordinary transitions.
513
+ */
514
+ matchSpec;
475
515
  /**
476
516
  * Per-transition **declared → actual** place correspondence (per
477
517
  * **MOD-031**), keyed by the author-original declared place **name** →
@@ -488,7 +528,7 @@ var Transition = class {
488
528
  _readPlaces;
489
529
  _outputPlaces;
490
530
  /** @internal Use {@link Transition.builder} to create instances. */
491
- constructor(key, name, inputSpecs, outputSpec, inhibitors, reads, resets, timing, action, priority, placeAlias = EMPTY_PLACE_ALIAS) {
531
+ constructor(key, name, inputSpecs, outputSpec, inhibitors, reads, resets, timing, action, priority, placeAlias = EMPTY_PLACE_ALIAS, matchSpec2 = null) {
492
532
  if (key !== TRANSITION_KEY) throw new Error("Use Transition.builder() to create instances");
493
533
  this.name = name;
494
534
  this.inputSpecs = inputSpecs;
@@ -501,6 +541,7 @@ var Transition = class {
501
541
  this.action = action;
502
542
  this.priority = priority;
503
543
  this.placeAlias = placeAlias.size === 0 ? EMPTY_PLACE_ALIAS : placeAlias;
544
+ this.matchSpec = matchSpec2;
504
545
  const inputPlaces = /* @__PURE__ */ new Set();
505
546
  for (const spec of inputSpecs) {
506
547
  inputPlaces.add(spec.place);
@@ -553,6 +594,7 @@ var TransitionBuilder = class {
553
594
  _action = passthrough();
554
595
  _priority = 0;
555
596
  _placeAlias = EMPTY_PLACE_ALIAS;
597
+ _matchSpec = null;
556
598
  constructor(name) {
557
599
  this._name = name;
558
600
  }
@@ -617,6 +659,15 @@ var TransitionBuilder = class {
617
659
  this._priority = priority;
618
660
  return this;
619
661
  }
662
+ /**
663
+ * Sets the ν-net join correlation spec: the named input places must be
664
+ * correlated by name equality on firing (spec NU-020). Every place referenced
665
+ * by the spec must also be declared as an input.
666
+ */
667
+ match(spec) {
668
+ this._matchSpec = spec;
669
+ return this;
670
+ }
620
671
  /**
621
672
  * Sets the per-transition declared→actual place correspondence (per
622
673
  * **MOD-031**). Populated by the subnet rewriter during the compose-time
@@ -638,6 +689,16 @@ var TransitionBuilder = class {
638
689
  }
639
690
  }
640
691
  }
692
+ if (this._matchSpec !== null) {
693
+ const inputPlaceNames = new Set(this._inputSpecs.map((s) => s.place.name));
694
+ for (const k of this._matchSpec.keys) {
695
+ if (!inputPlaceNames.has(k.place.name)) {
696
+ throw new Error(
697
+ `Transition '${this._name}': MatchSpec correlates non-input place '${k.place.name}'`
698
+ );
699
+ }
700
+ }
701
+ }
641
702
  return new Transition(
642
703
  TRANSITION_KEY,
643
704
  this._name,
@@ -649,7 +710,8 @@ var TransitionBuilder = class {
649
710
  this._timing,
650
711
  this._action,
651
712
  this._priority,
652
- this._placeAlias
713
+ this._placeAlias,
714
+ this._matchSpec
653
715
  );
654
716
  }
655
717
  };
@@ -967,6 +1029,14 @@ function rebuildWithName(t, name, remap) {
967
1029
  for (let i = 0; i < t.resets.length; i++) {
968
1030
  builder.reset(rewriteReset(t.resets[i], remap).place);
969
1031
  }
1032
+ if (t.matchSpec !== null) {
1033
+ builder.match({
1034
+ keys: t.matchSpec.keys.map((k) => ({
1035
+ place: remap.get(k.place.name) ?? k.place,
1036
+ key: k.key
1037
+ }))
1038
+ });
1039
+ }
970
1040
  return builder.build();
971
1041
  }
972
1042
  function buildPlaceAlias(t, remap) {
@@ -2298,7 +2368,8 @@ var Marking = class _Marking {
2298
2368
  for (let i = 0; i < queue.length; i++) {
2299
2369
  const token = queue[i];
2300
2370
  if (spec.guard(token.value)) {
2301
- queue.splice(i, 1);
2371
+ if (i === 0) queue.shift();
2372
+ else queue.splice(i, 1);
2302
2373
  return token;
2303
2374
  }
2304
2375
  }
@@ -2387,6 +2458,10 @@ var CompiledNet = class _CompiledNet {
2387
2458
  // Cardinality and guard flags
2388
2459
  _cardinalityChecks;
2389
2460
  _hasGuards;
2461
+ // ν-net join flag: the transition carries a MatchSpec (NU-020). Precomputed
2462
+ // so the hot enablement loop can skip the match check on non-ν transitions
2463
+ // (zero-cost gating), mirroring `_hasGuards`.
2464
+ _hasMatch;
2390
2465
  constructor(net) {
2391
2466
  this.net = net;
2392
2467
  const allPlacesSet = /* @__PURE__ */ new Map();
@@ -2418,12 +2493,14 @@ var CompiledNet = class _CompiledNet {
2418
2493
  this._consumptionPlaceIds = new Array(this.transitionCount);
2419
2494
  this._cardinalityChecks = new Array(this.transitionCount).fill(null);
2420
2495
  this._hasGuards = new Array(this.transitionCount).fill(false);
2496
+ this._hasMatch = new Array(this.transitionCount).fill(false);
2421
2497
  const placeToTransitionsList = new Array(this.placeCount);
2422
2498
  for (let i = 0; i < this.placeCount; i++) {
2423
2499
  placeToTransitionsList[i] = [];
2424
2500
  }
2425
2501
  for (let tid = 0; tid < this.transitionCount; tid++) {
2426
2502
  const t = this._transitionsById[tid];
2503
+ this._hasMatch[tid] = t.matchSpec !== null;
2427
2504
  const needs = new Uint32Array(this.wordCount);
2428
2505
  const inhibitors = new Uint32Array(this.wordCount);
2429
2506
  let needsCardinality = false;
@@ -2505,6 +2582,9 @@ var CompiledNet = class _CompiledNet {
2505
2582
  hasGuards(tid) {
2506
2583
  return this._hasGuards[tid];
2507
2584
  }
2585
+ hasMatch(tid) {
2586
+ return this._hasMatch[tid];
2587
+ }
2508
2588
  // ==================== Enablement Check ====================
2509
2589
  /**
2510
2590
  * Two-phase bitmap enablement check for a transition:
@@ -2609,6 +2689,224 @@ function inMemoryEventStore() {
2609
2689
  return new InMemoryEventStore();
2610
2690
  }
2611
2691
 
2692
+ // src/runtime/match-engine.ts
2693
+ function selectMatchName(perPlace, requireds) {
2694
+ if (perPlace.length === 0) return null;
2695
+ let seed = 0;
2696
+ for (let i = 1; i < perPlace.length; i++) {
2697
+ if (perPlace[i].size < perPlace[seed].size) seed = i;
2698
+ }
2699
+ let bestName = null;
2700
+ let bestTs = 0;
2701
+ for (const [name, stat] of perPlace[seed]) {
2702
+ if (stat.count < requireds[seed]) continue;
2703
+ let repTs = Number.MAX_SAFE_INTEGER;
2704
+ let satisfied = true;
2705
+ for (let j = 0; j < perPlace.length; j++) {
2706
+ const s = perPlace[j].get(name);
2707
+ if (s === void 0 || s.count < requireds[j]) {
2708
+ satisfied = false;
2709
+ break;
2710
+ }
2711
+ repTs = Math.min(repTs, s.minCreatedAt);
2712
+ }
2713
+ if (!satisfied) continue;
2714
+ const take = bestName === null || repTs < bestTs || repTs === bestTs && name < bestName;
2715
+ if (take) {
2716
+ bestName = name;
2717
+ bestTs = repTs;
2718
+ }
2719
+ }
2720
+ return bestName;
2721
+ }
2722
+ function buildNameIndex(tokens, key, guard) {
2723
+ const index = /* @__PURE__ */ new Map();
2724
+ for (const token of tokens) {
2725
+ if (guard && !guard(token.value)) continue;
2726
+ const name = key(token.value);
2727
+ if (name === void 0 || name === null) continue;
2728
+ const ts = token.createdAt;
2729
+ const prev = index.get(name);
2730
+ if (prev) {
2731
+ prev.count++;
2732
+ if (ts < prev.minCreatedAt) prev.minCreatedAt = ts;
2733
+ } else {
2734
+ index.set(name, { count: 1, minCreatedAt: ts });
2735
+ }
2736
+ }
2737
+ return index;
2738
+ }
2739
+ function requiredFor(t, placeName) {
2740
+ for (const inSpec of t.inputSpecs) {
2741
+ if (inSpec.place.name === placeName) {
2742
+ if (inSpec.type === "exactly") return inSpec.count;
2743
+ if (inSpec.type === "at-least") return inSpec.minimum;
2744
+ return 1;
2745
+ }
2746
+ }
2747
+ return 1;
2748
+ }
2749
+ function findBinding(t, getTokens) {
2750
+ const ms = t.matchSpec;
2751
+ if (!ms) return null;
2752
+ const perPlace = [];
2753
+ const requireds = [];
2754
+ for (const k of ms.keys) {
2755
+ perPlace.push(buildNameIndex(getTokens(k.place), k.key, guardFor(t, k.place.name)));
2756
+ requireds.push(requiredFor(t, k.place.name));
2757
+ }
2758
+ return selectMatchName(perPlace, requireds);
2759
+ }
2760
+ function guardFor(t, placeName) {
2761
+ for (const inSpec of t.inputSpecs) {
2762
+ if (inSpec.place.name === placeName) return inSpec.guard;
2763
+ }
2764
+ return void 0;
2765
+ }
2766
+ var MinQueue = class {
2767
+ inStack = [];
2768
+ inMin = [];
2769
+ outStack = [];
2770
+ outMin = [];
2771
+ get size() {
2772
+ return this.inStack.length + this.outStack.length;
2773
+ }
2774
+ pushBack(v) {
2775
+ this.inStack.push(v);
2776
+ const top = this.inMin.length;
2777
+ this.inMin.push(top ? Math.min(this.inMin[top - 1], v) : v);
2778
+ }
2779
+ popFront() {
2780
+ if (this.outStack.length === 0) {
2781
+ while (this.inStack.length) {
2782
+ const v = this.inStack.pop();
2783
+ this.inMin.pop();
2784
+ this.outStack.push(v);
2785
+ const top = this.outMin.length;
2786
+ this.outMin.push(top ? Math.min(this.outMin[top - 1], v) : v);
2787
+ }
2788
+ }
2789
+ this.outStack.pop();
2790
+ this.outMin.pop();
2791
+ }
2792
+ min() {
2793
+ const a = this.inMin.length ? this.inMin[this.inMin.length - 1] : Infinity;
2794
+ const b = this.outMin.length ? this.outMin[this.outMin.length - 1] : Infinity;
2795
+ return a < b ? a : b;
2796
+ }
2797
+ };
2798
+ var ReadyHeap = class {
2799
+ a = [];
2800
+ get size() {
2801
+ return this.a.length;
2802
+ }
2803
+ peek() {
2804
+ return this.a[0];
2805
+ }
2806
+ less(x, y) {
2807
+ return x.ts < y.ts || x.ts === y.ts && x.name < y.name;
2808
+ }
2809
+ push(e) {
2810
+ const a = this.a;
2811
+ a.push(e);
2812
+ let i = a.length - 1;
2813
+ while (i > 0) {
2814
+ const p = i - 1 >> 1;
2815
+ if (this.less(a[i], a[p])) {
2816
+ const tmp = a[i];
2817
+ a[i] = a[p];
2818
+ a[p] = tmp;
2819
+ i = p;
2820
+ } else break;
2821
+ }
2822
+ }
2823
+ pop() {
2824
+ const a = this.a;
2825
+ const last = a.pop();
2826
+ if (a.length === 0) return;
2827
+ a[0] = last;
2828
+ let i = 0;
2829
+ const n = a.length;
2830
+ for (; ; ) {
2831
+ const l = 2 * i + 1;
2832
+ const r = 2 * i + 2;
2833
+ let m = i;
2834
+ if (l < n && this.less(a[l], a[m])) m = l;
2835
+ if (r < n && this.less(a[r], a[m])) m = r;
2836
+ if (m === i) break;
2837
+ const tmp = a[i];
2838
+ a[i] = a[m];
2839
+ a[m] = tmp;
2840
+ i = m;
2841
+ }
2842
+ }
2843
+ };
2844
+ var IncrementalMatcher = class {
2845
+ constructor(requireds) {
2846
+ this.requireds = requireds;
2847
+ this.ts = requireds.map(() => /* @__PURE__ */ new Map());
2848
+ }
2849
+ ts;
2850
+ heap = new ReadyHeap();
2851
+ ready = /* @__PURE__ */ new Map();
2852
+ /** Add one (already guard-passing) token carrying `name` at `createdAt` to input `i`. */
2853
+ add(i, name, createdAt) {
2854
+ let q = this.ts[i].get(name);
2855
+ if (!q) {
2856
+ q = new MinQueue();
2857
+ this.ts[i].set(name, q);
2858
+ }
2859
+ q.pushBack(createdAt);
2860
+ this.refresh(name);
2861
+ this.cleanTop();
2862
+ }
2863
+ /** Remove the `required` earliest-inserted tokens of `name` from every input (NU-020). */
2864
+ consume(name) {
2865
+ for (let i = 0; i < this.requireds.length; i++) {
2866
+ const q = this.ts[i].get(name);
2867
+ if (q) {
2868
+ for (let r = 0; r < this.requireds[i]; r++) q.popFront();
2869
+ if (q.size === 0) this.ts[i].delete(name);
2870
+ }
2871
+ }
2872
+ this.refresh(name);
2873
+ this.cleanTop();
2874
+ }
2875
+ /** The name `selectMatchName` would pick, or `null`. O(1) read. */
2876
+ best() {
2877
+ const top = this.heap.peek();
2878
+ return top ? top.name : null;
2879
+ }
2880
+ repTs(name) {
2881
+ let rep = Infinity;
2882
+ for (let i = 0; i < this.requireds.length; i++) {
2883
+ const q = this.ts[i].get(name);
2884
+ if (!q || q.size < this.requireds[i]) return null;
2885
+ const m = q.min();
2886
+ if (m < rep) rep = m;
2887
+ }
2888
+ return rep;
2889
+ }
2890
+ refresh(name) {
2891
+ const rep = this.repTs(name);
2892
+ if (rep !== null) {
2893
+ if (this.ready.get(name) !== rep) {
2894
+ this.ready.set(name, rep);
2895
+ this.heap.push({ ts: rep, name });
2896
+ }
2897
+ } else {
2898
+ this.ready.delete(name);
2899
+ }
2900
+ }
2901
+ cleanTop() {
2902
+ for (; ; ) {
2903
+ const top = this.heap.peek();
2904
+ if (top && this.ready.get(top.name) !== top.ts) this.heap.pop();
2905
+ else break;
2906
+ }
2907
+ }
2908
+ };
2909
+
2612
2910
  // src/runtime/out-violation-error.ts
2613
2911
  var OutViolationError = class extends Error {
2614
2912
  constructor(message) {
@@ -2682,6 +2980,18 @@ function produceTimeoutOutput(context, timeoutChild) {
2682
2980
  var BitmapNetExecutor = class {
2683
2981
  compiled;
2684
2982
  marking;
2983
+ /** Monotonic source for ν-name minting (ctx.freshName(), NU-010). */
2984
+ freshNameCounter = 0;
2985
+ /**
2986
+ * ν-net incremental match caches (NU-020): per matched transition, an
2987
+ * {@link IncrementalMatcher} kept in lockstep with the marking when the
2988
+ * transition is fast-path eligible (every correlated input is `one`/`exactly`,
2989
+ * consumed by no other transition, never reset), else `null` → fall back to
2990
+ * the O(n) rebuild {@link findBinding}. Turns a draining matched join from
2991
+ * O(n²) into O(n log n). Mirrors the precompiled executor and the Rust backends.
2992
+ */
2993
+ matchCaches = [];
2994
+ placeMatchTargets = [];
2685
2995
  eventStore;
2686
2996
  environmentPlaces;
2687
2997
  hasEnvironmentPlaces;
@@ -2776,6 +3086,90 @@ var BitmapNetExecutor = class {
2776
3086
  for (const spec of t.inputSpecs) names.add(spec.place.name);
2777
3087
  this.transitionInputPlaceNames.set(t, names);
2778
3088
  }
3089
+ this.initMatchCaches();
3090
+ }
3091
+ /**
3092
+ * Builds the ν-net incremental match caches (NU-020). A matched join is
3093
+ * fast-path eligible only when every correlated input is `one`/`exactly`, is
3094
+ * consumed by no other transition, and is never reset — so the cache can never
3095
+ * desync from the marking. Mirrors the precompiled executor.
3096
+ */
3097
+ initMatchCaches() {
3098
+ const compiled = this.compiled;
3099
+ const tc = compiled.transitionCount;
3100
+ const pc = compiled.placeCount;
3101
+ this.matchCaches = new Array(tc).fill(null);
3102
+ this.placeMatchTargets = Array.from({ length: pc }, () => []);
3103
+ let anyMatch = false;
3104
+ for (let tid = 0; tid < tc; tid++) {
3105
+ if (compiled.hasMatch(tid)) {
3106
+ anyMatch = true;
3107
+ break;
3108
+ }
3109
+ }
3110
+ if (!anyMatch) return;
3111
+ const inputConsumers = Array.from({ length: pc }, () => []);
3112
+ const resetTarget = new Array(pc).fill(false);
3113
+ for (let tid = 0; tid < tc; tid++) {
3114
+ const t = compiled.transition(tid);
3115
+ for (const spec of t.inputSpecs) inputConsumers[compiled.placeId(spec.place)].push(tid);
3116
+ for (const arc of t.resets) resetTarget[compiled.placeId(arc.place)] = true;
3117
+ }
3118
+ for (let tid = 0; tid < tc; tid++) {
3119
+ if (!compiled.hasMatch(tid)) continue;
3120
+ const t = compiled.transition(tid);
3121
+ const ms = t.matchSpec;
3122
+ if (!ms) continue;
3123
+ const requireds = [];
3124
+ let eligible = true;
3125
+ for (const mk of ms.keys) {
3126
+ const pid = compiled.placeId(mk.place);
3127
+ const spec = t.inputSpecs.find((s) => s.place.name === mk.place.name);
3128
+ let required;
3129
+ if (spec?.type === "one") required = 1;
3130
+ else if (spec?.type === "exactly") required = spec.count;
3131
+ else {
3132
+ eligible = false;
3133
+ break;
3134
+ }
3135
+ const cons = inputConsumers[pid];
3136
+ if (resetTarget[pid] || cons.length !== 1 || cons[0] !== tid) {
3137
+ eligible = false;
3138
+ break;
3139
+ }
3140
+ requireds.push(required);
3141
+ }
3142
+ if (!eligible) continue;
3143
+ const matcher = new IncrementalMatcher(requireds);
3144
+ for (let keyIdx = 0; keyIdx < ms.keys.length; keyIdx++) {
3145
+ const mk = ms.keys[keyIdx];
3146
+ const pid = compiled.placeId(mk.place);
3147
+ const guard = t.inputSpecs.find((s) => s.place.name === mk.place.name)?.guard;
3148
+ for (const token of this.marking.peekTokens(mk.place)) {
3149
+ if (guard && !guard(token.value)) continue;
3150
+ const name = mk.key(token.value);
3151
+ if (name !== void 0 && name !== null) matcher.add(keyIdx, name, token.createdAt);
3152
+ }
3153
+ this.placeMatchTargets[pid].push([tid, keyIdx]);
3154
+ }
3155
+ this.matchCaches[tid] = matcher;
3156
+ }
3157
+ }
3158
+ /** Mirror a token added to correlated input `pid` into every fast-path matcher. */
3159
+ cacheAddToken(pid, token) {
3160
+ const targets = this.placeMatchTargets[pid];
3161
+ if (targets === void 0 || targets.length === 0) return;
3162
+ const compiled = this.compiled;
3163
+ for (const [tid, keyIdx] of targets) {
3164
+ const cache = this.matchCaches[tid];
3165
+ if (cache == null) continue;
3166
+ const t = compiled.transition(tid);
3167
+ const mk = t.matchSpec.keys[keyIdx];
3168
+ const guard = t.inputSpecs.find((s) => s.place.name === mk.place.name)?.guard;
3169
+ if (guard && !guard(token.value)) continue;
3170
+ const name = mk.key(token.value);
3171
+ if (name !== void 0 && name !== null) cache.add(keyIdx, name, token.createdAt);
3172
+ }
2779
3173
  }
2780
3174
  // ======================== Execution ========================
2781
3175
  async run(timeoutMs) {
@@ -2974,12 +3368,22 @@ var BitmapNetExecutor = class {
2974
3368
  }
2975
3369
  if (this.compiled.hasGuards(tid)) {
2976
3370
  const t = this.compiled.transition(tid);
3371
+ const cache = this.matchCaches[tid];
3372
+ const ms = t.matchSpec;
2977
3373
  for (const spec of t.inputSpecs) {
2978
3374
  if (!spec.guard) continue;
3375
+ if (cache != null && ms && keyForPlace(ms, spec.place.name) !== void 0) continue;
2979
3376
  const requiredCount2 = spec.type === "one" ? 1 : spec.type === "exactly" ? spec.count : spec.type === "at-least" ? spec.minimum : 1;
2980
3377
  if (this.marking.countMatching(spec) < requiredCount2) return false;
2981
3378
  }
2982
3379
  }
3380
+ if (this.compiled.hasMatch(tid)) {
3381
+ const cache = this.matchCaches[tid];
3382
+ const noBinding = cache != null ? cache.best() === null : findBinding(this.compiled.transition(tid), (p) => this.marking.peekTokens(p)) === null;
3383
+ if (noBinding) {
3384
+ return false;
3385
+ }
3386
+ }
2983
3387
  return true;
2984
3388
  }
2985
3389
  hasInputFromResetPlace(t) {
@@ -3057,7 +3461,22 @@ var BitmapNetExecutor = class {
3057
3461
  const t = this.compiled.transition(tid);
3058
3462
  const inputs = new TokenInput();
3059
3463
  const consumed = [];
3464
+ const ms = t.matchSpec;
3465
+ const cache = ms ? this.matchCaches[tid] : null;
3466
+ const chosen = ms ? cache != null ? cache.best() : findBinding(t, (p) => this.marking.peekTokens(p)) : null;
3467
+ if (cache != null && chosen !== null) cache.consume(chosen);
3060
3468
  for (const inSpec of t.inputSpecs) {
3469
+ const keyFn = ms ? keyForPlace(ms, inSpec.place.name) : void 0;
3470
+ let spec;
3471
+ if (keyFn && chosen !== null) {
3472
+ const baseGuard = inSpec.guard;
3473
+ spec = {
3474
+ place: inSpec.place,
3475
+ guard: (v) => (baseGuard ? baseGuard(v) : true) && keyFn(v) === chosen
3476
+ };
3477
+ } else {
3478
+ spec = inSpec;
3479
+ }
3061
3480
  let toConsume;
3062
3481
  switch (inSpec.type) {
3063
3482
  case "one":
@@ -3067,14 +3486,12 @@ var BitmapNetExecutor = class {
3067
3486
  toConsume = inSpec.count;
3068
3487
  break;
3069
3488
  case "all":
3070
- toConsume = inSpec.guard ? this.marking.countMatching(inSpec) : this.marking.tokenCount(inSpec.place);
3071
- break;
3072
3489
  case "at-least":
3073
- toConsume = inSpec.guard ? this.marking.countMatching(inSpec) : this.marking.tokenCount(inSpec.place);
3490
+ toConsume = spec.guard ? this.marking.countMatching(spec) : this.marking.tokenCount(inSpec.place);
3074
3491
  break;
3075
3492
  }
3076
3493
  for (let i = 0; i < toConsume; i++) {
3077
- const token = inSpec.guard ? this.marking.removeFirstMatching(inSpec) : this.marking.removeFirst(inSpec.place);
3494
+ const token = spec.guard ? this.marking.removeFirstMatching(spec) : this.marking.removeFirst(inSpec.place);
3078
3495
  if (token === null) break;
3079
3496
  consumed.push(token);
3080
3497
  inputs.add(inSpec.place, token);
@@ -3136,6 +3553,8 @@ var BitmapNetExecutor = class {
3136
3553
  logFn,
3137
3554
  t.placeAlias
3138
3555
  );
3556
+ const freshNameBase = t.name;
3557
+ context.setFreshNameSupplier(() => nameId(`${freshNameBase}#${this.freshNameCounter++}`));
3139
3558
  let actionPromise = t.action(context);
3140
3559
  if (t.hasActionTimeout()) {
3141
3560
  const timeoutSpec = t.actionTimeout;
@@ -3237,9 +3656,10 @@ var BitmapNetExecutor = class {
3237
3656
  }
3238
3657
  const produced = [];
3239
3658
  for (const entry of outputs.entries()) {
3659
+ const pid = this.compiled.placeId(entry.place);
3660
+ this.cacheAddToken(pid, entry.token);
3240
3661
  this.marking.addToken(entry.place, entry.token);
3241
3662
  produced.push(entry.token);
3242
- const pid = this.compiled.placeId(entry.place);
3243
3663
  setBit(this.markedPlaces, pid);
3244
3664
  this.markDirty(pid);
3245
3665
  this.emitEvent({
@@ -3280,8 +3700,9 @@ var BitmapNetExecutor = class {
3280
3700
  for (let i = 0; i < len; i++) {
3281
3701
  const event = this.externalQueue[i];
3282
3702
  try {
3283
- this.marking.addToken(event.place, event.token);
3284
3703
  const pid = this.compiled.placeId(event.place);
3704
+ this.cacheAddToken(pid, event.token);
3705
+ this.marking.addToken(event.place, event.token);
3285
3706
  setBit(this.markedPlaces, pid);
3286
3707
  this.markDirty(pid);
3287
3708
  this.emitEvent({
@@ -3488,6 +3909,9 @@ var PrecompiledNet = class _PrecompiledNet {
3488
3909
  // ==================== Cardinality & Guards ====================
3489
3910
  cardinalityChecks;
3490
3911
  hasGuards;
3912
+ // ν-net join flag (NU-020): gates the match-binding check off the hot
3913
+ // enablement path for non-ν transitions, mirroring `hasGuards`.
3914
+ hasMatch;
3491
3915
  // ==================== Global Flags ====================
3492
3916
  allImmediate;
3493
3917
  allSamePriority;
@@ -3582,12 +4006,15 @@ var PrecompiledNet = class _PrecompiledNet {
3582
4006
  this.consumptionPlaceIds = consumptionPlaceIds;
3583
4007
  const cardinalityChecks = new Array(tc);
3584
4008
  const hasGuards = new Array(tc);
4009
+ const hasMatch = new Array(tc);
3585
4010
  for (let tid = 0; tid < tc; tid++) {
3586
4011
  cardinalityChecks[tid] = compiled.cardinalityCheck(tid);
3587
4012
  hasGuards[tid] = compiled.hasGuards(tid);
4013
+ hasMatch[tid] = compiled.hasMatch(tid);
3588
4014
  }
3589
4015
  this.cardinalityChecks = cardinalityChecks;
3590
4016
  this.hasGuards = hasGuards;
4017
+ this.hasMatch = hasMatch;
3591
4018
  this.earliestMs = new Float64Array(tc);
3592
4019
  this.latestMs = new Float64Array(tc);
3593
4020
  this.hasDeadline = new Uint8Array(tc);
@@ -3783,6 +4210,28 @@ var PrecompiledNetExecutor = class {
3783
4210
  // ==================== Token Storage ====================
3784
4211
  /** Per-place token arrays, indexed by pid. */
3785
4212
  tokenQueues;
4213
+ /** Monotonic source for ν-name minting (ctx.freshName(), NU-010). */
4214
+ freshNameCounter = 0;
4215
+ /**
4216
+ * Per-transition ν-name minter cache, indexed by tid (built lazily). Each
4217
+ * supplier is created once and reused across firings, so installing it on a
4218
+ * per-fire context is a field assignment rather than a per-fire closure
4219
+ * allocation — keeping the fire path lean for non-ν transitions. Every
4220
+ * transition gets one (forks mint but carry no MatchSpec, so gating on
4221
+ * `hasMatch` would wrongly starve them).
4222
+ */
4223
+ freshNameSuppliers = [];
4224
+ // ==================== ν-net incremental match caches (NU-020) ====================
4225
+ /**
4226
+ * Per matched transition: an {@link IncrementalMatcher} kept in lockstep with
4227
+ * the token queues when the transition is fast-path eligible (every correlated
4228
+ * input is `one`/`exactly`, consumed by no other transition, and never reset),
4229
+ * else `null` → fall back to the O(n) rebuild {@link findBinding}. This turns a
4230
+ * draining matched join from O(n²) into O(n log n).
4231
+ */
4232
+ matchCaches = [];
4233
+ /** Per place (by pid): the `[tid, keyIndex]` of every fast-path correlated input it feeds. */
4234
+ placeMatchTargets = [];
3786
4235
  // ==================== Marking Bitmap ====================
3787
4236
  markingBitmap;
3788
4237
  // ==================== Transition State ====================
@@ -3869,6 +4318,92 @@ var PrecompiledNetExecutor = class {
3869
4318
  this.pendingResetWords = new Uint32Array(wc);
3870
4319
  this.markingSnapBuffer = new Uint32Array(wc);
3871
4320
  this.firingSnapBuffer = new Uint32Array(wc);
4321
+ this.initMatchCaches();
4322
+ }
4323
+ /**
4324
+ * Builds the ν-net incremental match caches (NU-020). A matched join is
4325
+ * fast-path eligible only when every correlated input is `one`/`exactly`, is
4326
+ * consumed by no other transition, and is never reset — so tokens enter a
4327
+ * correlated input only via produce/inject (mirrored by `add`) and leave only
4328
+ * via this join's matched consume (mirrored by `consume`), and the cache can
4329
+ * never desync. Mirrors the Rust backends.
4330
+ */
4331
+ initMatchCaches() {
4332
+ const prog = this.program;
4333
+ const tc = prog.transitionCount;
4334
+ const pc = prog.placeCount;
4335
+ this.matchCaches = new Array(tc).fill(null);
4336
+ this.placeMatchTargets = Array.from({ length: pc }, () => []);
4337
+ let anyMatch = false;
4338
+ for (let tid = 0; tid < tc; tid++) {
4339
+ if (prog.hasMatch[tid]) {
4340
+ anyMatch = true;
4341
+ break;
4342
+ }
4343
+ }
4344
+ if (!anyMatch) return;
4345
+ const inputConsumers = Array.from({ length: pc }, () => []);
4346
+ const resetTarget = new Array(pc).fill(false);
4347
+ for (let tid = 0; tid < tc; tid++) {
4348
+ const t = prog.compiled.transition(tid);
4349
+ for (const spec of t.inputSpecs) inputConsumers[prog.compiled.placeId(spec.place)].push(tid);
4350
+ for (const arc of t.resets) resetTarget[prog.compiled.placeId(arc.place)] = true;
4351
+ }
4352
+ for (let tid = 0; tid < tc; tid++) {
4353
+ if (!prog.hasMatch[tid]) continue;
4354
+ const t = prog.compiled.transition(tid);
4355
+ const ms = t.matchSpec;
4356
+ if (!ms) continue;
4357
+ const requireds = [];
4358
+ let eligible = true;
4359
+ for (const mk of ms.keys) {
4360
+ const pid = prog.compiled.placeId(mk.place);
4361
+ const spec = t.inputSpecs.find((s) => s.place.name === mk.place.name);
4362
+ let required;
4363
+ if (spec?.type === "one") required = 1;
4364
+ else if (spec?.type === "exactly") required = spec.count;
4365
+ else {
4366
+ eligible = false;
4367
+ break;
4368
+ }
4369
+ const cons = inputConsumers[pid];
4370
+ if (resetTarget[pid] || cons.length !== 1 || cons[0] !== tid) {
4371
+ eligible = false;
4372
+ break;
4373
+ }
4374
+ requireds.push(required);
4375
+ }
4376
+ if (!eligible) continue;
4377
+ const matcher = new IncrementalMatcher(requireds);
4378
+ for (let keyIdx = 0; keyIdx < ms.keys.length; keyIdx++) {
4379
+ const mk = ms.keys[keyIdx];
4380
+ const pid = prog.compiled.placeId(mk.place);
4381
+ const guard = t.inputSpecs.find((s) => s.place.name === mk.place.name)?.guard;
4382
+ for (const token of this.tokenQueues[pid]) {
4383
+ if (guard && !guard(token.value)) continue;
4384
+ const name = mk.key(token.value);
4385
+ if (name !== void 0 && name !== null) matcher.add(keyIdx, name, token.createdAt);
4386
+ }
4387
+ this.placeMatchTargets[pid].push([tid, keyIdx]);
4388
+ }
4389
+ this.matchCaches[tid] = matcher;
4390
+ }
4391
+ }
4392
+ /** Mirror a token added to correlated input `pid` into every fast-path matcher. */
4393
+ cacheAddToken(pid, token) {
4394
+ const targets = this.placeMatchTargets[pid];
4395
+ if (targets === void 0 || targets.length === 0) return;
4396
+ const prog = this.program;
4397
+ for (const [tid, keyIdx] of targets) {
4398
+ const cache = this.matchCaches[tid];
4399
+ if (cache == null) continue;
4400
+ const t = prog.compiled.transition(tid);
4401
+ const mk = t.matchSpec.keys[keyIdx];
4402
+ const guard = t.inputSpecs.find((s) => s.place.name === mk.place.name)?.guard;
4403
+ if (guard && !guard(token.value)) continue;
4404
+ const name = mk.key(token.value);
4405
+ if (name !== void 0 && name !== null) cache.add(keyIdx, name, token.createdAt);
4406
+ }
3872
4407
  }
3873
4408
  // ======================== Bitmap Helpers ========================
3874
4409
  markTransitionDirty(tid) {
@@ -4079,12 +4614,22 @@ var PrecompiledNetExecutor = class {
4079
4614
  }
4080
4615
  if (prog.hasGuards[tid]) {
4081
4616
  const t = prog.compiled.transition(tid);
4617
+ const cache = this.matchCaches[tid];
4618
+ const ms = t.matchSpec;
4082
4619
  for (const spec of t.inputSpecs) {
4083
4620
  if (!spec.guard) continue;
4621
+ if (cache != null && ms && keyForPlace(ms, spec.place.name) !== void 0) continue;
4084
4622
  const required = spec.type === "one" ? 1 : spec.type === "exactly" ? spec.count : spec.type === "at-least" ? spec.minimum : 1;
4085
4623
  if (this.countMatching(prog.compiled.placeId(spec.place), spec.guard) < required) return false;
4086
4624
  }
4087
4625
  }
4626
+ if (prog.hasMatch[tid]) {
4627
+ const cache = this.matchCaches[tid];
4628
+ const noBinding = cache != null ? cache.best() === null : findBinding(prog.compiled.transition(tid), (p) => this.tokenQueues[prog.compiled.placeId(p)]) === null;
4629
+ if (noBinding) {
4630
+ return false;
4631
+ }
4632
+ }
4088
4633
  return true;
4089
4634
  }
4090
4635
  countMatching(pid, guard) {
@@ -4099,7 +4644,7 @@ var PrecompiledNetExecutor = class {
4099
4644
  const q = this.tokenQueues[pid];
4100
4645
  for (let i = 0; i < q.length; i++) {
4101
4646
  if (guard(q[i].value)) {
4102
- return q.splice(i, 1)[0];
4647
+ return i === 0 ? q.shift() : q.splice(i, 1)[0];
4103
4648
  }
4104
4649
  }
4105
4650
  return null;
@@ -4174,7 +4719,9 @@ var PrecompiledNetExecutor = class {
4174
4719
  const t = prog.compiled.transition(tid);
4175
4720
  const consumed = [];
4176
4721
  const inputs = new TokenInput();
4177
- if (prog.hasGuards[tid]) {
4722
+ if (t.matchSpec) {
4723
+ this.fireTransitionMatched(tid, t, inputs, consumed);
4724
+ } else if (prog.hasGuards[tid]) {
4178
4725
  this.fireTransitionGuarded(tid, t, inputs, consumed);
4179
4726
  } else {
4180
4727
  const ops = prog.consumeOps[tid];
@@ -4308,6 +4855,13 @@ var PrecompiledNetExecutor = class {
4308
4855
  logFn,
4309
4856
  t.placeAlias
4310
4857
  );
4858
+ let freshNameSupplier = this.freshNameSuppliers[tid];
4859
+ if (freshNameSupplier === void 0) {
4860
+ const base = t.name;
4861
+ freshNameSupplier = () => nameId(`${base}#${this.freshNameCounter++}`);
4862
+ this.freshNameSuppliers[tid] = freshNameSupplier;
4863
+ }
4864
+ context.setFreshNameSupplier(freshNameSupplier);
4311
4865
  let actionPromise = t.action(context);
4312
4866
  if (t.hasActionTimeout()) {
4313
4867
  const timeoutSpec = t.actionTimeout;
@@ -4361,6 +4915,65 @@ var PrecompiledNetExecutor = class {
4361
4915
  this.enabledTransitionCount--;
4362
4916
  this.enabledAtMs[tid] = -Infinity;
4363
4917
  }
4918
+ /**
4919
+ * Consumes the name-matched tokens for a ν-net join (NU-020): correlated
4920
+ * inputs take tokens whose projected name equals the chosen binding (guard
4921
+ * first, then name equality — NU-021); other inputs consume FIFO. Reset arcs
4922
+ * are honoured as on the opcode path. Mirrors {@link fireTransitionGuarded}.
4923
+ */
4924
+ fireTransitionMatched(tid, t, inputs, consumed) {
4925
+ const prog = this.program;
4926
+ const ms = t.matchSpec;
4927
+ const cache = this.matchCaches[tid];
4928
+ const chosen = cache != null ? cache.best() : findBinding(t, (p) => this.tokenQueues[prog.compiled.placeId(p)]);
4929
+ if (cache != null && chosen !== null) cache.consume(chosen);
4930
+ for (const inSpec of t.inputSpecs) {
4931
+ const pid = prog.compiled.placeId(inSpec.place);
4932
+ const keyFn = keyForPlace(ms, inSpec.place.name);
4933
+ const baseGuard = inSpec.guard;
4934
+ const pred = keyFn && chosen !== null ? (v) => (baseGuard ? baseGuard(v) : true) && keyFn(v) === chosen : baseGuard;
4935
+ let toConsume;
4936
+ switch (inSpec.type) {
4937
+ case "one":
4938
+ toConsume = 1;
4939
+ break;
4940
+ case "exactly":
4941
+ toConsume = inSpec.count;
4942
+ break;
4943
+ case "all":
4944
+ case "at-least":
4945
+ toConsume = pred ? this.countMatching(pid, pred) : this.tokenQueues[pid].length;
4946
+ break;
4947
+ }
4948
+ for (let i = 0; i < toConsume; i++) {
4949
+ const token = pred ? this.removeFirstMatching(pid, pred) : this.tokenQueues[pid].shift() ?? null;
4950
+ if (token === null) break;
4951
+ consumed.push(token);
4952
+ inputs.add(inSpec.place, token);
4953
+ this.emitEvent({
4954
+ type: "token-removed",
4955
+ timestamp: Date.now(),
4956
+ placeName: inSpec.place.name,
4957
+ token
4958
+ });
4959
+ }
4960
+ }
4961
+ for (const arc of t.resets) {
4962
+ const pid = prog.compiled.placeId(arc.place);
4963
+ const tokens = this.tokenQueues[pid].splice(0);
4964
+ this.pendingResetWords[pid >>> WORD_SHIFT] |= 1 << (pid & BIT_MASK);
4965
+ this.hasPendingResets = true;
4966
+ for (const token of tokens) {
4967
+ consumed.push(token);
4968
+ this.emitEvent({
4969
+ type: "token-removed",
4970
+ timestamp: Date.now(),
4971
+ placeName: arc.place.name,
4972
+ token
4973
+ });
4974
+ }
4975
+ }
4976
+ }
4364
4977
  fireTransitionGuarded(_tid, t, inputs, consumed) {
4365
4978
  const prog = this.program;
4366
4979
  for (const inSpec of t.inputSpecs) {
@@ -4475,6 +5088,7 @@ var PrecompiledNetExecutor = class {
4475
5088
  const produced = [];
4476
5089
  for (const entry of outputs.entries()) {
4477
5090
  const pid = prog.compiled.placeId(entry.place);
5091
+ this.cacheAddToken(pid, entry.token);
4478
5092
  this.tokenQueues[pid].push(entry.token);
4479
5093
  produced.push(entry.token);
4480
5094
  this.setMarkingBit(pid);
@@ -4519,6 +5133,7 @@ var PrecompiledNetExecutor = class {
4519
5133
  const event = this.externalQueue[i];
4520
5134
  try {
4521
5135
  const pid = prog.compiled.placeId(event.place);
5136
+ this.cacheAddToken(pid, event.token);
4522
5137
  this.tokenQueues[pid].push(event.token);
4523
5138
  this.setMarkingBit(pid);
4524
5139
  this.markDirty(pid);
@@ -4719,8 +5334,13 @@ export {
4719
5334
  intersects,
4720
5335
  isFailureEvent,
4721
5336
  isUnit,
5337
+ keyForPlace,
4722
5338
  latest,
5339
+ matchCorrelates,
5340
+ matchKey,
5341
+ matchSpec,
4723
5342
  matchesGuard,
5343
+ nameId,
4724
5344
  noopEventStore,
4725
5345
  one,
4726
5346
  openSubnet,