libpetri 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,6 +68,10 @@ Supported properties:
68
68
 
69
69
  Also computes **P-invariants** (Farkas variant) and supports IC3/PDR-style incremental verification.
70
70
 
71
+ ### ν-nets (`src/runtime/`)
72
+
73
+ Correlated fork/join by identity. A `MatchSpec` declares a `value → NameId` key projection over a transition's input places; a fork mints a fresh opaque name via `ctx.freshName()` into the token payload, and a join consumes only the sibling tokens that project to the same name. The deterministic `(oldest-timestamp, then name)` tie-break is byte-identical across implementations (NU-022), and an incremental matcher keeps correlated-join drain at O(N log N). A bounded `Budget` place is the decidability lever for verification.
74
+
71
75
  ## Quick Start
72
76
 
73
77
  ```typescript
package/dist/index.d.ts CHANGED
@@ -254,6 +254,16 @@ declare class BitmapNetExecutor implements PetriNetExecutor {
254
254
  private readonly marking;
255
255
  /** Monotonic source for ν-name minting (ctx.freshName(), NU-010). */
256
256
  private freshNameCounter;
257
+ /**
258
+ * ν-net incremental match caches (NU-020): per matched transition, an
259
+ * {@link IncrementalMatcher} kept in lockstep with the marking when the
260
+ * transition is fast-path eligible (every correlated input is `one`/`exactly`,
261
+ * consumed by no other transition, never reset), else `null` → fall back to
262
+ * the O(n) rebuild {@link findBinding}. Turns a draining matched join from
263
+ * O(n²) into O(n log n). Mirrors the precompiled executor and the Rust backends.
264
+ */
265
+ private matchCaches;
266
+ private placeMatchTargets;
257
267
  private readonly eventStore;
258
268
  private readonly environmentPlaces;
259
269
  private readonly hasEnvironmentPlaces;
@@ -291,6 +301,15 @@ declare class BitmapNetExecutor implements PetriNetExecutor {
291
301
  private draining;
292
302
  private closed;
293
303
  constructor(net: PetriNet, initialTokens: Map<Place<any>, Token<any>[]>, options?: BitmapNetExecutorOptions);
304
+ /**
305
+ * Builds the ν-net incremental match caches (NU-020). A matched join is
306
+ * fast-path eligible only when every correlated input is `one`/`exactly`, is
307
+ * consumed by no other transition, and is never reset — so the cache can never
308
+ * desync from the marking. Mirrors the precompiled executor.
309
+ */
310
+ private initMatchCaches;
311
+ /** Mirror a token added to correlated input `pid` into every fast-path matcher. */
312
+ private cacheAddToken;
294
313
  run(timeoutMs?: number): Promise<Marking>;
295
314
  private executeLoop;
296
315
  inject<T>(envPlace: EnvironmentPlace<T>, token: Token<T>): Promise<boolean>;
@@ -515,6 +534,16 @@ declare class PrecompiledNetExecutor implements PetriNetExecutor {
515
534
  * `hasMatch` would wrongly starve them).
516
535
  */
517
536
  private readonly freshNameSuppliers;
537
+ /**
538
+ * Per matched transition: an {@link IncrementalMatcher} kept in lockstep with
539
+ * the token queues when the transition is fast-path eligible (every correlated
540
+ * input is `one`/`exactly`, consumed by no other transition, and never reset),
541
+ * else `null` → fall back to the O(n) rebuild {@link findBinding}. This turns a
542
+ * draining matched join from O(n²) into O(n log n).
543
+ */
544
+ private matchCaches;
545
+ /** Per place (by pid): the `[tid, keyIndex]` of every fast-path correlated input it feeds. */
546
+ private placeMatchTargets;
518
547
  private readonly markingBitmap;
519
548
  private readonly dirtyBitmap;
520
549
  private readonly dirtyScanBuffer;
@@ -545,6 +574,17 @@ declare class PrecompiledNetExecutor implements PetriNetExecutor {
545
574
  private closed;
546
575
  private marking;
547
576
  constructor(net: PetriNet, initialTokens: Map<Place<any>, Token<any>[]>, options?: PrecompiledNetExecutorOptions);
577
+ /**
578
+ * Builds the ν-net incremental match caches (NU-020). A matched join is
579
+ * fast-path eligible only when every correlated input is `one`/`exactly`, is
580
+ * consumed by no other transition, and is never reset — so tokens enter a
581
+ * correlated input only via produce/inject (mirrored by `add`) and leave only
582
+ * via this join's matched consume (mirrored by `consume`), and the cache can
583
+ * never desync. Mirrors the Rust backends.
584
+ */
585
+ private initMatchCaches;
586
+ /** Mirror a token added to correlated input `pid` into every fast-path matcher. */
587
+ private cacheAddToken;
548
588
  private markTransitionDirty;
549
589
  private markDirty;
550
590
  private setMarkingBit;
package/dist/index.js CHANGED
@@ -2368,7 +2368,8 @@ var Marking = class _Marking {
2368
2368
  for (let i = 0; i < queue.length; i++) {
2369
2369
  const token = queue[i];
2370
2370
  if (spec.guard(token.value)) {
2371
- queue.splice(i, 1);
2371
+ if (i === 0) queue.shift();
2372
+ else queue.splice(i, 1);
2372
2373
  return token;
2373
2374
  }
2374
2375
  }
@@ -2762,6 +2763,206 @@ function guardFor(t, placeName) {
2762
2763
  }
2763
2764
  return void 0;
2764
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
+ /** Monotonic fast path: ascending `(rep_ts, name)`; `fifo[fifoHead]` (after
2851
+ * `cleanTop`) is the min. Active while `!heaped`. Advance `fifoHead` instead of
2852
+ * `shift()` (O(1)); reset when the head catches the length. */
2853
+ fifo = [];
2854
+ fifoHead = 0;
2855
+ /** Largest entry appended to `fifo` in the current monotonic run. */
2856
+ maxPushed = null;
2857
+ /** Once an out-of-order push migrates `fifo` into `heap`. */
2858
+ heaped = false;
2859
+ heap = new ReadyHeap();
2860
+ ready = /* @__PURE__ */ new Map();
2861
+ /** Add one (already guard-passing) token carrying `name` at `createdAt` to input `i`. */
2862
+ add(i, name, createdAt) {
2863
+ let q = this.ts[i].get(name);
2864
+ if (!q) {
2865
+ q = new MinQueue();
2866
+ this.ts[i].set(name, q);
2867
+ }
2868
+ q.pushBack(createdAt);
2869
+ this.refresh(name);
2870
+ this.cleanTop();
2871
+ }
2872
+ /** Remove the `required` earliest-inserted tokens of `name` from every input (NU-020). */
2873
+ consume(name) {
2874
+ for (let i = 0; i < this.requireds.length; i++) {
2875
+ const q = this.ts[i].get(name);
2876
+ if (q) {
2877
+ for (let r = 0; r < this.requireds[i]; r++) q.popFront();
2878
+ if (q.size === 0) this.ts[i].delete(name);
2879
+ }
2880
+ }
2881
+ this.refresh(name);
2882
+ this.cleanTop();
2883
+ }
2884
+ /** The name `selectMatchName` would pick, or `null`. O(1) read. */
2885
+ best() {
2886
+ if (this.heaped) {
2887
+ const top = this.heap.peek();
2888
+ return top ? top.name : null;
2889
+ }
2890
+ return this.fifoHead < this.fifo.length ? this.fifo[this.fifoHead].name : null;
2891
+ }
2892
+ /**
2893
+ * Route a ready name's `(rep, name)` to the active structure: append to the
2894
+ * FIFO in O(1) while pushes stay non-decreasing; on the first inversion,
2895
+ * migrate the FIFO into the heap and stay heap-backed.
2896
+ */
2897
+ pushReady(rep, name) {
2898
+ const entry = { ts: rep, name };
2899
+ if (this.heaped) {
2900
+ this.heap.push(entry);
2901
+ return;
2902
+ }
2903
+ const max = this.maxPushed;
2904
+ const monotonic = max === null || rep > max.ts || rep === max.ts && name >= max.name;
2905
+ if (monotonic) {
2906
+ this.maxPushed = entry;
2907
+ this.fifo.push(entry);
2908
+ } else {
2909
+ this.heaped = true;
2910
+ for (let i = this.fifoHead; i < this.fifo.length; i++) this.heap.push(this.fifo[i]);
2911
+ this.fifo.length = 0;
2912
+ this.fifoHead = 0;
2913
+ this.heap.push(entry);
2914
+ }
2915
+ }
2916
+ repTs(name) {
2917
+ let rep = Infinity;
2918
+ for (let i = 0; i < this.requireds.length; i++) {
2919
+ const q = this.ts[i].get(name);
2920
+ if (!q || q.size < this.requireds[i]) return null;
2921
+ const m = q.min();
2922
+ if (m < rep) rep = m;
2923
+ }
2924
+ return rep;
2925
+ }
2926
+ refresh(name) {
2927
+ const rep = this.repTs(name);
2928
+ if (rep !== null) {
2929
+ if (this.ready.get(name) !== rep) {
2930
+ this.ready.set(name, rep);
2931
+ this.pushReady(rep, name);
2932
+ }
2933
+ } else {
2934
+ this.ready.delete(name);
2935
+ }
2936
+ }
2937
+ cleanTop() {
2938
+ if (this.heaped) {
2939
+ for (; ; ) {
2940
+ const top = this.heap.peek();
2941
+ if (top && this.ready.get(top.name) !== top.ts) this.heap.pop();
2942
+ else break;
2943
+ }
2944
+ if (this.heap.size === 0) {
2945
+ this.heaped = false;
2946
+ this.maxPushed = null;
2947
+ }
2948
+ } else {
2949
+ while (this.fifoHead < this.fifo.length) {
2950
+ const top = this.fifo[this.fifoHead];
2951
+ if (this.ready.get(top.name) !== top.ts) this.fifoHead++;
2952
+ else break;
2953
+ }
2954
+ if (this.fifoHead >= this.fifo.length) {
2955
+ this.fifo.length = 0;
2956
+ this.fifoHead = 0;
2957
+ this.maxPushed = null;
2958
+ }
2959
+ }
2960
+ }
2961
+ /** Test-only: has the matcher fallen back from the FIFO fast path to the heap? */
2962
+ isHeaped() {
2963
+ return this.heaped;
2964
+ }
2965
+ };
2765
2966
 
2766
2967
  // src/runtime/out-violation-error.ts
2767
2968
  var OutViolationError = class extends Error {
@@ -2838,6 +3039,16 @@ var BitmapNetExecutor = class {
2838
3039
  marking;
2839
3040
  /** Monotonic source for ν-name minting (ctx.freshName(), NU-010). */
2840
3041
  freshNameCounter = 0;
3042
+ /**
3043
+ * ν-net incremental match caches (NU-020): per matched transition, an
3044
+ * {@link IncrementalMatcher} kept in lockstep with the marking when the
3045
+ * transition is fast-path eligible (every correlated input is `one`/`exactly`,
3046
+ * consumed by no other transition, never reset), else `null` → fall back to
3047
+ * the O(n) rebuild {@link findBinding}. Turns a draining matched join from
3048
+ * O(n²) into O(n log n). Mirrors the precompiled executor and the Rust backends.
3049
+ */
3050
+ matchCaches = [];
3051
+ placeMatchTargets = [];
2841
3052
  eventStore;
2842
3053
  environmentPlaces;
2843
3054
  hasEnvironmentPlaces;
@@ -2932,6 +3143,90 @@ var BitmapNetExecutor = class {
2932
3143
  for (const spec of t.inputSpecs) names.add(spec.place.name);
2933
3144
  this.transitionInputPlaceNames.set(t, names);
2934
3145
  }
3146
+ this.initMatchCaches();
3147
+ }
3148
+ /**
3149
+ * Builds the ν-net incremental match caches (NU-020). A matched join is
3150
+ * fast-path eligible only when every correlated input is `one`/`exactly`, is
3151
+ * consumed by no other transition, and is never reset — so the cache can never
3152
+ * desync from the marking. Mirrors the precompiled executor.
3153
+ */
3154
+ initMatchCaches() {
3155
+ const compiled = this.compiled;
3156
+ const tc = compiled.transitionCount;
3157
+ const pc = compiled.placeCount;
3158
+ this.matchCaches = new Array(tc).fill(null);
3159
+ this.placeMatchTargets = Array.from({ length: pc }, () => []);
3160
+ let anyMatch = false;
3161
+ for (let tid = 0; tid < tc; tid++) {
3162
+ if (compiled.hasMatch(tid)) {
3163
+ anyMatch = true;
3164
+ break;
3165
+ }
3166
+ }
3167
+ if (!anyMatch) return;
3168
+ const inputConsumers = Array.from({ length: pc }, () => []);
3169
+ const resetTarget = new Array(pc).fill(false);
3170
+ for (let tid = 0; tid < tc; tid++) {
3171
+ const t = compiled.transition(tid);
3172
+ for (const spec of t.inputSpecs) inputConsumers[compiled.placeId(spec.place)].push(tid);
3173
+ for (const arc of t.resets) resetTarget[compiled.placeId(arc.place)] = true;
3174
+ }
3175
+ for (let tid = 0; tid < tc; tid++) {
3176
+ if (!compiled.hasMatch(tid)) continue;
3177
+ const t = compiled.transition(tid);
3178
+ const ms = t.matchSpec;
3179
+ if (!ms) continue;
3180
+ const requireds = [];
3181
+ let eligible = true;
3182
+ for (const mk of ms.keys) {
3183
+ const pid = compiled.placeId(mk.place);
3184
+ const spec = t.inputSpecs.find((s) => s.place.name === mk.place.name);
3185
+ let required;
3186
+ if (spec?.type === "one") required = 1;
3187
+ else if (spec?.type === "exactly") required = spec.count;
3188
+ else {
3189
+ eligible = false;
3190
+ break;
3191
+ }
3192
+ const cons = inputConsumers[pid];
3193
+ if (resetTarget[pid] || cons.length !== 1 || cons[0] !== tid) {
3194
+ eligible = false;
3195
+ break;
3196
+ }
3197
+ requireds.push(required);
3198
+ }
3199
+ if (!eligible) continue;
3200
+ const matcher = new IncrementalMatcher(requireds);
3201
+ for (let keyIdx = 0; keyIdx < ms.keys.length; keyIdx++) {
3202
+ const mk = ms.keys[keyIdx];
3203
+ const pid = compiled.placeId(mk.place);
3204
+ const guard = t.inputSpecs.find((s) => s.place.name === mk.place.name)?.guard;
3205
+ for (const token of this.marking.peekTokens(mk.place)) {
3206
+ if (guard && !guard(token.value)) continue;
3207
+ const name = mk.key(token.value);
3208
+ if (name !== void 0 && name !== null) matcher.add(keyIdx, name, token.createdAt);
3209
+ }
3210
+ this.placeMatchTargets[pid].push([tid, keyIdx]);
3211
+ }
3212
+ this.matchCaches[tid] = matcher;
3213
+ }
3214
+ }
3215
+ /** Mirror a token added to correlated input `pid` into every fast-path matcher. */
3216
+ cacheAddToken(pid, token) {
3217
+ const targets = this.placeMatchTargets[pid];
3218
+ if (targets === void 0 || targets.length === 0) return;
3219
+ const compiled = this.compiled;
3220
+ for (const [tid, keyIdx] of targets) {
3221
+ const cache = this.matchCaches[tid];
3222
+ if (cache == null) continue;
3223
+ const t = compiled.transition(tid);
3224
+ const mk = t.matchSpec.keys[keyIdx];
3225
+ const guard = t.inputSpecs.find((s) => s.place.name === mk.place.name)?.guard;
3226
+ if (guard && !guard(token.value)) continue;
3227
+ const name = mk.key(token.value);
3228
+ if (name !== void 0 && name !== null) cache.add(keyIdx, name, token.createdAt);
3229
+ }
2935
3230
  }
2936
3231
  // ======================== Execution ========================
2937
3232
  async run(timeoutMs) {
@@ -3130,15 +3425,19 @@ var BitmapNetExecutor = class {
3130
3425
  }
3131
3426
  if (this.compiled.hasGuards(tid)) {
3132
3427
  const t = this.compiled.transition(tid);
3428
+ const cache = this.matchCaches[tid];
3429
+ const ms = t.matchSpec;
3133
3430
  for (const spec of t.inputSpecs) {
3134
3431
  if (!spec.guard) continue;
3432
+ if (cache != null && ms && keyForPlace(ms, spec.place.name) !== void 0) continue;
3135
3433
  const requiredCount2 = spec.type === "one" ? 1 : spec.type === "exactly" ? spec.count : spec.type === "at-least" ? spec.minimum : 1;
3136
3434
  if (this.marking.countMatching(spec) < requiredCount2) return false;
3137
3435
  }
3138
3436
  }
3139
3437
  if (this.compiled.hasMatch(tid)) {
3140
- const tMatch = this.compiled.transition(tid);
3141
- if (findBinding(tMatch, (p) => this.marking.peekTokens(p)) === null) {
3438
+ const cache = this.matchCaches[tid];
3439
+ const noBinding = cache != null ? cache.best() === null : findBinding(this.compiled.transition(tid), (p) => this.marking.peekTokens(p)) === null;
3440
+ if (noBinding) {
3142
3441
  return false;
3143
3442
  }
3144
3443
  }
@@ -3220,7 +3519,9 @@ var BitmapNetExecutor = class {
3220
3519
  const inputs = new TokenInput();
3221
3520
  const consumed = [];
3222
3521
  const ms = t.matchSpec;
3223
- const chosen = ms ? findBinding(t, (p) => this.marking.peekTokens(p)) : null;
3522
+ const cache = ms ? this.matchCaches[tid] : null;
3523
+ const chosen = ms ? cache != null ? cache.best() : findBinding(t, (p) => this.marking.peekTokens(p)) : null;
3524
+ if (cache != null && chosen !== null) cache.consume(chosen);
3224
3525
  for (const inSpec of t.inputSpecs) {
3225
3526
  const keyFn = ms ? keyForPlace(ms, inSpec.place.name) : void 0;
3226
3527
  let spec;
@@ -3412,9 +3713,10 @@ var BitmapNetExecutor = class {
3412
3713
  }
3413
3714
  const produced = [];
3414
3715
  for (const entry of outputs.entries()) {
3716
+ const pid = this.compiled.placeId(entry.place);
3717
+ this.cacheAddToken(pid, entry.token);
3415
3718
  this.marking.addToken(entry.place, entry.token);
3416
3719
  produced.push(entry.token);
3417
- const pid = this.compiled.placeId(entry.place);
3418
3720
  setBit(this.markedPlaces, pid);
3419
3721
  this.markDirty(pid);
3420
3722
  this.emitEvent({
@@ -3455,8 +3757,9 @@ var BitmapNetExecutor = class {
3455
3757
  for (let i = 0; i < len; i++) {
3456
3758
  const event = this.externalQueue[i];
3457
3759
  try {
3458
- this.marking.addToken(event.place, event.token);
3459
3760
  const pid = this.compiled.placeId(event.place);
3761
+ this.cacheAddToken(pid, event.token);
3762
+ this.marking.addToken(event.place, event.token);
3460
3763
  setBit(this.markedPlaces, pid);
3461
3764
  this.markDirty(pid);
3462
3765
  this.emitEvent({
@@ -3975,6 +4278,17 @@ var PrecompiledNetExecutor = class {
3975
4278
  * `hasMatch` would wrongly starve them).
3976
4279
  */
3977
4280
  freshNameSuppliers = [];
4281
+ // ==================== ν-net incremental match caches (NU-020) ====================
4282
+ /**
4283
+ * Per matched transition: an {@link IncrementalMatcher} kept in lockstep with
4284
+ * the token queues when the transition is fast-path eligible (every correlated
4285
+ * input is `one`/`exactly`, consumed by no other transition, and never reset),
4286
+ * else `null` → fall back to the O(n) rebuild {@link findBinding}. This turns a
4287
+ * draining matched join from O(n²) into O(n log n).
4288
+ */
4289
+ matchCaches = [];
4290
+ /** Per place (by pid): the `[tid, keyIndex]` of every fast-path correlated input it feeds. */
4291
+ placeMatchTargets = [];
3978
4292
  // ==================== Marking Bitmap ====================
3979
4293
  markingBitmap;
3980
4294
  // ==================== Transition State ====================
@@ -4061,6 +4375,92 @@ var PrecompiledNetExecutor = class {
4061
4375
  this.pendingResetWords = new Uint32Array(wc);
4062
4376
  this.markingSnapBuffer = new Uint32Array(wc);
4063
4377
  this.firingSnapBuffer = new Uint32Array(wc);
4378
+ this.initMatchCaches();
4379
+ }
4380
+ /**
4381
+ * Builds the ν-net incremental match caches (NU-020). A matched join is
4382
+ * fast-path eligible only when every correlated input is `one`/`exactly`, is
4383
+ * consumed by no other transition, and is never reset — so tokens enter a
4384
+ * correlated input only via produce/inject (mirrored by `add`) and leave only
4385
+ * via this join's matched consume (mirrored by `consume`), and the cache can
4386
+ * never desync. Mirrors the Rust backends.
4387
+ */
4388
+ initMatchCaches() {
4389
+ const prog = this.program;
4390
+ const tc = prog.transitionCount;
4391
+ const pc = prog.placeCount;
4392
+ this.matchCaches = new Array(tc).fill(null);
4393
+ this.placeMatchTargets = Array.from({ length: pc }, () => []);
4394
+ let anyMatch = false;
4395
+ for (let tid = 0; tid < tc; tid++) {
4396
+ if (prog.hasMatch[tid]) {
4397
+ anyMatch = true;
4398
+ break;
4399
+ }
4400
+ }
4401
+ if (!anyMatch) return;
4402
+ const inputConsumers = Array.from({ length: pc }, () => []);
4403
+ const resetTarget = new Array(pc).fill(false);
4404
+ for (let tid = 0; tid < tc; tid++) {
4405
+ const t = prog.compiled.transition(tid);
4406
+ for (const spec of t.inputSpecs) inputConsumers[prog.compiled.placeId(spec.place)].push(tid);
4407
+ for (const arc of t.resets) resetTarget[prog.compiled.placeId(arc.place)] = true;
4408
+ }
4409
+ for (let tid = 0; tid < tc; tid++) {
4410
+ if (!prog.hasMatch[tid]) continue;
4411
+ const t = prog.compiled.transition(tid);
4412
+ const ms = t.matchSpec;
4413
+ if (!ms) continue;
4414
+ const requireds = [];
4415
+ let eligible = true;
4416
+ for (const mk of ms.keys) {
4417
+ const pid = prog.compiled.placeId(mk.place);
4418
+ const spec = t.inputSpecs.find((s) => s.place.name === mk.place.name);
4419
+ let required;
4420
+ if (spec?.type === "one") required = 1;
4421
+ else if (spec?.type === "exactly") required = spec.count;
4422
+ else {
4423
+ eligible = false;
4424
+ break;
4425
+ }
4426
+ const cons = inputConsumers[pid];
4427
+ if (resetTarget[pid] || cons.length !== 1 || cons[0] !== tid) {
4428
+ eligible = false;
4429
+ break;
4430
+ }
4431
+ requireds.push(required);
4432
+ }
4433
+ if (!eligible) continue;
4434
+ const matcher = new IncrementalMatcher(requireds);
4435
+ for (let keyIdx = 0; keyIdx < ms.keys.length; keyIdx++) {
4436
+ const mk = ms.keys[keyIdx];
4437
+ const pid = prog.compiled.placeId(mk.place);
4438
+ const guard = t.inputSpecs.find((s) => s.place.name === mk.place.name)?.guard;
4439
+ for (const token of this.tokenQueues[pid]) {
4440
+ if (guard && !guard(token.value)) continue;
4441
+ const name = mk.key(token.value);
4442
+ if (name !== void 0 && name !== null) matcher.add(keyIdx, name, token.createdAt);
4443
+ }
4444
+ this.placeMatchTargets[pid].push([tid, keyIdx]);
4445
+ }
4446
+ this.matchCaches[tid] = matcher;
4447
+ }
4448
+ }
4449
+ /** Mirror a token added to correlated input `pid` into every fast-path matcher. */
4450
+ cacheAddToken(pid, token) {
4451
+ const targets = this.placeMatchTargets[pid];
4452
+ if (targets === void 0 || targets.length === 0) return;
4453
+ const prog = this.program;
4454
+ for (const [tid, keyIdx] of targets) {
4455
+ const cache = this.matchCaches[tid];
4456
+ if (cache == null) continue;
4457
+ const t = prog.compiled.transition(tid);
4458
+ const mk = t.matchSpec.keys[keyIdx];
4459
+ const guard = t.inputSpecs.find((s) => s.place.name === mk.place.name)?.guard;
4460
+ if (guard && !guard(token.value)) continue;
4461
+ const name = mk.key(token.value);
4462
+ if (name !== void 0 && name !== null) cache.add(keyIdx, name, token.createdAt);
4463
+ }
4064
4464
  }
4065
4465
  // ======================== Bitmap Helpers ========================
4066
4466
  markTransitionDirty(tid) {
@@ -4271,15 +4671,19 @@ var PrecompiledNetExecutor = class {
4271
4671
  }
4272
4672
  if (prog.hasGuards[tid]) {
4273
4673
  const t = prog.compiled.transition(tid);
4674
+ const cache = this.matchCaches[tid];
4675
+ const ms = t.matchSpec;
4274
4676
  for (const spec of t.inputSpecs) {
4275
4677
  if (!spec.guard) continue;
4678
+ if (cache != null && ms && keyForPlace(ms, spec.place.name) !== void 0) continue;
4276
4679
  const required = spec.type === "one" ? 1 : spec.type === "exactly" ? spec.count : spec.type === "at-least" ? spec.minimum : 1;
4277
4680
  if (this.countMatching(prog.compiled.placeId(spec.place), spec.guard) < required) return false;
4278
4681
  }
4279
4682
  }
4280
4683
  if (prog.hasMatch[tid]) {
4281
- const tMatch = prog.compiled.transition(tid);
4282
- if (findBinding(tMatch, (p) => this.tokenQueues[prog.compiled.placeId(p)]) === null) {
4684
+ const cache = this.matchCaches[tid];
4685
+ const noBinding = cache != null ? cache.best() === null : findBinding(prog.compiled.transition(tid), (p) => this.tokenQueues[prog.compiled.placeId(p)]) === null;
4686
+ if (noBinding) {
4283
4687
  return false;
4284
4688
  }
4285
4689
  }
@@ -4297,7 +4701,7 @@ var PrecompiledNetExecutor = class {
4297
4701
  const q = this.tokenQueues[pid];
4298
4702
  for (let i = 0; i < q.length; i++) {
4299
4703
  if (guard(q[i].value)) {
4300
- return q.splice(i, 1)[0];
4704
+ return i === 0 ? q.shift() : q.splice(i, 1)[0];
4301
4705
  }
4302
4706
  }
4303
4707
  return null;
@@ -4574,10 +4978,12 @@ var PrecompiledNetExecutor = class {
4574
4978
  * first, then name equality — NU-021); other inputs consume FIFO. Reset arcs
4575
4979
  * are honoured as on the opcode path. Mirrors {@link fireTransitionGuarded}.
4576
4980
  */
4577
- fireTransitionMatched(_tid, t, inputs, consumed) {
4981
+ fireTransitionMatched(tid, t, inputs, consumed) {
4578
4982
  const prog = this.program;
4579
4983
  const ms = t.matchSpec;
4580
- const chosen = findBinding(t, (p) => this.tokenQueues[prog.compiled.placeId(p)]);
4984
+ const cache = this.matchCaches[tid];
4985
+ const chosen = cache != null ? cache.best() : findBinding(t, (p) => this.tokenQueues[prog.compiled.placeId(p)]);
4986
+ if (cache != null && chosen !== null) cache.consume(chosen);
4581
4987
  for (const inSpec of t.inputSpecs) {
4582
4988
  const pid = prog.compiled.placeId(inSpec.place);
4583
4989
  const keyFn = keyForPlace(ms, inSpec.place.name);
@@ -4739,6 +5145,7 @@ var PrecompiledNetExecutor = class {
4739
5145
  const produced = [];
4740
5146
  for (const entry of outputs.entries()) {
4741
5147
  const pid = prog.compiled.placeId(entry.place);
5148
+ this.cacheAddToken(pid, entry.token);
4742
5149
  this.tokenQueues[pid].push(entry.token);
4743
5150
  produced.push(entry.token);
4744
5151
  this.setMarkingBit(pid);
@@ -4783,6 +5190,7 @@ var PrecompiledNetExecutor = class {
4783
5190
  const event = this.externalQueue[i];
4784
5191
  try {
4785
5192
  const pid = prog.compiled.placeId(event.place);
5193
+ this.cacheAddToken(pid, event.token);
4786
5194
  this.tokenQueues[pid].push(event.token);
4787
5195
  this.setMarkingBit(pid);
4788
5196
  this.markDirty(pid);