libpetri 2.8.0 → 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/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,149 @@ 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
+ 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
+ };
2765
2909
 
2766
2910
  // src/runtime/out-violation-error.ts
2767
2911
  var OutViolationError = class extends Error {
@@ -2838,6 +2982,16 @@ var BitmapNetExecutor = class {
2838
2982
  marking;
2839
2983
  /** Monotonic source for ν-name minting (ctx.freshName(), NU-010). */
2840
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 = [];
2841
2995
  eventStore;
2842
2996
  environmentPlaces;
2843
2997
  hasEnvironmentPlaces;
@@ -2932,6 +3086,90 @@ var BitmapNetExecutor = class {
2932
3086
  for (const spec of t.inputSpecs) names.add(spec.place.name);
2933
3087
  this.transitionInputPlaceNames.set(t, names);
2934
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
+ }
2935
3173
  }
2936
3174
  // ======================== Execution ========================
2937
3175
  async run(timeoutMs) {
@@ -3130,15 +3368,19 @@ var BitmapNetExecutor = class {
3130
3368
  }
3131
3369
  if (this.compiled.hasGuards(tid)) {
3132
3370
  const t = this.compiled.transition(tid);
3371
+ const cache = this.matchCaches[tid];
3372
+ const ms = t.matchSpec;
3133
3373
  for (const spec of t.inputSpecs) {
3134
3374
  if (!spec.guard) continue;
3375
+ if (cache != null && ms && keyForPlace(ms, spec.place.name) !== void 0) continue;
3135
3376
  const requiredCount2 = spec.type === "one" ? 1 : spec.type === "exactly" ? spec.count : spec.type === "at-least" ? spec.minimum : 1;
3136
3377
  if (this.marking.countMatching(spec) < requiredCount2) return false;
3137
3378
  }
3138
3379
  }
3139
3380
  if (this.compiled.hasMatch(tid)) {
3140
- const tMatch = this.compiled.transition(tid);
3141
- if (findBinding(tMatch, (p) => this.marking.peekTokens(p)) === null) {
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) {
3142
3384
  return false;
3143
3385
  }
3144
3386
  }
@@ -3220,7 +3462,9 @@ var BitmapNetExecutor = class {
3220
3462
  const inputs = new TokenInput();
3221
3463
  const consumed = [];
3222
3464
  const ms = t.matchSpec;
3223
- const chosen = ms ? findBinding(t, (p) => this.marking.peekTokens(p)) : null;
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);
3224
3468
  for (const inSpec of t.inputSpecs) {
3225
3469
  const keyFn = ms ? keyForPlace(ms, inSpec.place.name) : void 0;
3226
3470
  let spec;
@@ -3412,9 +3656,10 @@ var BitmapNetExecutor = class {
3412
3656
  }
3413
3657
  const produced = [];
3414
3658
  for (const entry of outputs.entries()) {
3659
+ const pid = this.compiled.placeId(entry.place);
3660
+ this.cacheAddToken(pid, entry.token);
3415
3661
  this.marking.addToken(entry.place, entry.token);
3416
3662
  produced.push(entry.token);
3417
- const pid = this.compiled.placeId(entry.place);
3418
3663
  setBit(this.markedPlaces, pid);
3419
3664
  this.markDirty(pid);
3420
3665
  this.emitEvent({
@@ -3455,8 +3700,9 @@ var BitmapNetExecutor = class {
3455
3700
  for (let i = 0; i < len; i++) {
3456
3701
  const event = this.externalQueue[i];
3457
3702
  try {
3458
- this.marking.addToken(event.place, event.token);
3459
3703
  const pid = this.compiled.placeId(event.place);
3704
+ this.cacheAddToken(pid, event.token);
3705
+ this.marking.addToken(event.place, event.token);
3460
3706
  setBit(this.markedPlaces, pid);
3461
3707
  this.markDirty(pid);
3462
3708
  this.emitEvent({
@@ -3975,6 +4221,17 @@ var PrecompiledNetExecutor = class {
3975
4221
  * `hasMatch` would wrongly starve them).
3976
4222
  */
3977
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 = [];
3978
4235
  // ==================== Marking Bitmap ====================
3979
4236
  markingBitmap;
3980
4237
  // ==================== Transition State ====================
@@ -4061,6 +4318,92 @@ var PrecompiledNetExecutor = class {
4061
4318
  this.pendingResetWords = new Uint32Array(wc);
4062
4319
  this.markingSnapBuffer = new Uint32Array(wc);
4063
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
+ }
4064
4407
  }
4065
4408
  // ======================== Bitmap Helpers ========================
4066
4409
  markTransitionDirty(tid) {
@@ -4271,15 +4614,19 @@ var PrecompiledNetExecutor = class {
4271
4614
  }
4272
4615
  if (prog.hasGuards[tid]) {
4273
4616
  const t = prog.compiled.transition(tid);
4617
+ const cache = this.matchCaches[tid];
4618
+ const ms = t.matchSpec;
4274
4619
  for (const spec of t.inputSpecs) {
4275
4620
  if (!spec.guard) continue;
4621
+ if (cache != null && ms && keyForPlace(ms, spec.place.name) !== void 0) continue;
4276
4622
  const required = spec.type === "one" ? 1 : spec.type === "exactly" ? spec.count : spec.type === "at-least" ? spec.minimum : 1;
4277
4623
  if (this.countMatching(prog.compiled.placeId(spec.place), spec.guard) < required) return false;
4278
4624
  }
4279
4625
  }
4280
4626
  if (prog.hasMatch[tid]) {
4281
- const tMatch = prog.compiled.transition(tid);
4282
- if (findBinding(tMatch, (p) => this.tokenQueues[prog.compiled.placeId(p)]) === null) {
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) {
4283
4630
  return false;
4284
4631
  }
4285
4632
  }
@@ -4297,7 +4644,7 @@ var PrecompiledNetExecutor = class {
4297
4644
  const q = this.tokenQueues[pid];
4298
4645
  for (let i = 0; i < q.length; i++) {
4299
4646
  if (guard(q[i].value)) {
4300
- return q.splice(i, 1)[0];
4647
+ return i === 0 ? q.shift() : q.splice(i, 1)[0];
4301
4648
  }
4302
4649
  }
4303
4650
  return null;
@@ -4574,10 +4921,12 @@ var PrecompiledNetExecutor = class {
4574
4921
  * first, then name equality — NU-021); other inputs consume FIFO. Reset arcs
4575
4922
  * are honoured as on the opcode path. Mirrors {@link fireTransitionGuarded}.
4576
4923
  */
4577
- fireTransitionMatched(_tid, t, inputs, consumed) {
4924
+ fireTransitionMatched(tid, t, inputs, consumed) {
4578
4925
  const prog = this.program;
4579
4926
  const ms = t.matchSpec;
4580
- const chosen = findBinding(t, (p) => this.tokenQueues[prog.compiled.placeId(p)]);
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);
4581
4930
  for (const inSpec of t.inputSpecs) {
4582
4931
  const pid = prog.compiled.placeId(inSpec.place);
4583
4932
  const keyFn = keyForPlace(ms, inSpec.place.name);
@@ -4739,6 +5088,7 @@ var PrecompiledNetExecutor = class {
4739
5088
  const produced = [];
4740
5089
  for (const entry of outputs.entries()) {
4741
5090
  const pid = prog.compiled.placeId(entry.place);
5091
+ this.cacheAddToken(pid, entry.token);
4742
5092
  this.tokenQueues[pid].push(entry.token);
4743
5093
  produced.push(entry.token);
4744
5094
  this.setMarkingBit(pid);
@@ -4783,6 +5133,7 @@ var PrecompiledNetExecutor = class {
4783
5133
  const event = this.externalQueue[i];
4784
5134
  try {
4785
5135
  const pid = prog.compiled.placeId(event.place);
5136
+ this.cacheAddToken(pid, event.token);
4786
5137
  this.tokenQueues[pid].push(event.token);
4787
5138
  this.setMarkingBit(pid);
4788
5139
  this.markDirty(pid);