libpetri 2.11.0 → 2.12.1

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.
@@ -588,6 +588,90 @@ function computePInvariants(matrix, flatNet, initialMarking) {
588
588
  }
589
589
  return invariants;
590
590
  }
591
+ function computePSemiflows(matrix, flatNet, initialMarking) {
592
+ const np = matrix.numPlaces();
593
+ const nt = matrix.numTransitions();
594
+ if (np === 0) return [];
595
+ const incidence = matrix.incidence();
596
+ let rows = [];
597
+ for (let p = 0; p < np; p++) {
598
+ const sig = new Array(nt);
599
+ for (let t = 0; t < nt; t++) sig[t] = incidence[t][p];
600
+ const weight = new Array(np).fill(0);
601
+ weight[p] = 1;
602
+ rows.push({ sig, weight });
603
+ }
604
+ for (let t = 0; t < nt; t++) {
605
+ const next = rows.filter((r) => r.sig[t] === 0);
606
+ const pos = rows.filter((r) => r.sig[t] > 0);
607
+ const neg = rows.filter((r) => r.sig[t] < 0);
608
+ for (const rp of pos) {
609
+ for (const rn of neg) {
610
+ const cp = -rn.sig[t];
611
+ const cn = rp.sig[t];
612
+ const sig = combineRow(cp, rp.sig, cn, rn.sig);
613
+ const weight = combineRow(cp, rp.weight, cn, rn.weight);
614
+ if (sig === null || weight === null) continue;
615
+ reduceGcd(sig, weight);
616
+ next.push({ sig, weight });
617
+ }
618
+ }
619
+ rows = keepSupportMinimal(next);
620
+ if (rows.length > 8192) rows.length = 8192;
621
+ }
622
+ const semiflows = [];
623
+ for (const { weight } of rows) {
624
+ if (!weight.some((x) => x !== 0)) continue;
625
+ const support = /* @__PURE__ */ new Set();
626
+ let constant = 0;
627
+ for (let p = 0; p < np; p++) {
628
+ if (weight[p] !== 0) {
629
+ support.add(p);
630
+ constant += weight[p] * initialMarking.tokens(flatNet.places[p]);
631
+ }
632
+ }
633
+ if (!Number.isSafeInteger(constant)) continue;
634
+ semiflows.push(pInvariant(weight, constant, support));
635
+ }
636
+ return semiflows;
637
+ }
638
+ function combineRow(cp, a, cn, b) {
639
+ const out = new Array(a.length);
640
+ for (let i = 0; i < a.length; i++) {
641
+ const v = cp * a[i] + cn * b[i];
642
+ if (!Number.isSafeInteger(v)) return null;
643
+ out[i] = v;
644
+ }
645
+ return out;
646
+ }
647
+ function reduceGcd(sig, weight) {
648
+ let g = 0;
649
+ for (const v of sig) g = gcd(g, Math.abs(v));
650
+ for (const v of weight) g = gcd(g, Math.abs(v));
651
+ if (g > 1) {
652
+ for (let i = 0; i < sig.length; i++) sig[i] = sig[i] / g;
653
+ for (let i = 0; i < weight.length; i++) weight[i] = weight[i] / g;
654
+ }
655
+ }
656
+ function keepSupportMinimal(rows) {
657
+ const supports = rows.map((r) => {
658
+ const s = [];
659
+ for (let i = 0; i < r.weight.length; i++) if (r.weight[i] !== 0) s.push(i);
660
+ return s;
661
+ });
662
+ const keep = new Array(rows.length).fill(true);
663
+ for (let i = 0; i < rows.length; i++) {
664
+ if (!keep[i]) continue;
665
+ for (let j = 0; j < rows.length; j++) {
666
+ if (i === j || !keep[j]) continue;
667
+ if (supports[j].length < supports[i].length && supports[j].every((p) => supports[i].includes(p))) {
668
+ keep[i] = false;
669
+ break;
670
+ }
671
+ }
672
+ }
673
+ return rows.filter((_, i) => keep[i]);
674
+ }
591
675
  function isCoveredByInvariants(invariants, numPlaces) {
592
676
  const covered = new Array(numPlaces).fill(false);
593
677
  for (const inv of invariants) {
@@ -1277,10 +1361,24 @@ var StateClass = class {
1277
1361
  marking;
1278
1362
  firingDomain;
1279
1363
  enabledTransitions;
1280
- constructor(marking, firingDomain, enabledTransitions) {
1364
+ /**
1365
+ * Class-relative earliest-ready time (seconds) of each enabled transition,
1366
+ * parallel to `enabledTransitions`. Captured from the firing-domain DBM
1367
+ * (`getLowerBound(k)`) *before* `letTimePass()` zeroes the lower bounds, i.e.
1368
+ * the minimum time from class entry at which clock `k` may fire.
1369
+ *
1370
+ * Purely additive: base timed-reachability (marking + DBM zone, `equals`,
1371
+ * `classKey`) ignores it. Read only by the ν conflict-priority prune (NU-052,
1372
+ * `priorityDominated`), where comparing `readyEarliest[H] <= readyEarliest[L]`
1373
+ * decides whether the strictly higher-priority `H` becomes ready no later than
1374
+ * `L` and so pre-empts it.
1375
+ */
1376
+ readyEarliest;
1377
+ constructor(marking, firingDomain, enabledTransitions, readyEarliest) {
1281
1378
  this.marking = marking;
1282
1379
  this.firingDomain = firingDomain;
1283
1380
  this.enabledTransitions = [...enabledTransitions];
1381
+ this.readyEarliest = [...readyEarliest];
1284
1382
  }
1285
1383
  isEmpty() {
1286
1384
  return this.firingDomain.isEmpty();
@@ -1453,9 +1551,10 @@ function initialStateClass(net, initialMarking, envPlaces, envMode) {
1453
1551
  const clockNames = enabledTransitions.map((t) => t.name);
1454
1552
  const lowerBounds = enabledTransitions.map((t) => earliest(t.timing) / 1e3);
1455
1553
  const upperBounds = enabledTransitions.map((t) => latest(t.timing) / 1e3);
1456
- let initialDBM = DBM.create(clockNames, lowerBounds, upperBounds);
1457
- initialDBM = initialDBM.letTimePass();
1458
- return new StateClass(initialMarking, initialDBM, enabledTransitions);
1554
+ const baseDBM = DBM.create(clockNames, lowerBounds, upperBounds);
1555
+ const readyEarliest = enabledTransitions.map((_, k) => baseDBM.getLowerBound(k));
1556
+ const initialDBM = baseDBM.letTimePass();
1557
+ return new StateClass(initialMarking, initialDBM, enabledTransitions, readyEarliest);
1459
1558
  }
1460
1559
  function expandTransition(t) {
1461
1560
  let branches;
@@ -1493,16 +1592,17 @@ function computeSuccessor(net, current, fired, environmentPlaces, environmentMod
1493
1592
  const newClockNames = newlyEnabled.map((t) => t.name);
1494
1593
  const newLowerBounds = newlyEnabled.map((t) => earliest(t.timing) / 1e3);
1495
1594
  const newUpperBounds = newlyEnabled.map((t) => latest(t.timing) / 1e3);
1496
- let newDBM = current.firingDomain.fireTransition(
1595
+ const firedDBM = current.firingDomain.fireTransition(
1497
1596
  firedIdx,
1498
1597
  newClockNames,
1499
1598
  newLowerBounds,
1500
1599
  newUpperBounds,
1501
1600
  persistentIndices
1502
1601
  );
1503
- newDBM = newDBM.letTimePass();
1504
1602
  const allEnabled = [...persistent, ...newlyEnabled];
1505
- return new StateClass(newMarking, newDBM, allEnabled);
1603
+ const readyEarliest = allEnabled.map((_, k) => firedDBM.getLowerBound(k));
1604
+ const newDBM = firedDBM.letTimePass();
1605
+ return new StateClass(newMarking, newDBM, allEnabled, readyEarliest);
1506
1606
  }
1507
1607
  function findEnabledTransitions(net, marking, environmentPlaces, environmentMode) {
1508
1608
  const enabled = [];
@@ -1661,14 +1761,37 @@ function extractMarking(ctx, reachableApp, flatNet) {
1661
1761
  }
1662
1762
 
1663
1763
  // src/verification/z3/name-coloured-encoder.ts
1664
- function buildColouredPlan(net, flat, initial, budgetNames) {
1665
- const P = flat.places.length;
1666
- if (flat.transitions.length !== [...net.transitions].length) {
1667
- return null;
1764
+ function colourSlotBound(coloured, semiflows) {
1765
+ const w = (inv, pid) => inv.weights[pid] ?? 0;
1766
+ const isSemiflow = (inv) => inv.weights.every((x) => x >= 0);
1767
+ let single = null;
1768
+ for (const inv of semiflows) {
1769
+ if (isSemiflow(inv) && inv.constant >= 1 && coloured.every((pid) => w(inv, pid) >= 1)) {
1770
+ if (single === null || inv.constant < single) single = inv.constant;
1771
+ }
1772
+ }
1773
+ if (single !== null) return single;
1774
+ let sumConst = 0;
1775
+ const covered = new Array(coloured.length).fill(false);
1776
+ for (const inv of semiflows) {
1777
+ if (!isSemiflow(inv)) continue;
1778
+ let touches = false;
1779
+ for (let i = 0; i < coloured.length; i++) {
1780
+ if (w(inv, coloured[i]) >= 1) {
1781
+ covered[i] = true;
1782
+ touches = true;
1783
+ }
1784
+ }
1785
+ if (touches) sumConst += inv.constant;
1668
1786
  }
1787
+ if (covered.every((c) => c) && sumConst >= 1) return sumConst;
1788
+ return null;
1789
+ }
1790
+ function buildColouredPlan(net, flat, initial, budgetNames, fragmentMode, carrierPlaces, semiflows) {
1791
+ const P = flat.places.length;
1669
1792
  const isColoured = new Array(P).fill(false);
1670
- for (const ft of flat.transitions) {
1671
- const ms = ft.source.matchSpec;
1793
+ for (const t of net.transitions) {
1794
+ const ms = t.matchSpec;
1672
1795
  if (ms) {
1673
1796
  for (const key of ms.keys) {
1674
1797
  const pid = flat.placeIndex.get(key.place.name);
@@ -1677,21 +1800,25 @@ function buildColouredPlan(net, flat, initial, budgetNames) {
1677
1800
  }
1678
1801
  }
1679
1802
  }
1803
+ if (fragmentMode === "extended") {
1804
+ for (const c of carrierPlaces) {
1805
+ const pid = flat.placeIndex.get(c);
1806
+ if (pid != null) isColoured[pid] = true;
1807
+ }
1808
+ }
1680
1809
  const coloured = [];
1681
1810
  for (let i = 0; i < P; i++) if (isColoured[i]) coloured.push(i);
1682
1811
  if (coloured.length === 0) return null;
1683
1812
  for (const pid of coloured) {
1684
1813
  if (initial.tokens(flat.places[pid]) !== 0) return null;
1685
1814
  }
1815
+ const k = colourSlotBound(coloured, semiflows);
1816
+ if (k === null) return null;
1686
1817
  const budgetIdx = /* @__PURE__ */ new Set();
1687
1818
  for (const n of budgetNames) {
1688
1819
  const i = flat.placeIndex.get(n);
1689
1820
  if (i != null) budgetIdx.add(i);
1690
1821
  }
1691
- if (budgetIdx.size === 0) return null;
1692
- let k = 0;
1693
- for (const b of budgetIdx) k += initial.tokens(flat.places[b]);
1694
- if (k === 0) return null;
1695
1822
  for (const ft of flat.transitions) {
1696
1823
  const touches = ft.inhibitorPlaces.some((i) => isColoured[i]) || ft.readPlaces.some((i) => isColoured[i]) || ft.resetPlaces.some((i) => isColoured[i]) || ft.consumeAll.some((ca, i) => ca && isColoured[i]);
1697
1824
  if (touches) return null;
@@ -1705,38 +1832,21 @@ function buildColouredPlan(net, flat, initial, budgetNames) {
1705
1832
  if (colouredOut.length !== 0 || colouredIn.length === 0) return null;
1706
1833
  if (colouredIn.some((pid) => ft.preVector[pid] !== 1)) return null;
1707
1834
  classes.push({ kind: "join", colouredIn });
1835
+ } else if (colouredIn.length !== 0) {
1836
+ if (fragmentMode !== "extended") return null;
1837
+ if (colouredIn.length !== 1 || ft.preVector[colouredIn[0]] !== 1) return null;
1838
+ if (colouredOut.some((o) => ft.postVector[o] !== 1)) return null;
1839
+ classes.push({ kind: "consume", inputCol: colouredIn[0], colouredOut });
1708
1840
  } else if (colouredOut.length !== 0) {
1709
- if (colouredIn.length !== 0) return null;
1710
1841
  if (colouredOut.some((o) => ft.postVector[o] !== 1)) return null;
1711
1842
  let budgetConsumed = 0;
1712
1843
  for (const b of budgetIdx) budgetConsumed += ft.preVector[b];
1713
1844
  if (budgetConsumed < 1) return null;
1714
1845
  classes.push({ kind: "mint", colouredOut });
1715
1846
  } else {
1716
- if (colouredIn.length !== 0) return null;
1717
1847
  classes.push({ kind: "untouched" });
1718
1848
  }
1719
1849
  }
1720
- let minMintCost = null;
1721
- let maxJoinRefund = 0;
1722
- for (let ti = 0; ti < classes.length; ti++) {
1723
- const cls = classes[ti];
1724
- const ft = flat.transitions[ti];
1725
- for (const b of budgetIdx) {
1726
- if (ft.preVector[b] > 0 && cls.kind !== "mint") return null;
1727
- if (ft.postVector[b] > 0 && cls.kind !== "join") return null;
1728
- }
1729
- if (cls.kind === "mint") {
1730
- let cost = 0;
1731
- for (const b of budgetIdx) cost += ft.preVector[b];
1732
- minMintCost = minMintCost === null ? cost : Math.min(minMintCost, cost);
1733
- } else if (cls.kind === "join") {
1734
- let refund = 0;
1735
- for (const b of budgetIdx) refund += ft.postVector[b];
1736
- maxJoinRefund = Math.max(maxJoinRefund, refund);
1737
- }
1738
- }
1739
- if (minMintCost === null || maxJoinRefund > minMintCost) return null;
1740
1850
  return { coloured, isColoured, k, classes };
1741
1851
  }
1742
1852
  function buildLayout(ctx, plan, P) {
@@ -1760,7 +1870,7 @@ function buildLayout(ctx, plan, P) {
1760
1870
  }
1761
1871
  return { colUnc, colCol, nCols, cur, nxt };
1762
1872
  }
1763
- function encodeColoured(ctx, fp, plan, flat, initial, property, invariants) {
1873
+ function encodeColoured(ctx, fp, plan, flat, initial, property, invariants, sinkPlaces = /* @__PURE__ */ new Set()) {
1764
1874
  const P = flat.places.length;
1765
1875
  const k = plan.k;
1766
1876
  const lay = buildLayout(ctx, plan, P);
@@ -1807,7 +1917,7 @@ function encodeColoured(ctx, fp, plan, flat, initial, property, invariants) {
1807
1917
  }
1808
1918
  });
1809
1919
  }
1810
- } else {
1920
+ } else if (cls.kind === "join") {
1811
1921
  const colouredIn = cls.colouredIn;
1812
1922
  for (let c = 0; c < k; c++) {
1813
1923
  const cc = c;
@@ -1820,9 +1930,27 @@ function encodeColoured(ctx, fp, plan, flat, initial, property, invariants) {
1820
1930
  }
1821
1931
  });
1822
1932
  }
1933
+ } else {
1934
+ const inputCol = cls.inputCol;
1935
+ const colouredOut = cls.colouredOut;
1936
+ for (let c = 0; c < k; c++) {
1937
+ const cc = c;
1938
+ addRule(ctx, fp, reachable, lay, plan, invariants, `${ft.name}_consume_${cc}`, (enab, upd) => {
1939
+ uncolouredIncidence(ctx, lay, plan, ft, enab, upd);
1940
+ const icol = lay.colCol[inputCol][cc];
1941
+ enab.push(lay.cur[icol].ge(1));
1942
+ upd.set(icol, lay.cur[icol].add(-1));
1943
+ for (const o of colouredOut) {
1944
+ const ocol = lay.colCol[o][cc];
1945
+ upd.set(ocol, lay.cur[ocol].add(1));
1946
+ }
1947
+ });
1948
+ }
1823
1949
  }
1824
1950
  }
1825
- addErrorRule(ctx, fp, reachable, error, lay, plan, flat, property);
1951
+ if (!addErrorRule(ctx, fp, reachable, error, lay, plan, flat, property, sinkPlaces)) {
1952
+ return null;
1953
+ }
1826
1954
  return {
1827
1955
  errorExpr: error.call(),
1828
1956
  reachableDecl: reachable
@@ -1886,20 +2014,22 @@ function liftedInvariant(ctx, inv, plan, lay, vars) {
1886
2014
  }
1887
2015
  return sum.eq(inv.constant);
1888
2016
  }
1889
- function addErrorRule(ctx, fp, reachable, error, lay, plan, flat, property) {
2017
+ function addErrorRule(ctx, fp, reachable, error, lay, plan, flat, property, sinkPlaces) {
2018
+ const violation = encodeViolation(ctx, plan, lay, flat, property, lay.cur, sinkPlaces);
2019
+ if (violation === null) return false;
1890
2020
  const reachBody = reachable.call(...lay.cur);
1891
- const violation = encodeViolation(ctx, plan, lay, flat, property, lay.cur);
1892
2021
  const body = ctx.And(reachBody, violation);
1893
2022
  const head = error.call();
1894
2023
  const qRule = ctx.ForAll([...lay.cur], ctx.Implies(body, head));
1895
2024
  fp.addRule(qRule, "error");
2025
+ return true;
1896
2026
  }
1897
- function encodeViolation(ctx, plan, lay, flat, property, cur) {
2027
+ function encodeViolation(ctx, plan, lay, flat, property, cur, sinkPlaces) {
1898
2028
  switch (property.type) {
1899
2029
  case "place-bound":
1900
2030
  case "branch-place-bound": {
1901
2031
  const idx = flatNetIndexOf(flat, property.place);
1902
- if (idx < 0) return ctx.Bool.val(false);
2032
+ if (idx < 0) return null;
1903
2033
  return aggregate(plan, lay, idx, cur).gt(property.bound);
1904
2034
  }
1905
2035
  case "mutual-exclusion": {
@@ -1917,11 +2047,126 @@ function encodeViolation(ctx, plan, lay, flat, property, cur) {
1917
2047
  if (conds.length === 0) return ctx.Bool.val(false);
1918
2048
  return conds.length === 1 ? conds[0] : ctx.And(...conds);
1919
2049
  }
1920
- // Quiescence properties are never routed here.
1921
2050
  case "deadlock-free":
1922
- case "joined-or-dead-lettered":
2051
+ return encodeColouredDeadlock(ctx, plan, lay, flat, sinkPlaces);
2052
+ case "joined-or-dead-lettered": {
2053
+ const idx = flatNetIndexOf(flat, property.pending);
2054
+ if (idx < 0) return null;
2055
+ const deadlock = encodeColouredDeadlock(ctx, plan, lay, flat, sinkPlaces);
2056
+ return ctx.And(deadlock, aggregate(plan, lay, idx, cur).ge(1));
2057
+ }
2058
+ }
2059
+ }
2060
+ function andAll(ctx, xs) {
2061
+ if (xs.length === 0) return ctx.Bool.val(true);
2062
+ let r = xs[0];
2063
+ for (let i = 1; i < xs.length; i++) r = ctx.And(r, xs[i]);
2064
+ return r;
2065
+ }
2066
+ function orAll(ctx, xs) {
2067
+ if (xs.length === 0) return ctx.Bool.val(false);
2068
+ let r = xs[0];
2069
+ for (let i = 1; i < xs.length; i++) r = ctx.Or(r, xs[i]);
2070
+ return r;
2071
+ }
2072
+ function injectedEnvIndices2(flat) {
2073
+ const out = /* @__PURE__ */ new Map();
2074
+ for (const [name, bound] of flat.environmentInjection) {
2075
+ const idx = flat.placeIndex.get(name);
2076
+ if (idx != null) out.set(idx, bound);
2077
+ }
2078
+ return out;
2079
+ }
2080
+ function uncolouredDisable(ft, lay, plan, envInj) {
2081
+ const reasons = [];
2082
+ let permanentlyDisabled = false;
2083
+ const P = ft.preVector.length;
2084
+ for (let i = 0; i < P; i++) {
2085
+ if (plan.isColoured[i] || ft.preVector[i] === 0) continue;
2086
+ if (envInj.has(i)) {
2087
+ const bound = envInj.get(i);
2088
+ if (bound !== null && ft.preVector[i] > bound) permanentlyDisabled = true;
2089
+ continue;
2090
+ }
2091
+ reasons.push(lay.cur[lay.colUnc[i]].lt(ft.preVector[i]));
2092
+ }
2093
+ for (const inh of ft.inhibitorPlaces) {
2094
+ reasons.push(lay.cur[lay.colUnc[inh]].gt(0));
2095
+ }
2096
+ for (const rd of ft.readPlaces) {
2097
+ if (envInj.has(rd)) {
2098
+ const bound = envInj.get(rd);
2099
+ if (bound !== null && bound < 1) permanentlyDisabled = true;
2100
+ continue;
2101
+ }
2102
+ reasons.push(lay.cur[lay.colUnc[rd]].lt(1));
2103
+ }
2104
+ return { reasons, permanentlyDisabled };
2105
+ }
2106
+ function colouredDisabledTerm(ctx, cls, plan, lay) {
2107
+ const k = plan.k;
2108
+ switch (cls.kind) {
2109
+ case "untouched":
2110
+ return null;
2111
+ case "mint": {
2112
+ const perColour = [];
2113
+ for (let c = 0; c < k; c++) {
2114
+ const present = plan.coloured.map((q) => lay.cur[lay.colCol[q][c]].ge(1));
2115
+ perColour.push(orAll(ctx, present));
2116
+ }
2117
+ return andAll(ctx, perColour);
2118
+ }
2119
+ case "join": {
2120
+ const perColour = [];
2121
+ for (let c = 0; c < k; c++) {
2122
+ const missing = cls.colouredIn.map((i) => lay.cur[lay.colCol[i][c]].eq(0));
2123
+ perColour.push(orAll(ctx, missing));
2124
+ }
2125
+ return andAll(ctx, perColour);
2126
+ }
2127
+ case "consume": {
2128
+ const perColour = [];
2129
+ for (let c = 0; c < k; c++) {
2130
+ perColour.push(lay.cur[lay.colCol[cls.inputCol][c]].eq(0));
2131
+ }
2132
+ return andAll(ctx, perColour);
2133
+ }
2134
+ }
2135
+ }
2136
+ function encodeColouredDeadlock(ctx, plan, lay, flat, sinkPlaces) {
2137
+ const envInj = injectedEnvIndices2(flat);
2138
+ const disabledConditions = [];
2139
+ for (let ti = 0; ti < plan.classes.length; ti++) {
2140
+ const cls = plan.classes[ti];
2141
+ const ft = flat.transitions[ti];
2142
+ const { reasons, permanentlyDisabled } = uncolouredDisable(ft, lay, plan, envInj);
2143
+ if (permanentlyDisabled) {
2144
+ disabledConditions.push(ctx.Bool.val(true));
2145
+ continue;
2146
+ }
2147
+ const term = colouredDisabledTerm(ctx, cls, plan, lay);
2148
+ if (term !== null) reasons.push(term);
2149
+ if (reasons.length === 0) {
1923
2150
  return ctx.Bool.val(false);
2151
+ }
2152
+ disabledConditions.push(reasons.length === 1 ? reasons[0] : orAll(ctx, reasons));
2153
+ }
2154
+ const sinkIndices = /* @__PURE__ */ new Set();
2155
+ for (const sink of sinkPlaces) {
2156
+ const idx = flatNetIndexOf(flat, sink);
2157
+ if (idx >= 0) sinkIndices.add(idx);
1924
2158
  }
2159
+ if (sinkIndices.size > 0) {
2160
+ const nonSink = [];
2161
+ for (let pid = 0; pid < flat.places.length; pid++) {
2162
+ if (sinkIndices.has(pid)) continue;
2163
+ nonSink.push(aggregate(plan, lay, pid, lay.cur).ge(1));
2164
+ }
2165
+ if (nonSink.length > 0) {
2166
+ disabledConditions.push(orAll(ctx, nonSink));
2167
+ }
2168
+ }
2169
+ return andAll(ctx, disabledConditions);
1925
2170
  }
1926
2171
 
1927
2172
  // src/verification/analysis/name-fragment.ts
@@ -2127,7 +2372,7 @@ var NameStateClassGraph = class _NameStateClassGraph {
2127
2372
  markingOf(idx) {
2128
2373
  return this.classes[idx].base.marking;
2129
2374
  }
2130
- static build(net, initialMarking, fragment, maxClasses, environmentPlaces, environmentMode) {
2375
+ static build(net, initialMarking, fragment, maxClasses, environmentPlaces, environmentMode, prioritySemantics = "none") {
2131
2376
  const envMode = environmentMode ?? ignore();
2132
2377
  const envPlaces = /* @__PURE__ */ new Set();
2133
2378
  if (environmentPlaces) {
@@ -2147,7 +2392,20 @@ var NameStateClassGraph = class _NameStateClassGraph {
2147
2392
  }
2148
2393
  const curIdx = queue.shift();
2149
2394
  const current = graph.classes[curIdx];
2150
- for (const transition of current.base.enabledTransitions) {
2395
+ const enabled = current.base.enabledTransitions;
2396
+ for (let idxL = 0; idxL < enabled.length; idxL++) {
2397
+ const transition = enabled[idxL];
2398
+ if (prioritySemantics === "conflict" && priorityDominated(
2399
+ transition,
2400
+ idxL,
2401
+ enabled,
2402
+ current.base.readyEarliest,
2403
+ current.base.marking,
2404
+ current.names,
2405
+ fragment
2406
+ )) {
2407
+ continue;
2408
+ }
2151
2409
  const role = fragment.role(transition.name);
2152
2410
  for (const vt of expandTransition(transition)) {
2153
2411
  const baseSucc = computeSuccessor(net, current.base, vt, envPlaces, envMode);
@@ -2179,6 +2437,57 @@ var NameStateClassGraph = class _NameStateClassGraph {
2179
2437
  this._successors[from].push(to);
2180
2438
  }
2181
2439
  };
2440
+ var READY_EPS = 1e-9;
2441
+ function priorityDominated(l, idxL, enabled, readyEarliest, marking, names, fragment) {
2442
+ return enabled.some(
2443
+ (h, idxH) => h !== l && h.priority > l.priority && readyEarliest[idxH] <= readyEarliest[idxL] + READY_EPS && willFire(h, names, fragment) && sharesConsumedInput(h, l, marking)
2444
+ );
2445
+ }
2446
+ function willFire(h, names, fragment) {
2447
+ const role = fragment.role(h.name);
2448
+ switch (role.type) {
2449
+ case "join":
2450
+ return enablingSymbols(names, role.colouredIn).length > 0;
2451
+ case "consume":
2452
+ return names.symbolsIn(role.colouredInput).length > 0;
2453
+ case "ordinary":
2454
+ case "mint":
2455
+ return true;
2456
+ default: {
2457
+ const _exhaustive = role;
2458
+ return _exhaustive;
2459
+ }
2460
+ }
2461
+ }
2462
+ function sharesConsumedInput(h, l, marking) {
2463
+ const lIns = /* @__PURE__ */ new Set();
2464
+ for (const p of l.inputPlaces()) lIns.add(p.name);
2465
+ for (const p of h.inputPlaces()) {
2466
+ if (lIns.has(p.name) && marking.tokens(p) < consumedDemand(h, p.name) + consumedDemand(l, p.name)) {
2467
+ return true;
2468
+ }
2469
+ }
2470
+ return false;
2471
+ }
2472
+ function consumedDemand(t, placeName) {
2473
+ let demand = 0;
2474
+ for (const spec of t.inputSpecs) {
2475
+ if (spec.place.name === placeName) demand += inputRequiredCount2(spec);
2476
+ }
2477
+ return demand;
2478
+ }
2479
+ function inputRequiredCount2(spec) {
2480
+ switch (spec.type) {
2481
+ case "one":
2482
+ return 1;
2483
+ case "exactly":
2484
+ return spec.count;
2485
+ case "all":
2486
+ return 1;
2487
+ case "at-least":
2488
+ return spec.minimum;
2489
+ }
2490
+ }
2182
2491
  function colouredOutputs(outputPlaces, fragment) {
2183
2492
  return [...outputPlaces].filter((p) => fragment.isColoured(p.name)).map((p) => p.name);
2184
2493
  }
@@ -2238,13 +2547,21 @@ function enablingSymbols(names, colouredIn) {
2238
2547
 
2239
2548
  // src/verification/nu-scg-verifier.ts
2240
2549
  var NOTE_EXACT = "\nNote: \u03BD-join correlation decided exactly via the state-class-graph name-partition quotient \u2014 the symbolic graph closed, so the verdict is sound AND complete (no spurious different-name counterexample; quiescence is name-aware), beyond the bounded-budget fragment (NU-050, Route B).\n";
2241
- function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces, environmentMode, maxClasses, fragmentMode, carrierPlaces) {
2550
+ function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces, environmentMode, maxClasses, fragmentMode, carrierPlaces, prioritySemantics) {
2242
2551
  const fragment = classify(net, fragmentMode, carrierPlaces);
2243
2552
  if (fragment === null) return null;
2244
2553
  for (const p of initial.placesWithTokens()) {
2245
2554
  if (fragment.isColoured(p.name)) return null;
2246
2555
  }
2247
- const scg = NameStateClassGraph.build(net, initial, fragment, maxClasses, environmentPlaces, environmentMode);
2556
+ const scg = NameStateClassGraph.build(
2557
+ net,
2558
+ initial,
2559
+ fragment,
2560
+ maxClasses,
2561
+ environmentPlaces,
2562
+ environmentMode,
2563
+ prioritySemantics
2564
+ );
2248
2565
  if (!scg.isComplete()) {
2249
2566
  return {
2250
2567
  verdict: {
@@ -2352,6 +2669,7 @@ var SmtVerifier = class _SmtVerifier {
2352
2669
  _nuMaxClasses = 1e5;
2353
2670
  _fragmentMode = "base";
2354
2671
  _carrierPlaces = /* @__PURE__ */ new Set();
2672
+ _prioritySemantics = "none";
2355
2673
  static forNet(net) {
2356
2674
  return new _SmtVerifier(net);
2357
2675
  }
@@ -2443,6 +2761,18 @@ var SmtVerifier = class _SmtVerifier {
2443
2761
  }
2444
2762
  return this;
2445
2763
  }
2764
+ /**
2765
+ * Selects how the Route-B name-aware analyzer treats transition priority
2766
+ * (NU-052). Defaults to `'none'` (the priority-blind over-approximation).
2767
+ * `'conflict'` models the executor's conflict-only priority resolution, so a
2768
+ * lower-priority transition pre-empted by a conflicting, no-later-ready,
2769
+ * strictly-higher-priority one is not explored — removing spurious
2770
+ * dead-letter-drain stalls the eager, priority-ordered executor never produces.
2771
+ */
2772
+ prioritySemantics(semantics) {
2773
+ this._prioritySemantics = semantics;
2774
+ return this;
2775
+ }
2446
2776
  /**
2447
2777
  * Runs the verification pipeline.
2448
2778
  */
@@ -2467,9 +2797,11 @@ var SmtVerifier = class _SmtVerifier {
2467
2797
  this._environmentMode,
2468
2798
  this._nuMaxClasses,
2469
2799
  this._fragmentMode,
2470
- this._carrierPlaces
2800
+ this._carrierPlaces,
2801
+ this._prioritySemantics
2471
2802
  );
2472
- if (outcome !== null) {
2803
+ const deferToRouteA = outcome !== null && outcome.verdict.type === "unknown" && !isReachabilitySafety(this._property) && nuBounded;
2804
+ if (outcome !== null && !deferToRouteA) {
2473
2805
  report.push("=== \u03BD-net Route B: name-aware state-class graph (NU-050) ===");
2474
2806
  report.push(` Name-partition state classes: ${outcome.classCount}`);
2475
2807
  report.push(outcome.note);
@@ -2491,8 +2823,12 @@ var SmtVerifier = class _SmtVerifier {
2491
2823
  structuralResult: "n/a (\u03BD name-partition SCG)"
2492
2824
  }
2493
2825
  );
2826
+ } else if (deferToRouteA) {
2827
+ report.push(
2828
+ "\u03BD-net Route B inconclusive (name-partition truncated); deferring to Route A coloured IC3/PDR (NU-053)."
2829
+ );
2494
2830
  }
2495
- if (this._fragmentMode === "extended") {
2831
+ if (this._fragmentMode === "extended" && !deferToRouteA) {
2496
2832
  report.push(
2497
2833
  "\u03BD-net Route B (EXTENDED) declined: net outside coloured-consumer fragment (a coloured place consumed count != 1 or by multiple inputs, carries a reset/read/inhibitor arc, or a join re-mints a coloured place); verified via sound over-approximation instead."
2498
2834
  );
@@ -2540,6 +2876,7 @@ var SmtVerifier = class _SmtVerifier {
2540
2876
  report.push("Phase 3: Computing P-invariants...");
2541
2877
  const matrix = IncidenceMatrix.from(flatNet);
2542
2878
  const invariants = computePInvariants(matrix, flatNet, this._initialMarking);
2879
+ const semiflows = computePSemiflows(matrix, flatNet, this._initialMarking);
2543
2880
  report.push(` Found: ${invariants.length} P-invariant(s)`);
2544
2881
  const structurallyBounded = isCoveredByInvariants(invariants, flatNet.places.length);
2545
2882
  report.push(` Structurally bounded: ${structurallyBounded ? "YES" : "NO"}`);
@@ -2548,7 +2885,15 @@ var SmtVerifier = class _SmtVerifier {
2548
2885
  }
2549
2886
  report.push("");
2550
2887
  report.push("Phase 4: IC3/PDR verification via Z3 Spacer...");
2551
- const colouredPlan = hasMatch && nuBounded && isReachabilitySafety(this._property) ? buildColouredPlan(this.net, flatNet, this._initialMarking, this._budgetPlaces) : null;
2888
+ const colouredPlan = hasMatch && nuBounded ? buildColouredPlan(
2889
+ this.net,
2890
+ flatNet,
2891
+ this._initialMarking,
2892
+ this._budgetPlaces,
2893
+ this._fragmentMode,
2894
+ this._carrierPlaces,
2895
+ semiflows
2896
+ ) : null;
2552
2897
  let runner;
2553
2898
  try {
2554
2899
  runner = await createSpacerRunner(this._timeoutMs);
@@ -2574,7 +2919,23 @@ var SmtVerifier = class _SmtVerifier {
2574
2919
  report.push(
2575
2920
  ` \u03BD-encoding: name-coloured (exact within budget k=${colouredPlan.k}; ${colouredPlan.coloured.length} coloured place(s))`
2576
2921
  );
2577
- encoding = encodeColoured(runner.ctx, runner.fp, colouredPlan, flatNet, this._initialMarking, this._property, invariants);
2922
+ encoding = encodeColoured(runner.ctx, runner.fp, colouredPlan, flatNet, this._initialMarking, this._property, invariants, this._sinkPlaces);
2923
+ if (encoding == null) {
2924
+ const reason = "property names a place that does not resolve in the net; refusing to certify (the encoding would be vacuously proven)";
2925
+ report.push(" Status: UNKNOWN (unresolved property place)\n");
2926
+ report.push("=== RESULT ===\n");
2927
+ report.push(`UNKNOWN: ${reason}`);
2928
+ return buildResult(
2929
+ { type: "unknown", reason },
2930
+ report.join("\n"),
2931
+ invariants,
2932
+ [],
2933
+ [],
2934
+ [],
2935
+ performance.now() - start,
2936
+ { places: flatNet.places.length, transitions: flatNet.transitions.length, invariantsFound: invariants.length, structuralResult: structResultStr }
2937
+ );
2938
+ }
2578
2939
  } else {
2579
2940
  encoding = encode(runner.ctx, runner.fp, flatNet, this._initialMarking, this._property, invariants, this._sinkPlaces);
2580
2941
  }
@@ -2724,6 +3085,10 @@ var SmtVerifier = class _SmtVerifier {
2724
3085
  */
2725
3086
  applyNuGuard(result, hasMatch, nuBounded, exact) {
2726
3087
  if (!hasMatch || result.verdict.type === "unknown") return result;
3088
+ if (exact) {
3089
+ const note2 = "\nNote: \u03BD-join name equality is encoded exactly via bounded name-colouring (k = budget); the verdict is sound and complete within the budget bound \u2014 no spurious different-name counterexample (NU-050 #1 / NU-053).\n";
3090
+ return { ...result, report: result.report + note2 };
3091
+ }
2727
3092
  if (!isReachabilitySafety(this._property)) {
2728
3093
  return downgradeToUnknown(
2729
3094
  result,
@@ -2736,7 +3101,7 @@ var SmtVerifier = class _SmtVerifier {
2736
3101
  "\u03BD-matching transitions present with unbounded fresh names (no budget place declared via budgetPlaces(...)); reachability over unbounded fresh names is undecidable (NU-040) \u2014 declare the budget place(s) that gate minting to verify within the bounded fragment"
2737
3102
  );
2738
3103
  }
2739
- const note = exact ? "\nNote: \u03BD-join name equality is encoded exactly via bounded name-colouring (k = budget); the verdict is sound and complete within the budget bound \u2014 no spurious different-name counterexample (NU-050 #1).\n" : "\nNote: matched (\u03BD-join) transitions are over-approximated (name equality assumed satisfiable). 'proven' is sound; a 'violated' counterexample may be spurious pending the exact \u03BD-analysis (NU-050).\n";
3104
+ const note = "\nNote: matched (\u03BD-join) transitions are over-approximated (name equality assumed satisfiable). 'proven' is sound; a 'violated' counterexample may be spurious pending the exact \u03BD-analysis (NU-050).\n";
2740
3105
  return { ...result, report: result.report + note };
2741
3106
  }
2742
3107
  };
@@ -2822,6 +3187,7 @@ export {
2822
3187
  pInvariant,
2823
3188
  pInvariantToString,
2824
3189
  computePInvariants,
3190
+ computePSemiflows,
2825
3191
  isCoveredByInvariants,
2826
3192
  structuralCheck,
2827
3193
  findMinimalSiphons,
@@ -2839,4 +3205,4 @@ export {
2839
3205
  isProven,
2840
3206
  isViolated
2841
3207
  };
2842
- //# sourceMappingURL=chunk-YKJQ7IKZ.js.map
3208
+ //# sourceMappingURL=chunk-V3WTQRHC.js.map