libpetri 2.10.5 → 2.12.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.
@@ -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,15 +2047,130 @@ 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);
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
+ }
1924
2168
  }
2169
+ return andAll(ctx, disabledConditions);
1925
2170
  }
1926
2171
 
1927
2172
  // src/verification/analysis/name-fragment.ts
1928
- function classify(net) {
2173
+ function classify(net, mode, carrierPlaces) {
1929
2174
  const coloured = /* @__PURE__ */ new Set();
1930
2175
  let anyMatch = false;
1931
2176
  for (const t of net.transitions) {
@@ -1935,9 +2180,18 @@ function classify(net) {
1935
2180
  }
1936
2181
  }
1937
2182
  if (!anyMatch || coloured.size === 0) return null;
2183
+ if (mode === "extended") {
2184
+ for (const c of carrierPlaces) coloured.add(c);
2185
+ }
2186
+ for (const t of net.transitions) {
2187
+ if (t.resets.some((r) => coloured.has(r.place.name)) || t.reads.some((r) => coloured.has(r.place.name)) || t.inhibitors.some((i) => coloured.has(i.place.name))) {
2188
+ return null;
2189
+ }
2190
+ }
1938
2191
  const roles = /* @__PURE__ */ new Map();
1939
2192
  for (const t of net.transitions) {
1940
- const consumesColoured = t.inputSpecs.some((s) => coloured.has(s.place.name));
2193
+ const colouredInputs = t.inputSpecs.filter((s) => coloured.has(s.place.name));
2194
+ const consumesColoured = colouredInputs.length > 0;
1941
2195
  let producesColoured = false;
1942
2196
  if (t.outputSpec !== null) {
1943
2197
  for (const branch of enumerateBranches(t.outputSpec)) {
@@ -1958,11 +2212,16 @@ function classify(net) {
1958
2212
  }
1959
2213
  colouredIn.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
1960
2214
  role = { type: "join", colouredIn };
2215
+ } else if (consumesColoured) {
2216
+ if (mode === "base") return null;
2217
+ if (colouredInputs.length !== 1) return null;
2218
+ const spec = colouredInputs[0];
2219
+ const countOne = spec.type === "one" || spec.type === "exactly" && spec.count === 1;
2220
+ if (!countOne) return null;
2221
+ role = { type: "consume", colouredInput: spec.place.name };
1961
2222
  } else if (producesColoured) {
1962
- if (consumesColoured) return null;
1963
2223
  role = { type: "mint" };
1964
2224
  } else {
1965
- if (consumesColoured) return null;
1966
2225
  role = { type: "ordinary" };
1967
2226
  }
1968
2227
  roles.set(t.name, role);
@@ -2113,7 +2372,7 @@ var NameStateClassGraph = class _NameStateClassGraph {
2113
2372
  markingOf(idx) {
2114
2373
  return this.classes[idx].base.marking;
2115
2374
  }
2116
- static build(net, initialMarking, fragment, maxClasses, environmentPlaces, environmentMode) {
2375
+ static build(net, initialMarking, fragment, maxClasses, environmentPlaces, environmentMode, prioritySemantics = "none") {
2117
2376
  const envMode = environmentMode ?? ignore();
2118
2377
  const envPlaces = /* @__PURE__ */ new Set();
2119
2378
  if (environmentPlaces) {
@@ -2133,7 +2392,20 @@ var NameStateClassGraph = class _NameStateClassGraph {
2133
2392
  }
2134
2393
  const curIdx = queue.shift();
2135
2394
  const current = graph.classes[curIdx];
2136
- 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
+ }
2137
2409
  const role = fragment.role(transition.name);
2138
2410
  for (const vt of expandTransition(transition)) {
2139
2411
  const baseSucc = computeSuccessor(net, current.base, vt, envPlaces, envMode);
@@ -2165,12 +2437,66 @@ var NameStateClassGraph = class _NameStateClassGraph {
2165
2437
  this._successors[from].push(to);
2166
2438
  }
2167
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
+ }
2491
+ function colouredOutputs(outputPlaces, fragment) {
2492
+ return [...outputPlaces].filter((p) => fragment.isColoured(p.name)).map((p) => p.name);
2493
+ }
2168
2494
  function nameSuccessors(role, names, outputPlaces, fragment, sym) {
2169
2495
  switch (role.type) {
2170
2496
  case "ordinary":
2171
2497
  return [names.copy()];
2172
2498
  case "mint": {
2173
- const colouredOut = [...outputPlaces].filter((p) => fragment.isColoured(p.name)).map((p) => p.name);
2499
+ const colouredOut = colouredOutputs(outputPlaces, fragment);
2174
2500
  const nm = names.copy();
2175
2501
  if (colouredOut.length > 0) {
2176
2502
  const fresh = sym.next++;
@@ -2187,6 +2513,17 @@ function nameSuccessors(role, names, outputPlaces, fragment, sym) {
2187
2513
  }
2188
2514
  return result;
2189
2515
  }
2516
+ case "consume": {
2517
+ const colouredOut = colouredOutputs(outputPlaces, fragment);
2518
+ const result = [];
2519
+ for (const s of names.symbolsIn(role.colouredInput)) {
2520
+ const nm = names.copy();
2521
+ nm.remove(role.colouredInput, s, 1);
2522
+ for (const p of colouredOut) nm.add(p, s, 1);
2523
+ result.push(nm);
2524
+ }
2525
+ return result;
2526
+ }
2190
2527
  }
2191
2528
  }
2192
2529
  function enablingSymbols(names, colouredIn) {
@@ -2210,13 +2547,21 @@ function enablingSymbols(names, colouredIn) {
2210
2547
 
2211
2548
  // src/verification/nu-scg-verifier.ts
2212
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";
2213
- function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces, environmentMode, maxClasses) {
2214
- const fragment = classify(net);
2550
+ function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces, environmentMode, maxClasses, fragmentMode, carrierPlaces, prioritySemantics) {
2551
+ const fragment = classify(net, fragmentMode, carrierPlaces);
2215
2552
  if (fragment === null) return null;
2216
2553
  for (const p of initial.placesWithTokens()) {
2217
2554
  if (fragment.isColoured(p.name)) return null;
2218
2555
  }
2219
- 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
+ );
2220
2565
  if (!scg.isComplete()) {
2221
2566
  return {
2222
2567
  verdict: {
@@ -2322,6 +2667,9 @@ var SmtVerifier = class _SmtVerifier {
2322
2667
  _environmentMode = alwaysAvailable();
2323
2668
  _timeoutMs = 6e4;
2324
2669
  _nuMaxClasses = 1e5;
2670
+ _fragmentMode = "base";
2671
+ _carrierPlaces = /* @__PURE__ */ new Set();
2672
+ _prioritySemantics = "none";
2325
2673
  static forNet(net) {
2326
2674
  return new _SmtVerifier(net);
2327
2675
  }
@@ -2381,6 +2729,50 @@ var SmtVerifier = class _SmtVerifier {
2381
2729
  this._nuMaxClasses = max;
2382
2730
  return this;
2383
2731
  }
2732
+ /**
2733
+ * Selects the ν-net coloured-place fragment for Route B (NU-051). `base`
2734
+ * (default) admits the shipped mint → matched-join fragment only; `extended`
2735
+ * additionally admits the opt-in coloured-consumer (drain/relay) role and the
2736
+ * declared {@link carrierPlaces}. When `extended` is requested but the net
2737
+ * falls outside the coloured-consumer fragment, Route B declines and a short
2738
+ * note is appended to the report before falling back to the sound
2739
+ * over-approximation.
2740
+ */
2741
+ fragmentMode(mode) {
2742
+ this._fragmentMode = mode;
2743
+ return this;
2744
+ }
2745
+ /**
2746
+ * Declares ν-net *carrier* places (NU-051, EXTENDED only): intermediate places
2747
+ * that carry a fresh name from the minting fork onward to a ν-join input. Under
2748
+ * {@link fragmentMode} `extended` they are unioned into the coloured set so the
2749
+ * existing mint co-mints one fresh name into all of them; under `base` they are
2750
+ * ignored. Accumulating. Throws if a declared place is not in the net — a
2751
+ * mistyped carrier name would let two fork branches mint independent names, so
2752
+ * the join never becomes name-enabled and the verifier would otherwise report a
2753
+ * confident false deadlock; it must surface, never silently proceed.
2754
+ */
2755
+ carrierPlaces(...places) {
2756
+ for (const p of places) {
2757
+ if (![...this.net.places].some((np) => np.name === p.name)) {
2758
+ throw new Error(`declared carrier place '${p.name}' not in the net`);
2759
+ }
2760
+ this._carrierPlaces.add(p.name);
2761
+ }
2762
+ return this;
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
+ }
2384
2776
  /**
2385
2777
  * Runs the verification pipeline.
2386
2778
  */
@@ -2403,9 +2795,13 @@ var SmtVerifier = class _SmtVerifier {
2403
2795
  this._sinkPlaces,
2404
2796
  this._environmentPlaces,
2405
2797
  this._environmentMode,
2406
- this._nuMaxClasses
2798
+ this._nuMaxClasses,
2799
+ this._fragmentMode,
2800
+ this._carrierPlaces,
2801
+ this._prioritySemantics
2407
2802
  );
2408
- if (outcome !== null) {
2803
+ const deferToRouteA = outcome !== null && outcome.verdict.type === "unknown" && !isReachabilitySafety(this._property) && nuBounded;
2804
+ if (outcome !== null && !deferToRouteA) {
2409
2805
  report.push("=== \u03BD-net Route B: name-aware state-class graph (NU-050) ===");
2410
2806
  report.push(` Name-partition state classes: ${outcome.classCount}`);
2411
2807
  report.push(outcome.note);
@@ -2427,6 +2823,15 @@ var SmtVerifier = class _SmtVerifier {
2427
2823
  structuralResult: "n/a (\u03BD name-partition SCG)"
2428
2824
  }
2429
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
+ );
2830
+ }
2831
+ if (this._fragmentMode === "extended" && !deferToRouteA) {
2832
+ report.push(
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."
2834
+ );
2430
2835
  }
2431
2836
  }
2432
2837
  report.push("Phase 1: Flattening net...");
@@ -2471,6 +2876,7 @@ var SmtVerifier = class _SmtVerifier {
2471
2876
  report.push("Phase 3: Computing P-invariants...");
2472
2877
  const matrix = IncidenceMatrix.from(flatNet);
2473
2878
  const invariants = computePInvariants(matrix, flatNet, this._initialMarking);
2879
+ const semiflows = computePSemiflows(matrix, flatNet, this._initialMarking);
2474
2880
  report.push(` Found: ${invariants.length} P-invariant(s)`);
2475
2881
  const structurallyBounded = isCoveredByInvariants(invariants, flatNet.places.length);
2476
2882
  report.push(` Structurally bounded: ${structurallyBounded ? "YES" : "NO"}`);
@@ -2479,7 +2885,15 @@ var SmtVerifier = class _SmtVerifier {
2479
2885
  }
2480
2886
  report.push("");
2481
2887
  report.push("Phase 4: IC3/PDR verification via Z3 Spacer...");
2482
- 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;
2483
2897
  let runner;
2484
2898
  try {
2485
2899
  runner = await createSpacerRunner(this._timeoutMs);
@@ -2505,7 +2919,23 @@ var SmtVerifier = class _SmtVerifier {
2505
2919
  report.push(
2506
2920
  ` \u03BD-encoding: name-coloured (exact within budget k=${colouredPlan.k}; ${colouredPlan.coloured.length} coloured place(s))`
2507
2921
  );
2508
- 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
+ }
2509
2939
  } else {
2510
2940
  encoding = encode(runner.ctx, runner.fp, flatNet, this._initialMarking, this._property, invariants, this._sinkPlaces);
2511
2941
  }
@@ -2655,6 +3085,10 @@ var SmtVerifier = class _SmtVerifier {
2655
3085
  */
2656
3086
  applyNuGuard(result, hasMatch, nuBounded, exact) {
2657
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
+ }
2658
3092
  if (!isReachabilitySafety(this._property)) {
2659
3093
  return downgradeToUnknown(
2660
3094
  result,
@@ -2667,7 +3101,7 @@ var SmtVerifier = class _SmtVerifier {
2667
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"
2668
3102
  );
2669
3103
  }
2670
- 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";
2671
3105
  return { ...result, report: result.report + note };
2672
3106
  }
2673
3107
  };
@@ -2753,6 +3187,7 @@ export {
2753
3187
  pInvariant,
2754
3188
  pInvariantToString,
2755
3189
  computePInvariants,
3190
+ computePSemiflows,
2756
3191
  isCoveredByInvariants,
2757
3192
  structuralCheck,
2758
3193
  findMinimalSiphons,
@@ -2770,4 +3205,4 @@ export {
2770
3205
  isProven,
2771
3206
  isViolated
2772
3207
  };
2773
- //# sourceMappingURL=chunk-M3KT7BFO.js.map
3208
+ //# sourceMappingURL=chunk-V3WTQRHC.js.map