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 +4 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +362 -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,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
|
|
3141
|
-
|
|
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
|
|
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
|
|
4282
|
-
|
|
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(
|
|
4924
|
+
fireTransitionMatched(tid, t, inputs, consumed) {
|
|
4578
4925
|
const prog = this.program;
|
|
4579
4926
|
const ms = t.matchSpec;
|
|
4580
|
-
const
|
|
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);
|