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 +4 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +419 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
3141
|
-
|
|
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
|
|
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
|
|
4282
|
-
|
|
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(
|
|
4981
|
+
fireTransitionMatched(tid, t, inputs, consumed) {
|
|
4578
4982
|
const prog = this.program;
|
|
4579
4983
|
const ms = t.matchSpec;
|
|
4580
|
-
const
|
|
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);
|