libpetri 4.1.0 → 5.1.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/dist/{chunk-GJAHGHGT.js → chunk-BPGE7GZR.js} +2 -2
- package/dist/{chunk-FD3S4TZM.js → chunk-EL4E6LVO.js} +1093 -197
- package/dist/chunk-EL4E6LVO.js.map +1 -0
- package/dist/debug/index.d.ts +2 -2
- package/dist/doclet/index.d.ts +1 -1
- package/dist/doclet/resources/petrinet-diagrams.js +4892 -4896
- package/dist/{event-store-B3ppVTpO.d.ts → event-store-CUyHiIH7.d.ts} +1 -1
- package/dist/export/index.d.ts +1 -1
- package/dist/index.d.ts +65 -13
- package/dist/index.js +195 -73
- package/dist/index.js.map +1 -1
- package/dist/{petri-net-p0ONpSAO.d.ts → petri-net-6G5r1Wwb.d.ts} +82 -4
- package/dist/render-dom/index.js +1 -1
- package/dist/verification/index.d.ts +435 -14
- package/dist/verification/index.js +33 -1
- package/dist/verification/index.js.map +1 -1
- package/dist/viewer/index.js +1 -1
- package/dist/viewer/viewer.iife.js +4892 -4896
- package/package.json +5 -5
- package/dist/chunk-FD3S4TZM.js.map +0 -1
- /package/dist/{chunk-GJAHGHGT.js.map → chunk-BPGE7GZR.js.map} +0 -0
|
@@ -220,6 +220,11 @@ function withTimeout(action, timeoutMs, timeoutPlace2, timeoutValue) {
|
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// src/verification/programming-error.ts
|
|
224
|
+
function rethrowIfProgrammingError(e) {
|
|
225
|
+
if (e instanceof TypeError || e instanceof ReferenceError) throw e;
|
|
226
|
+
}
|
|
227
|
+
|
|
223
228
|
// src/verification/marking-state.ts
|
|
224
229
|
var MARKING_STATE_KEY = /* @__PURE__ */ Symbol("MarkingState.internal");
|
|
225
230
|
var MarkingState = class _MarkingState {
|
|
@@ -252,9 +257,9 @@ var MarkingState = class _MarkingState {
|
|
|
252
257
|
}
|
|
253
258
|
/** Returns the total number of tokens. */
|
|
254
259
|
totalTokens() {
|
|
255
|
-
let
|
|
256
|
-
for (const count of this.tokenCounts.values())
|
|
257
|
-
return
|
|
260
|
+
let sum2 = 0;
|
|
261
|
+
for (const count of this.tokenCounts.values()) sum2 += count;
|
|
262
|
+
return sum2;
|
|
258
263
|
}
|
|
259
264
|
/** Checks if no tokens exist anywhere. */
|
|
260
265
|
isEmpty() {
|
|
@@ -331,6 +336,9 @@ var MarkingStateBuilder = class {
|
|
|
331
336
|
function deadlockFree() {
|
|
332
337
|
return { type: "deadlock-free" };
|
|
333
338
|
}
|
|
339
|
+
function terminatesAtSink() {
|
|
340
|
+
return { type: "terminates-at-sink" };
|
|
341
|
+
}
|
|
334
342
|
function mutualExclusion(p1, p2) {
|
|
335
343
|
return { type: "mutual-exclusion", p1, p2 };
|
|
336
344
|
}
|
|
@@ -350,6 +358,8 @@ function propertyDescription(prop) {
|
|
|
350
358
|
switch (prop.type) {
|
|
351
359
|
case "deadlock-free":
|
|
352
360
|
return "Deadlock-freedom";
|
|
361
|
+
case "terminates-at-sink":
|
|
362
|
+
return "Terminates at a declared sink";
|
|
353
363
|
case "mutual-exclusion":
|
|
354
364
|
return `Mutual exclusion of ${prop.p1.name} and ${prop.p2.name}`;
|
|
355
365
|
case "place-bound":
|
|
@@ -363,6 +373,53 @@ function propertyDescription(prop) {
|
|
|
363
373
|
}
|
|
364
374
|
}
|
|
365
375
|
|
|
376
|
+
// src/verification/rest-set.ts
|
|
377
|
+
function strandingExcuses(flatNet, sinkPlaces, conditional) {
|
|
378
|
+
const P = flatNet.places.length;
|
|
379
|
+
const excuses = new Array(P);
|
|
380
|
+
for (let pid = 0; pid < P; pid++) excuses[pid] = [];
|
|
381
|
+
for (const sink of sinkPlaces) {
|
|
382
|
+
const pid = flatNet.placeIndex.get(sink.name);
|
|
383
|
+
if (pid != null) excuses[pid] = null;
|
|
384
|
+
}
|
|
385
|
+
for (const { marker, places } of conditional) {
|
|
386
|
+
const mid = flatNet.placeIndex.get(marker.name);
|
|
387
|
+
if (mid == null) continue;
|
|
388
|
+
excuses[mid] = null;
|
|
389
|
+
for (const place of places) {
|
|
390
|
+
const pid = flatNet.placeIndex.get(place.name);
|
|
391
|
+
if (pid == null) continue;
|
|
392
|
+
const list = excuses[pid];
|
|
393
|
+
if (list != null && !list.includes(mid)) list.push(mid);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (const list of excuses) if (list != null) list.sort((a, b) => a - b);
|
|
397
|
+
return excuses;
|
|
398
|
+
}
|
|
399
|
+
function strandsToken(m, sinkPlaces, conditional) {
|
|
400
|
+
const resting = /* @__PURE__ */ new Set();
|
|
401
|
+
for (const s of sinkPlaces) resting.add(s.name);
|
|
402
|
+
for (const { marker, places } of conditional) {
|
|
403
|
+
resting.add(marker.name);
|
|
404
|
+
if (m.hasTokens(marker)) {
|
|
405
|
+
for (const p of places) resting.add(p.name);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const p of m.placesWithTokens()) {
|
|
409
|
+
if (!resting.has(p.name)) return true;
|
|
410
|
+
}
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
function describeSinks(sinkPlaces, conditional) {
|
|
414
|
+
const parts = [];
|
|
415
|
+
if (sinkPlaces.size > 0) parts.push(`sinks: ${[...sinkPlaces].map((p) => p.name).join(", ")}`);
|
|
416
|
+
for (const { marker, places } of conditional) {
|
|
417
|
+
const names = [...places].map((p) => p.name);
|
|
418
|
+
parts.push(names.length === 0 ? `when ${marker.name}` : `when ${marker.name}: ${names.join(", ")}`);
|
|
419
|
+
}
|
|
420
|
+
return parts.length === 0 ? null : parts.join("; ");
|
|
421
|
+
}
|
|
422
|
+
|
|
366
423
|
// src/verification/encoding/flat-transition.ts
|
|
367
424
|
function flatTransition(name, source, branchIndex, preVector, postVector, inhibitorPlaces, readPlaces, resetPlaces, consumeAll) {
|
|
368
425
|
return {
|
|
@@ -821,6 +878,8 @@ function strengthenWithSemiflows(invariants, semiflows) {
|
|
|
821
878
|
}
|
|
822
879
|
return { invariants: strengthened, added };
|
|
823
880
|
}
|
|
881
|
+
var MAX_SEMIFLOW_ROWS = 8192;
|
|
882
|
+
var MAX_SEMIFLOW_CANDIDATES = 65536;
|
|
824
883
|
function computePSemiflows(matrix, flatNet, initialMarking) {
|
|
825
884
|
const np = matrix.numPlaces();
|
|
826
885
|
const nt = matrix.numTransitions();
|
|
@@ -838,19 +897,21 @@ function computePSemiflows(matrix, flatNet, initialMarking) {
|
|
|
838
897
|
const next = rows.filter((r) => r.sig[t] === 0);
|
|
839
898
|
const pos = rows.filter((r) => r.sig[t] > 0);
|
|
840
899
|
const neg = rows.filter((r) => r.sig[t] < 0);
|
|
841
|
-
|
|
842
|
-
for (const
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
900
|
+
outer:
|
|
901
|
+
for (const rp of pos) {
|
|
902
|
+
for (const rn of neg) {
|
|
903
|
+
if (next.length >= MAX_SEMIFLOW_CANDIDATES) break outer;
|
|
904
|
+
const cp = -rn.sig[t];
|
|
905
|
+
const cn = rp.sig[t];
|
|
906
|
+
const sig = combineRow(cp, rp.sig, cn, rn.sig);
|
|
907
|
+
const weight = combineRow(cp, rp.weight, cn, rn.weight);
|
|
908
|
+
if (sig === null || weight === null) continue;
|
|
909
|
+
reduceGcd(sig, weight);
|
|
910
|
+
next.push({ sig, weight });
|
|
911
|
+
}
|
|
850
912
|
}
|
|
851
|
-
}
|
|
852
913
|
rows = keepSupportMinimal(next);
|
|
853
|
-
if (rows.length >
|
|
914
|
+
if (rows.length > MAX_SEMIFLOW_ROWS) rows.length = MAX_SEMIFLOW_ROWS;
|
|
854
915
|
}
|
|
855
916
|
const semiflows = [];
|
|
856
917
|
for (const { weight } of rows) {
|
|
@@ -887,17 +948,43 @@ function reduceGcd(sig, weight) {
|
|
|
887
948
|
}
|
|
888
949
|
}
|
|
889
950
|
function keepSupportMinimal(rows) {
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
for (let
|
|
899
|
-
if (
|
|
900
|
-
|
|
951
|
+
const n = rows.length;
|
|
952
|
+
if (n < 2) return rows;
|
|
953
|
+
const words = rows[0].weight.length + 31 >>> 5 || 1;
|
|
954
|
+
const bits = new Uint32Array(n * words);
|
|
955
|
+
const sizes = new Int32Array(n);
|
|
956
|
+
for (let i = 0; i < n; i++) {
|
|
957
|
+
const w = rows[i].weight;
|
|
958
|
+
let size = 0;
|
|
959
|
+
for (let p = 0; p < w.length; p++) {
|
|
960
|
+
if (w[p] !== 0) {
|
|
961
|
+
const idx = i * words + (p >>> 5);
|
|
962
|
+
bits[idx] = bits[idx] | 1 << (p & 31);
|
|
963
|
+
size++;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
sizes[i] = size;
|
|
967
|
+
}
|
|
968
|
+
const order = new Int32Array(n);
|
|
969
|
+
for (let i = 0; i < n; i++) order[i] = i;
|
|
970
|
+
order.sort((a, b) => sizes[a] - sizes[b]);
|
|
971
|
+
const keep = new Array(n).fill(true);
|
|
972
|
+
for (let oi = 0; oi < n; oi++) {
|
|
973
|
+
const i = order[oi];
|
|
974
|
+
const base = i * words;
|
|
975
|
+
for (let oj = 0; oj < oi; oj++) {
|
|
976
|
+
const j = order[oj];
|
|
977
|
+
if (sizes[j] >= sizes[i]) break;
|
|
978
|
+
const jbase = j * words;
|
|
979
|
+
let subset = true;
|
|
980
|
+
for (let w = 0; w < words; w++) {
|
|
981
|
+
const jb = bits[jbase + w];
|
|
982
|
+
if ((jb & ~bits[base + w]) !== 0) {
|
|
983
|
+
subset = false;
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (subset) {
|
|
901
988
|
keep[i] = false;
|
|
902
989
|
break;
|
|
903
990
|
}
|
|
@@ -1236,7 +1323,8 @@ function locateZ3(program, env = process.env) {
|
|
|
1236
1323
|
const isFile = (p) => {
|
|
1237
1324
|
try {
|
|
1238
1325
|
return existsSync(p) && statSync(p).isFile();
|
|
1239
|
-
} catch {
|
|
1326
|
+
} catch (e) {
|
|
1327
|
+
rethrowIfProgrammingError(e);
|
|
1240
1328
|
return false;
|
|
1241
1329
|
}
|
|
1242
1330
|
};
|
|
@@ -1247,9 +1335,9 @@ function locateZ3(program, env = process.env) {
|
|
|
1247
1335
|
const windows = process.platform === "win32";
|
|
1248
1336
|
for (const dir of searchPath.split(path.delimiter)) {
|
|
1249
1337
|
if (dir === "") continue;
|
|
1250
|
-
const
|
|
1251
|
-
if (isFile(
|
|
1252
|
-
if (windows && isFile(
|
|
1338
|
+
const candidate = path.join(dir, program);
|
|
1339
|
+
if (isFile(candidate)) return candidate;
|
|
1340
|
+
if (windows && isFile(candidate + ".exe")) return candidate + ".exe";
|
|
1253
1341
|
}
|
|
1254
1342
|
return null;
|
|
1255
1343
|
}
|
|
@@ -1295,7 +1383,8 @@ function z3Available(env = process.env) {
|
|
|
1295
1383
|
try {
|
|
1296
1384
|
resolveZ3(env);
|
|
1297
1385
|
return true;
|
|
1298
|
-
} catch {
|
|
1386
|
+
} catch (e) {
|
|
1387
|
+
rethrowIfProgrammingError(e);
|
|
1299
1388
|
return false;
|
|
1300
1389
|
}
|
|
1301
1390
|
}
|
|
@@ -1368,6 +1457,7 @@ async function runZ3Spacer(solver, timeoutMs, smt2, phase) {
|
|
|
1368
1457
|
try {
|
|
1369
1458
|
reply = await runZ3Text(solver, smt2, phase, timeoutMs, ["fp.engine=spacer"]);
|
|
1370
1459
|
} catch (e) {
|
|
1460
|
+
rethrowIfProgrammingError(e);
|
|
1371
1461
|
return { type: "unknown", reason: String(e?.message ?? e) };
|
|
1372
1462
|
}
|
|
1373
1463
|
const stdout = reply.stdout.trim();
|
|
@@ -1386,36 +1476,50 @@ async function runZ3Spacer(solver, timeoutMs, smt2, phase) {
|
|
|
1386
1476
|
}
|
|
1387
1477
|
|
|
1388
1478
|
// src/verification/z3/smt-encoder.ts
|
|
1389
|
-
function encode(flatNet, initialMarking, property, invariants, sinkPlaces = /* @__PURE__ */ new Set(), produceProofs = false) {
|
|
1479
|
+
function encode(flatNet, initialMarking, property, invariants, sinkPlaces = /* @__PURE__ */ new Set(), produceProofs = false, conditionalSinks = []) {
|
|
1480
|
+
return encodeNet(flatNet, initialMarking, property, invariants, { sinkPlaces, produceProofs, conditionalSinks });
|
|
1481
|
+
}
|
|
1482
|
+
function encodeNet(flatNet, initialMarking, property, invariants, options = {}) {
|
|
1483
|
+
const sinkPlaces = options.sinkPlaces ?? /* @__PURE__ */ new Set();
|
|
1484
|
+
const produceProofs = options.produceProofs ?? false;
|
|
1485
|
+
const conditionalSinks = options.conditionalSinks ?? [];
|
|
1390
1486
|
const P = flatNet.places.length;
|
|
1487
|
+
const T = options.stateEquation ? flatNet.transitions.length : 0;
|
|
1391
1488
|
const lines = [];
|
|
1392
1489
|
const envInject = resolveEnvInjection(flatNet);
|
|
1393
1490
|
if (produceProofs) lines.push("(set-option :produce-proofs true)");
|
|
1394
1491
|
lines.push("(set-logic HORN)");
|
|
1395
1492
|
lines.push("");
|
|
1396
|
-
lines.push(`(declare-fun Reachable (${ints(P).join(" ")}) Bool)`);
|
|
1493
|
+
lines.push(`(declare-fun Reachable (${ints(P + T).join(" ")}) Bool)`);
|
|
1397
1494
|
lines.push("(declare-fun Error () Bool)");
|
|
1398
1495
|
lines.push("");
|
|
1399
1496
|
const mVars = vars(P, "");
|
|
1400
1497
|
const mpVars = vars(P, "p");
|
|
1498
|
+
const nVars = counterVars(T, "");
|
|
1499
|
+
const npVars = counterVars(T, "p");
|
|
1401
1500
|
const m0 = [];
|
|
1402
1501
|
for (let i = 0; i < P; i++) m0.push(String(initialMarking.tokens(flatNet.places[i])));
|
|
1502
|
+
for (let k = 0; k < T; k++) m0.push("0");
|
|
1403
1503
|
lines.push(`(assert (Reachable ${m0.join(" ")}))`);
|
|
1404
1504
|
lines.push("");
|
|
1405
|
-
|
|
1406
|
-
|
|
1505
|
+
const equation = T > 0 ? stateEquationConditions(flatNet, initialMarking, npVars) : [];
|
|
1506
|
+
for (let k = 0; k < flatNet.transitions.length; k++) {
|
|
1507
|
+
const ft = flatNet.transitions[k];
|
|
1508
|
+
const strengthening = [...invariantConditions(invariants, mpVars)];
|
|
1509
|
+
if (T > 0) strengthening.push(...counterConditions(k, nVars, npVars), ...equation);
|
|
1510
|
+
lines.push(encodeTransitionRule(flatNet, ft, mVars, mpVars, nVars, npVars, strengthening));
|
|
1407
1511
|
}
|
|
1408
1512
|
for (const inj of envInject) {
|
|
1409
|
-
lines.push(encodeInjectionRule(P, inj.pid, inj.bound, mVars, mpVars));
|
|
1513
|
+
lines.push(encodeInjectionRule(P, inj.pid, inj.bound, mVars, mpVars, nVars, npVars));
|
|
1410
1514
|
}
|
|
1411
1515
|
lines.push("");
|
|
1412
|
-
lines.push(encodeErrorRule(flatNet, property, mVars, sinkPlaces, envInject));
|
|
1516
|
+
lines.push(encodeErrorRule(flatNet, property, mVars, nVars, sinkPlaces, envInject, conditionalSinks));
|
|
1413
1517
|
lines.push("");
|
|
1414
1518
|
lines.push("(assert (not Error))");
|
|
1415
1519
|
lines.push("(check-sat)");
|
|
1416
1520
|
if (produceProofs) lines.push("(get-proof)");
|
|
1417
1521
|
lines.push("(get-model)");
|
|
1418
|
-
return { smt2: lines.join("\n"), placeCount: P };
|
|
1522
|
+
return { smt2: lines.join("\n"), placeCount: P, counterCount: T };
|
|
1419
1523
|
}
|
|
1420
1524
|
function resolveEnvInjection(flatNet) {
|
|
1421
1525
|
const out = [];
|
|
@@ -1443,9 +1547,44 @@ function vars(P, suffix) {
|
|
|
1443
1547
|
for (let i = 0; i < P; i++) out.push(`m${i}${suffix}`);
|
|
1444
1548
|
return out;
|
|
1445
1549
|
}
|
|
1550
|
+
function counterVars(T, suffix) {
|
|
1551
|
+
const out = [];
|
|
1552
|
+
for (let k = 0; k < T; k++) out.push(`n${k}${suffix}`);
|
|
1553
|
+
return out;
|
|
1554
|
+
}
|
|
1446
1555
|
function quantified(names) {
|
|
1447
1556
|
return names.map((v) => `(${v} Int)`).join(" ");
|
|
1448
1557
|
}
|
|
1558
|
+
function equationPlaces(flatNet) {
|
|
1559
|
+
const excluded = new Set(nonlinearPlaces(flatNet));
|
|
1560
|
+
for (const inj of resolveEnvInjection(flatNet)) excluded.add(inj.pid);
|
|
1561
|
+
const out = [];
|
|
1562
|
+
for (let p = 0; p < flatNet.places.length; p++) if (!excluded.has(p)) out.push(p);
|
|
1563
|
+
return out;
|
|
1564
|
+
}
|
|
1565
|
+
function counterConditions(fired, nVars, npVars) {
|
|
1566
|
+
const conditions = [];
|
|
1567
|
+
for (let k = 0; k < nVars.length; k++) {
|
|
1568
|
+
conditions.push(k === fired ? `(= ${npVars[k]} (+ ${nVars[k]} 1))` : `(= ${npVars[k]} ${nVars[k]})`);
|
|
1569
|
+
}
|
|
1570
|
+
for (let k = 0; k < npVars.length; k++) conditions.push(`(>= ${npVars[k]} 0)`);
|
|
1571
|
+
return conditions;
|
|
1572
|
+
}
|
|
1573
|
+
function stateEquationConditions(flatNet, initialMarking, nVars, mVars = vars(flatNet.places.length, "p")) {
|
|
1574
|
+
const conditions = [];
|
|
1575
|
+
for (const p of equationPlaces(flatNet)) {
|
|
1576
|
+
const terms = [];
|
|
1577
|
+
for (let t = 0; t < flatNet.transitions.length; t++) {
|
|
1578
|
+
const ft = flatNet.transitions[t];
|
|
1579
|
+
const c = ft.postVector[p] - ft.preVector[p];
|
|
1580
|
+
if (c === 0) continue;
|
|
1581
|
+
terms.push(c === 1 ? nVars[t] : c === -1 ? `(- ${nVars[t]})` : c > 0 ? `(* ${c} ${nVars[t]})` : `(* (- ${-c}) ${nVars[t]})`);
|
|
1582
|
+
}
|
|
1583
|
+
const m0 = initialMarking.tokens(flatNet.places[p]);
|
|
1584
|
+
conditions.push(terms.length === 0 ? `(= ${mVars[p]} ${m0})` : `(= ${mVars[p]} (+ ${m0} ${terms.join(" ")}))`);
|
|
1585
|
+
}
|
|
1586
|
+
return conditions;
|
|
1587
|
+
}
|
|
1449
1588
|
function firingConditions(flatNet, ft, mVars, mpVars) {
|
|
1450
1589
|
const P = flatNet.places.length;
|
|
1451
1590
|
const conditions = [];
|
|
@@ -1472,8 +1611,8 @@ function invariantConditions(invariants, names) {
|
|
|
1472
1611
|
for (const inv of invariants) {
|
|
1473
1612
|
const terms = [...inv.support].sort((a, b) => a - b).map((i) => `(* ${inv.weights[i]} ${names[i]})`);
|
|
1474
1613
|
if (terms.length === 0) continue;
|
|
1475
|
-
const
|
|
1476
|
-
conditions.push(`(= ${
|
|
1614
|
+
const sum2 = terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
|
|
1615
|
+
conditions.push(`(= ${sum2} ${inv.constant})`);
|
|
1477
1616
|
}
|
|
1478
1617
|
return conditions;
|
|
1479
1618
|
}
|
|
@@ -1489,50 +1628,61 @@ function injectionConditions(P, pid, bound, mVars, mpVars) {
|
|
|
1489
1628
|
}
|
|
1490
1629
|
return conditions;
|
|
1491
1630
|
}
|
|
1492
|
-
function encodeTransitionRule(flatNet, ft, mVars, mpVars,
|
|
1493
|
-
const conditions = [`(Reachable ${mVars.join(" ")})`];
|
|
1631
|
+
function encodeTransitionRule(flatNet, ft, mVars, mpVars, nVars, npVars, strengthening) {
|
|
1632
|
+
const conditions = [`(Reachable ${[...mVars, ...nVars].join(" ")})`];
|
|
1494
1633
|
conditions.push(...firingConditions(flatNet, ft, mVars, mpVars));
|
|
1495
|
-
conditions.push(...
|
|
1634
|
+
conditions.push(...strengthening);
|
|
1496
1635
|
conditions.push(...envBoundConditions(flatNet, mpVars));
|
|
1497
1636
|
const body = `(and ${conditions.join("\n ")})`;
|
|
1498
|
-
|
|
1637
|
+
const quantifiedVars = quantified([...mVars, ...mpVars, ...nVars, ...npVars]);
|
|
1638
|
+
return `(assert (forall (${quantifiedVars})
|
|
1499
1639
|
(=> ${body}
|
|
1500
|
-
(Reachable ${mpVars.join(" ")}))))`;
|
|
1640
|
+
(Reachable ${[...mpVars, ...npVars].join(" ")}))))`;
|
|
1501
1641
|
}
|
|
1502
|
-
function encodeInjectionRule(P, pid, bound, mVars, mpVars) {
|
|
1503
|
-
const conditions = [`(Reachable ${mVars.join(" ")})`];
|
|
1642
|
+
function encodeInjectionRule(P, pid, bound, mVars, mpVars, nVars, npVars) {
|
|
1643
|
+
const conditions = [`(Reachable ${[...mVars, ...nVars].join(" ")})`];
|
|
1504
1644
|
conditions.push(...injectionConditions(P, pid, bound, mVars, mpVars));
|
|
1645
|
+
if (nVars.length > 0) conditions.push(...counterConditions(-1, nVars, npVars));
|
|
1505
1646
|
const body = `(and ${conditions.join("\n ")})`;
|
|
1506
|
-
|
|
1647
|
+
const quantifiedVars = quantified([...mVars, ...mpVars, ...nVars, ...npVars]);
|
|
1648
|
+
return `(assert (forall (${quantifiedVars})
|
|
1507
1649
|
(=> ${body}
|
|
1508
|
-
(Reachable ${mpVars.join(" ")}))))`;
|
|
1650
|
+
(Reachable ${[...mpVars, ...npVars].join(" ")}))))`;
|
|
1509
1651
|
}
|
|
1510
1652
|
function conjoin(conditions) {
|
|
1511
1653
|
if (conditions.length === 0) return "true";
|
|
1512
1654
|
if (conditions.length === 1) return conditions[0];
|
|
1513
1655
|
return `(and ${conditions.join(" ")})`;
|
|
1514
1656
|
}
|
|
1515
|
-
function encodeStepRelationSmt2(flatNet) {
|
|
1657
|
+
function encodeStepRelationSmt2(flatNet, stateEquation = false) {
|
|
1516
1658
|
const P = flatNet.places.length;
|
|
1659
|
+
const T = stateEquation ? flatNet.transitions.length : 0;
|
|
1517
1660
|
const mVars = vars(P, "");
|
|
1518
1661
|
const mpVars = vars(P, "p");
|
|
1662
|
+
const nVars = counterVars(T, "");
|
|
1663
|
+
const npVars = counterVars(T, "p");
|
|
1519
1664
|
const disjuncts = [];
|
|
1520
|
-
for (
|
|
1665
|
+
for (let k = 0; k < flatNet.transitions.length; k++) {
|
|
1666
|
+
const ft = flatNet.transitions[k];
|
|
1521
1667
|
const conditions = firingConditions(flatNet, ft, mVars, mpVars);
|
|
1668
|
+
if (T > 0) conditions.push(...counterConditions(k, nVars, npVars));
|
|
1522
1669
|
conditions.push(...envBoundConditions(flatNet, mpVars));
|
|
1523
1670
|
disjuncts.push(conjoin(conditions));
|
|
1524
1671
|
}
|
|
1525
1672
|
for (const inj of resolveEnvInjection(flatNet)) {
|
|
1526
|
-
|
|
1673
|
+
const conditions = injectionConditions(P, inj.pid, inj.bound, mVars, mpVars);
|
|
1674
|
+
if (T > 0) conditions.push(...counterConditions(-1, nVars, npVars));
|
|
1675
|
+
disjuncts.push(conjoin(conditions));
|
|
1527
1676
|
}
|
|
1528
1677
|
if (disjuncts.length === 0) return "false";
|
|
1529
1678
|
if (disjuncts.length === 1) return disjuncts[0];
|
|
1530
1679
|
return `(or ${disjuncts.join("\n ")})`;
|
|
1531
1680
|
}
|
|
1532
|
-
function encodeErrorRule(flatNet, property, mVars, sinkPlaces, envInject) {
|
|
1533
|
-
const violation = encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject);
|
|
1534
|
-
|
|
1535
|
-
(
|
|
1681
|
+
function encodeErrorRule(flatNet, property, mVars, nVars, sinkPlaces, envInject, conditionalSinks) {
|
|
1682
|
+
const violation = encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject, conditionalSinks);
|
|
1683
|
+
const state = [...mVars, ...nVars];
|
|
1684
|
+
return `(assert (forall (${quantified(state)})
|
|
1685
|
+
(=> (and (Reachable ${state.join(" ")}) ${violation})
|
|
1536
1686
|
Error)))`;
|
|
1537
1687
|
}
|
|
1538
1688
|
function indexOrdered(flatNet, places) {
|
|
@@ -1543,10 +1693,30 @@ function indexOrdered(flatNet, places) {
|
|
|
1543
1693
|
}
|
|
1544
1694
|
return [...idx].sort((a, b) => a - b);
|
|
1545
1695
|
}
|
|
1546
|
-
function encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject) {
|
|
1696
|
+
function encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject, conditionalSinks = []) {
|
|
1547
1697
|
switch (property.type) {
|
|
1548
|
-
|
|
1549
|
-
|
|
1698
|
+
// DeadlockFree (VER-002): a quiescent marking that STRANDS a token — holds one
|
|
1699
|
+
// in a place where resting is not permitted. The empty marking strands nothing
|
|
1700
|
+
// and is therefore not a violation (AC4). A conditional sink (VER-014) is
|
|
1701
|
+
// stranded only while every marker that would excuse it is unmarked.
|
|
1702
|
+
case "deadlock-free": {
|
|
1703
|
+
const conditions = encodeQuiescent(flatNet, mVars, envInject);
|
|
1704
|
+
if (conditions == null) return "false";
|
|
1705
|
+
const stranded = strandedConditions(strandingExcuses(flatNet, sinkPlaces, conditionalSinks), mVars);
|
|
1706
|
+
if (stranded.length === 0) return "false";
|
|
1707
|
+
conditions.push(`(or ${stranded.join(" ")})`);
|
|
1708
|
+
return joinConditions(conditions);
|
|
1709
|
+
}
|
|
1710
|
+
// TerminatesAtSink (VER-002): a quiescent marking that reached NO declared sink.
|
|
1711
|
+
// This is the predicate DeadlockFree carried before the VER-002 split, unchanged.
|
|
1712
|
+
case "terminates-at-sink": {
|
|
1713
|
+
const conditions = encodeQuiescent(flatNet, mVars, envInject);
|
|
1714
|
+
if (conditions == null) return "false";
|
|
1715
|
+
for (const pid of indexOrdered(flatNet, sinkPlaces)) {
|
|
1716
|
+
conditions.push(`(= ${mVars[pid]} 0)`);
|
|
1717
|
+
}
|
|
1718
|
+
return joinConditions(conditions);
|
|
1719
|
+
}
|
|
1550
1720
|
case "mutual-exclusion": {
|
|
1551
1721
|
const conditions = indexOrdered(flatNet, [property.p1, property.p2]).map((i) => `(>= ${mVars[i]} 1)`);
|
|
1552
1722
|
return conditions.length === 0 ? "false" : `(and ${conditions.join(" ")})`;
|
|
@@ -1560,14 +1730,36 @@ function encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject
|
|
|
1560
1730
|
const conditions = indexOrdered(flatNet, property.places).map((i) => `(>= ${mVars[i]} 1)`);
|
|
1561
1731
|
return conditions.length === 0 ? "false" : `(and ${conditions.join(" ")})`;
|
|
1562
1732
|
}
|
|
1733
|
+
// JoinedOrDeadLettered (NU-040 AC4): a quiescent state that still holds a
|
|
1734
|
+
// `pending` token is a stranded correlation group. Carries NO sink clause — a
|
|
1735
|
+
// declared sink must not excuse a stranded group.
|
|
1563
1736
|
case "joined-or-dead-lettered": {
|
|
1564
|
-
const deadlock = encodeDeadlock(flatNet, mVars, sinkPlaces, envInject);
|
|
1565
1737
|
const pid = flatNet.placeIndex.get(property.pending.name);
|
|
1566
|
-
|
|
1738
|
+
if (pid == null) return "false";
|
|
1739
|
+
const conditions = encodeQuiescent(flatNet, mVars, envInject);
|
|
1740
|
+
if (conditions == null) return "false";
|
|
1741
|
+
conditions.push(`(>= ${mVars[pid]} 1)`);
|
|
1742
|
+
return joinConditions(conditions);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
function strandedConditions(excuses, counts) {
|
|
1747
|
+
const stranded = [];
|
|
1748
|
+
for (let pid = 0; pid < excuses.length; pid++) {
|
|
1749
|
+
const markers = excuses[pid];
|
|
1750
|
+
if (markers == null) continue;
|
|
1751
|
+
if (markers.length === 0) {
|
|
1752
|
+
stranded.push(`(>= ${counts[pid]} 1)`);
|
|
1753
|
+
} else {
|
|
1754
|
+
stranded.push(`(and (>= ${counts[pid]} 1) ${markers.map((k) => `(= ${counts[k]} 0)`).join(" ")})`);
|
|
1567
1755
|
}
|
|
1568
1756
|
}
|
|
1757
|
+
return stranded;
|
|
1569
1758
|
}
|
|
1570
|
-
function
|
|
1759
|
+
function joinConditions(conditions) {
|
|
1760
|
+
return conditions.length === 0 ? "true" : `(and ${conditions.join("\n ")})`;
|
|
1761
|
+
}
|
|
1762
|
+
function encodeQuiescent(flatNet, mVars, envInject) {
|
|
1571
1763
|
const envBound = /* @__PURE__ */ new Map();
|
|
1572
1764
|
for (const inj of envInject) envBound.set(inj.pid, inj.bound);
|
|
1573
1765
|
const disabledConditions = [];
|
|
@@ -1597,13 +1789,13 @@ function encodeDeadlock(flatNet, mVars, sinkPlaces, envInject) {
|
|
|
1597
1789
|
disabledConditions.push("true");
|
|
1598
1790
|
continue;
|
|
1599
1791
|
}
|
|
1600
|
-
if (disableReasons.length === 0) return
|
|
1792
|
+
if (disableReasons.length === 0) return null;
|
|
1601
1793
|
disabledConditions.push(`(or ${disableReasons.join(" ")})`);
|
|
1602
1794
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
return
|
|
1795
|
+
return disabledConditions;
|
|
1796
|
+
}
|
|
1797
|
+
function quiescenceUnreachable(flatNet, envInject) {
|
|
1798
|
+
return encodeQuiescent(flatNet, vars(flatNet.places.length, ""), envInject) === null;
|
|
1607
1799
|
}
|
|
1608
1800
|
function injectionMap(flatNet) {
|
|
1609
1801
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -1613,7 +1805,7 @@ function injectionMap(flatNet) {
|
|
|
1613
1805
|
|
|
1614
1806
|
// src/verification/z3/certificate-checker.ts
|
|
1615
1807
|
var VC_LABELS = ["initiation (VC1)", "consecution (VC2)", "safety (VC3)"];
|
|
1616
|
-
async function checkCertificate(certificate, flatNet, initialMarking, property, invariants, sinkPlaces, solver, timeoutMs) {
|
|
1808
|
+
async function checkCertificate(certificate, flatNet, initialMarking, property, invariants, sinkPlaces, solver, timeoutMs, conditionalSinks = [], stateEquation = false) {
|
|
1617
1809
|
if (certificate == null) {
|
|
1618
1810
|
return {
|
|
1619
1811
|
type: "unavailable",
|
|
@@ -1626,11 +1818,12 @@ async function checkCertificate(certificate, flatNet, initialMarking, property,
|
|
|
1626
1818
|
if (!certificate.includes("(define-fun Reachable ") && !certificate.includes("(define-fun |Reachable| ")) {
|
|
1627
1819
|
return { type: "unavailable", reason: "certificate does not define Reachable", invariant: certificate };
|
|
1628
1820
|
}
|
|
1629
|
-
const vcs = buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants);
|
|
1821
|
+
const vcs = buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants, conditionalSinks, stateEquation);
|
|
1630
1822
|
let results;
|
|
1631
1823
|
try {
|
|
1632
1824
|
results = await runVcScript(script(vcs), timeoutMs, solver);
|
|
1633
1825
|
} catch (e) {
|
|
1826
|
+
rethrowIfProgrammingError(e);
|
|
1634
1827
|
return { type: "unavailable", reason: String(e?.message ?? e), invariant: certificate };
|
|
1635
1828
|
}
|
|
1636
1829
|
for (let i = 0; i < results.length; i++) {
|
|
@@ -1641,8 +1834,8 @@ async function checkCertificate(certificate, flatNet, initialMarking, property,
|
|
|
1641
1834
|
}
|
|
1642
1835
|
return { type: "passed", invariant: certificate };
|
|
1643
1836
|
}
|
|
1644
|
-
function vcScript(certificate, flatNet, initialMarking, property, sinkPlaces, invariants) {
|
|
1645
|
-
return script(buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants));
|
|
1837
|
+
function vcScript(certificate, flatNet, initialMarking, property, sinkPlaces, invariants, conditionalSinks = [], stateEquation = false) {
|
|
1838
|
+
return script(buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants, conditionalSinks, stateEquation));
|
|
1646
1839
|
}
|
|
1647
1840
|
function shapeFailure(flatNet, invariants) {
|
|
1648
1841
|
const P = flatNet.places.length;
|
|
@@ -1686,14 +1879,29 @@ function parseVcResults(stdout) {
|
|
|
1686
1879
|
}
|
|
1687
1880
|
return results;
|
|
1688
1881
|
}
|
|
1689
|
-
function buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants) {
|
|
1882
|
+
function buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants, conditionalSinks, stateEquation) {
|
|
1690
1883
|
const P = flatNet.places.length;
|
|
1884
|
+
const T = stateEquation ? flatNet.transitions.length : 0;
|
|
1691
1885
|
const mVars = [];
|
|
1692
1886
|
const mpVars = [];
|
|
1693
1887
|
for (let i = 0; i < P; i++) {
|
|
1694
1888
|
mVars.push(`m${i}`);
|
|
1695
1889
|
mpVars.push(`m${i}p`);
|
|
1696
1890
|
}
|
|
1891
|
+
const nVars = [];
|
|
1892
|
+
const npVars = [];
|
|
1893
|
+
for (let k = 0; k < T; k++) {
|
|
1894
|
+
nVars.push(`n${k}`);
|
|
1895
|
+
npVars.push(`n${k}p`);
|
|
1896
|
+
}
|
|
1897
|
+
const candidateOf = (m, n) => {
|
|
1898
|
+
const parts = [`(Reachable ${[...m, ...n].join(" ")})`, ...invariantConditions(invariants, m)];
|
|
1899
|
+
if (T > 0) {
|
|
1900
|
+
for (const v of n) parts.push(`(>= ${v} 0)`);
|
|
1901
|
+
parts.push(...stateEquationConditions(flatNet, initialMarking, n, m));
|
|
1902
|
+
}
|
|
1903
|
+
return conjoin(parts);
|
|
1904
|
+
};
|
|
1697
1905
|
const prelude = [
|
|
1698
1906
|
"; IC3/PDR certificate check (plain SMT-LIB2, not HORN):",
|
|
1699
1907
|
"; each VC below must be unsat for the certificate to stand.",
|
|
@@ -1702,19 +1910,22 @@ function buildVerificationConditions(certificate, flatNet, initialMarking, prope
|
|
|
1702
1910
|
];
|
|
1703
1911
|
for (const v of mVars) prelude.push(`(declare-const ${v} Int)`);
|
|
1704
1912
|
for (const v of mpVars) prelude.push(`(declare-const ${v} Int)`);
|
|
1913
|
+
for (const v of nVars) prelude.push(`(declare-const ${v} Int)`);
|
|
1914
|
+
for (const v of npVars) prelude.push(`(declare-const ${v} Int)`);
|
|
1705
1915
|
const m0 = [];
|
|
1706
1916
|
for (let i = 0; i < P; i++) m0.push(String(initialMarking.tokens(flatNet.places[i])));
|
|
1707
|
-
const
|
|
1708
|
-
const
|
|
1709
|
-
const
|
|
1917
|
+
const n0 = new Array(T).fill("0");
|
|
1918
|
+
const vc1 = [`(assert (not ${candidateOf(m0, n0)}))`];
|
|
1919
|
+
const nonNegative = [...mVars, ...nVars].map((v) => `(assert (>= ${v} 0))`);
|
|
1920
|
+
const step = encodeStepRelationSmt2(flatNet, stateEquation);
|
|
1710
1921
|
const vc2 = [
|
|
1711
1922
|
...nonNegative,
|
|
1712
|
-
`(assert ${
|
|
1923
|
+
`(assert ${candidateOf(mVars, nVars)})`,
|
|
1713
1924
|
`(assert ${step})`,
|
|
1714
|
-
`(assert (not ${
|
|
1925
|
+
`(assert (not ${candidateOf(mpVars, npVars)}))`
|
|
1715
1926
|
];
|
|
1716
|
-
const bad = encodePropertyViolation(flatNet, property, mVars, sinkPlaces, resolveEnvInjection(flatNet));
|
|
1717
|
-
const vc3 = [...nonNegative, `(assert ${
|
|
1927
|
+
const bad = encodePropertyViolation(flatNet, property, mVars, sinkPlaces, resolveEnvInjection(flatNet), conditionalSinks);
|
|
1928
|
+
const vc3 = [...nonNegative, `(assert ${candidateOf(mVars, nVars)})`, `(assert ${bad})`];
|
|
1718
1929
|
return { prelude, asserts: [vc1, vc2, vc3] };
|
|
1719
1930
|
}
|
|
1720
1931
|
function script(vcs) {
|
|
@@ -1778,8 +1989,190 @@ function reasonUnknown(reply) {
|
|
|
1778
1989
|
reason = reason.trim();
|
|
1779
1990
|
return reason === "" ? null : reason;
|
|
1780
1991
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1992
|
+
|
|
1993
|
+
// src/verification/z3/linear-bound.ts
|
|
1994
|
+
function violationDemand(flatNet, property) {
|
|
1995
|
+
const demand = /* @__PURE__ */ new Map();
|
|
1996
|
+
switch (property.type) {
|
|
1997
|
+
case "unreachable":
|
|
1998
|
+
for (const p of property.places) {
|
|
1999
|
+
const pid = flatNet.placeIndex.get(p.name);
|
|
2000
|
+
if (pid != null) demand.set(pid, 1);
|
|
2001
|
+
}
|
|
2002
|
+
break;
|
|
2003
|
+
case "mutual-exclusion": {
|
|
2004
|
+
for (const p of [property.p1, property.p2]) {
|
|
2005
|
+
const pid = flatNet.placeIndex.get(p.name);
|
|
2006
|
+
if (pid != null) demand.set(pid, 1);
|
|
2007
|
+
}
|
|
2008
|
+
break;
|
|
2009
|
+
}
|
|
2010
|
+
case "place-bound":
|
|
2011
|
+
case "branch-place-bound": {
|
|
2012
|
+
const pid = flatNet.placeIndex.get(property.place.name);
|
|
2013
|
+
if (pid != null) demand.set(pid, property.bound + 1);
|
|
2014
|
+
break;
|
|
2015
|
+
}
|
|
2016
|
+
case "deadlock-free":
|
|
2017
|
+
case "terminates-at-sink":
|
|
2018
|
+
case "joined-or-dead-lettered":
|
|
2019
|
+
return null;
|
|
2020
|
+
}
|
|
2021
|
+
return demand.size === 0 ? null : demand;
|
|
2022
|
+
}
|
|
2023
|
+
function zeroWeightPlaces(flatNet) {
|
|
2024
|
+
const zero = new Set(nonlinearPlaces(flatNet));
|
|
2025
|
+
for (const inj of resolveEnvInjection(flatNet)) zero.add(inj.pid);
|
|
2026
|
+
return zero;
|
|
2027
|
+
}
|
|
2028
|
+
function encodeLinearBound(flatNet, initialMarking, property) {
|
|
2029
|
+
const demand = violationDemand(flatNet, property);
|
|
2030
|
+
if (demand == null) return null;
|
|
2031
|
+
const P = flatNet.places.length;
|
|
2032
|
+
const zero = zeroWeightPlaces(flatNet);
|
|
2033
|
+
const lines = [];
|
|
2034
|
+
lines.push("; Linear state-equation bound (VER-015): y >= 0 with y.C <= 0 on every");
|
|
2035
|
+
lines.push("; transition gives y.M <= y.M0 for every reachable M; sat = the violating");
|
|
2036
|
+
lines.push("; markings' demand exceeds that bound, so none is reachable.");
|
|
2037
|
+
lines.push("(set-logic QF_LIA)");
|
|
2038
|
+
for (let p = 0; p < P; p++) lines.push(`(declare-const y${p} Int)`);
|
|
2039
|
+
for (let p = 0; p < P; p++) lines.push(`(assert (>= y${p} 0))`);
|
|
2040
|
+
for (const p of [...zero].sort((a, b) => a - b)) lines.push(`(assert (= y${p} 0))`);
|
|
2041
|
+
for (const ft of flatNet.transitions) {
|
|
2042
|
+
const terms = [];
|
|
2043
|
+
for (let p = 0; p < P; p++) {
|
|
2044
|
+
const c = ft.postVector[p] - ft.preVector[p];
|
|
2045
|
+
if (c !== 0) terms.push(term(c, `y${p}`));
|
|
2046
|
+
}
|
|
2047
|
+
if (terms.length > 0) lines.push(`(assert (<= ${sum(terms)} 0))`);
|
|
2048
|
+
}
|
|
2049
|
+
const demandTerms = [];
|
|
2050
|
+
for (const p of [...demand.keys()].sort((a, b) => a - b)) demandTerms.push(term(demand.get(p), `y${p}`));
|
|
2051
|
+
const initTerms = ["1"];
|
|
2052
|
+
for (let p = 0; p < P; p++) {
|
|
2053
|
+
const m0 = initialMarking.tokens(flatNet.places[p]);
|
|
2054
|
+
if (m0 > 0) initTerms.push(term(m0, `y${p}`));
|
|
2055
|
+
}
|
|
2056
|
+
lines.push(`(assert (>= ${sum(demandTerms)} ${sum(initTerms)}))`);
|
|
2057
|
+
lines.push("(check-sat)");
|
|
2058
|
+
lines.push("(get-model)");
|
|
2059
|
+
return lines.join("\n");
|
|
2060
|
+
}
|
|
2061
|
+
function term(c, v) {
|
|
2062
|
+
if (c === 1) return v;
|
|
2063
|
+
if (c === -1) return `(- ${v})`;
|
|
2064
|
+
return c > 0 ? `(* ${c} ${v})` : `(* (- ${-c}) ${v})`;
|
|
2065
|
+
}
|
|
2066
|
+
function sum(terms) {
|
|
2067
|
+
return terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
|
|
2068
|
+
}
|
|
2069
|
+
function decodeLinearBound(stdout, placeCount) {
|
|
2070
|
+
const y = new Array(placeCount).fill(0n);
|
|
2071
|
+
let seen = false;
|
|
2072
|
+
for (const def of extractDefineFuns(stdout)) {
|
|
2073
|
+
const m = /^\(define-fun\s+y(\d+)\s+\(\)\s+Int\s+(\(-\s*(\d+)\s*\)|(\d+))\s*\)$/s.exec(def.trim());
|
|
2074
|
+
if (m == null) continue;
|
|
2075
|
+
const pid = Number(m[1]);
|
|
2076
|
+
if (pid >= placeCount) continue;
|
|
2077
|
+
y[pid] = m[3] != null ? -BigInt(m[3]) : BigInt(m[4]);
|
|
2078
|
+
seen = true;
|
|
2079
|
+
}
|
|
2080
|
+
return seen ? y : null;
|
|
2081
|
+
}
|
|
2082
|
+
function checkLinearBoundExact(flatNet, initialMarking, property, y) {
|
|
2083
|
+
const demand = violationDemand(flatNet, property);
|
|
2084
|
+
if (demand == null) return null;
|
|
2085
|
+
const P = flatNet.places.length;
|
|
2086
|
+
if (y.length !== P) return null;
|
|
2087
|
+
const zero = zeroWeightPlaces(flatNet);
|
|
2088
|
+
for (let p = 0; p < P; p++) {
|
|
2089
|
+
if (y[p] < 0n) return null;
|
|
2090
|
+
if (zero.has(p) && y[p] !== 0n) return null;
|
|
2091
|
+
}
|
|
2092
|
+
for (const ft of flatNet.transitions) {
|
|
2093
|
+
let delta = 0n;
|
|
2094
|
+
for (let p = 0; p < P; p++) {
|
|
2095
|
+
if (y[p] === 0n) continue;
|
|
2096
|
+
delta += y[p] * BigInt(ft.postVector[p] - ft.preVector[p]);
|
|
2097
|
+
}
|
|
2098
|
+
if (delta > 0n) return null;
|
|
2099
|
+
}
|
|
2100
|
+
let constant = 0n;
|
|
2101
|
+
for (let p = 0; p < P; p++) {
|
|
2102
|
+
if (y[p] !== 0n) constant += y[p] * BigInt(initialMarking.tokens(flatNet.places[p]));
|
|
2103
|
+
}
|
|
2104
|
+
let demandValue = 0n;
|
|
2105
|
+
for (const [p, d] of demand) demandValue += y[p] * BigInt(d);
|
|
2106
|
+
if (demandValue < constant + 1n) return null;
|
|
2107
|
+
return { weights: y, constant, demandValue };
|
|
2108
|
+
}
|
|
2109
|
+
function formatLinearBound(flatNet, bound) {
|
|
2110
|
+
const parts = [];
|
|
2111
|
+
for (let p = 0; p < bound.weights.length; p++) {
|
|
2112
|
+
const w = bound.weights[p];
|
|
2113
|
+
if (w === 0n) continue;
|
|
2114
|
+
parts.push(w === 1n ? flatNet.places[p].name : `${w}*${flatNet.places[p].name}`);
|
|
2115
|
+
}
|
|
2116
|
+
return `${parts.length === 0 ? "0" : parts.join(" + ")} <= ${bound.constant}`;
|
|
2117
|
+
}
|
|
2118
|
+
function formatLinearDemand(flatNet, property, bound) {
|
|
2119
|
+
const demand = violationDemand(flatNet, property) ?? /* @__PURE__ */ new Map();
|
|
2120
|
+
const parts = [];
|
|
2121
|
+
for (const p of [...demand.keys()].sort((a, b) => a - b)) {
|
|
2122
|
+
const w = bound.weights[p] * BigInt(demand.get(p));
|
|
2123
|
+
if (w === 0n) continue;
|
|
2124
|
+
parts.push(w === 1n ? flatNet.places[p].name : `${w}*${flatNet.places[p].name}`);
|
|
2125
|
+
}
|
|
2126
|
+
return `${parts.length === 0 ? "0" : parts.join(" + ")} >= ${bound.demandValue}`;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// src/verification/graph-decision.ts
|
|
2130
|
+
function decideOverClasses(view, property, sinkPlaces, conditionalSinks = []) {
|
|
2131
|
+
const firstWhere = (pred) => {
|
|
2132
|
+
for (let i = 0; i < view.count; i++) {
|
|
2133
|
+
if (pred(i)) return i;
|
|
2134
|
+
}
|
|
2135
|
+
return -1;
|
|
2136
|
+
};
|
|
2137
|
+
switch (property.type) {
|
|
2138
|
+
case "place-bound":
|
|
2139
|
+
case "branch-place-bound":
|
|
2140
|
+
return firstWhere((i) => view.markingOf(i).tokens(property.place) > property.bound);
|
|
2141
|
+
case "unreachable":
|
|
2142
|
+
return firstWhere((i) => {
|
|
2143
|
+
const m = view.markingOf(i);
|
|
2144
|
+
for (const p of property.places) {
|
|
2145
|
+
if (!m.hasTokens(p)) return false;
|
|
2146
|
+
}
|
|
2147
|
+
return true;
|
|
2148
|
+
});
|
|
2149
|
+
case "mutual-exclusion":
|
|
2150
|
+
return firstWhere((i) => {
|
|
2151
|
+
const m = view.markingOf(i);
|
|
2152
|
+
return m.hasTokens(property.p1) && m.hasTokens(property.p2);
|
|
2153
|
+
});
|
|
2154
|
+
// DeadlockFree (VER-002): a quiescent class that strands a token — some marked
|
|
2155
|
+
// place is not where resting is permitted, the conditional sinks of VER-014
|
|
2156
|
+
// included. The empty marking strands nothing (AC4).
|
|
2157
|
+
case "deadlock-free":
|
|
2158
|
+
return firstWhere((i) => view.isQuiescent(i) && strandsToken(view.markingOf(i), sinkPlaces, conditionalSinks));
|
|
2159
|
+
// TerminatesAtSink (VER-002): a quiescent class that marks NO declared sink.
|
|
2160
|
+
// Inverts with DeadlockFree on the empty marking, by design.
|
|
2161
|
+
case "terminates-at-sink":
|
|
2162
|
+
return firstWhere((i) => view.isQuiescent(i) && !anySinkMarked(view.markingOf(i), sinkPlaces));
|
|
2163
|
+
// JoinedOrDeadLettered (NU-040 AC4): a quiescent class still holding a pending
|
|
2164
|
+
// token. No sink clause.
|
|
2165
|
+
case "joined-or-dead-lettered":
|
|
2166
|
+
return firstWhere((i) => view.isQuiescent(i) && view.markingOf(i).hasTokens(property.pending));
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
function anySinkMarked(m, sinks) {
|
|
2170
|
+
const sinkNames = /* @__PURE__ */ new Set();
|
|
2171
|
+
for (const s of sinks) sinkNames.add(s.name);
|
|
2172
|
+
for (const p of m.placesWithTokens()) {
|
|
2173
|
+
if (sinkNames.has(p.name)) return true;
|
|
2174
|
+
}
|
|
2175
|
+
return false;
|
|
1783
2176
|
}
|
|
1784
2177
|
|
|
1785
2178
|
// src/verification/analysis/dbm.ts
|
|
@@ -1888,6 +2281,35 @@ var DBM = class _DBM {
|
|
|
1888
2281
|
allNames.push(...newClockNames);
|
|
1889
2282
|
return new _DBM(newBounds, newDim, allNames, false).canonicalize();
|
|
1890
2283
|
}
|
|
2284
|
+
/**
|
|
2285
|
+
* The same zone with its clocks reordered: clock `k` of the result is clock
|
|
2286
|
+
* `order[k]` of this DBM. `order` must be a permutation of `0..clockCount()-1`.
|
|
2287
|
+
*
|
|
2288
|
+
* The state-class graph applies this to put every class's clocks in the one
|
|
2289
|
+
* canonical order (VER-010), so two arrivals at the same marking and zone whose
|
|
2290
|
+
* transitions became enabled in a different sequence share a key instead of
|
|
2291
|
+
* being counted as two classes. The reference row and column stay put; the
|
|
2292
|
+
* matrix is copied once, O(dim²) against the O(dim³) canonicalisation every
|
|
2293
|
+
* successor already pays.
|
|
2294
|
+
*/
|
|
2295
|
+
permuted(order) {
|
|
2296
|
+
if (this._empty) return this;
|
|
2297
|
+
const n = this.clockNames.length;
|
|
2298
|
+
const dim = this.dim;
|
|
2299
|
+
const out = new Float64Array(dim * dim);
|
|
2300
|
+
out[0] = 0;
|
|
2301
|
+
const names = new Array(n);
|
|
2302
|
+
for (let i = 0; i < n; i++) {
|
|
2303
|
+
const oi = order[i] + 1;
|
|
2304
|
+
names[i] = this.clockNames[order[i]];
|
|
2305
|
+
out[(i + 1) * dim] = this.bounds[oi * dim];
|
|
2306
|
+
out[i + 1] = this.bounds[oi];
|
|
2307
|
+
for (let j = 0; j < n; j++) {
|
|
2308
|
+
out[(i + 1) * dim + (j + 1)] = this.bounds[oi * dim + (order[j] + 1)];
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
return new _DBM(out, dim, names, false);
|
|
2312
|
+
}
|
|
1891
2313
|
/** Lets time pass: set all lower bounds to 0. */
|
|
1892
2314
|
letTimePass() {
|
|
1893
2315
|
if (this._empty) return this;
|
|
@@ -1919,6 +2341,24 @@ var DBM = class _DBM {
|
|
|
1919
2341
|
}
|
|
1920
2342
|
return true;
|
|
1921
2343
|
}
|
|
2344
|
+
/**
|
|
2345
|
+
* The zone's identity for state-class dedup: the clock names and the FULL
|
|
2346
|
+
* canonical matrix, every difference bound included.
|
|
2347
|
+
*
|
|
2348
|
+
* {@link toString} prints only the per-clock projections `[lo, hi]`, and two
|
|
2349
|
+
* zones can agree on every projection while disagreeing on a difference
|
|
2350
|
+
* constraint `θi - θj <= c` — the class where one transition must fire no later
|
|
2351
|
+
* than another versus the class where either may go first. Keying on the
|
|
2352
|
+
* projections merges those, and since the graph explores only the first
|
|
2353
|
+
* arrival's successors, a marking reachable only from the second is lost: a
|
|
2354
|
+
* false `proven`. This key is what {@link equals} compares, rendered.
|
|
2355
|
+
*/
|
|
2356
|
+
zoneKey() {
|
|
2357
|
+
if (this._empty) return "DBM[empty]";
|
|
2358
|
+
const parts = [this.clockNames.join(",")];
|
|
2359
|
+
for (let i = 0; i < this.bounds.length; i++) parts.push(formatBound(this.bounds[i]));
|
|
2360
|
+
return parts.join("|");
|
|
2361
|
+
}
|
|
1922
2362
|
toString() {
|
|
1923
2363
|
if (this._empty) return "DBM[empty]";
|
|
1924
2364
|
const parts = [];
|
|
@@ -2167,10 +2607,35 @@ var StateClassGraph = class _StateClassGraph {
|
|
|
2167
2607
|
}
|
|
2168
2608
|
};
|
|
2169
2609
|
function classKey(sc) {
|
|
2170
|
-
return `${sc.marking.toString()}|${sc.firingDomain.
|
|
2610
|
+
return `${sc.marking.toString()}|${sc.firingDomain.zoneKey()}`;
|
|
2611
|
+
}
|
|
2612
|
+
function canonicalOrder(transitions) {
|
|
2613
|
+
let sorted = true;
|
|
2614
|
+
for (let i = 1; i < transitions.length; i++) {
|
|
2615
|
+
if (transitions[i].name < transitions[i - 1].name) {
|
|
2616
|
+
sorted = false;
|
|
2617
|
+
break;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
if (sorted) return null;
|
|
2621
|
+
const order = new Array(transitions.length);
|
|
2622
|
+
for (let i = 0; i < order.length; i++) order[i] = i;
|
|
2623
|
+
order.sort((a, b) => {
|
|
2624
|
+
const na = transitions[a].name;
|
|
2625
|
+
const nb = transitions[b].name;
|
|
2626
|
+
return na < nb ? -1 : na > nb ? 1 : a - b;
|
|
2627
|
+
});
|
|
2628
|
+
return order;
|
|
2629
|
+
}
|
|
2630
|
+
function permute(items, order) {
|
|
2631
|
+
const out = new Array(items.length);
|
|
2632
|
+
for (let i = 0; i < order.length; i++) out[i] = items[order[i]];
|
|
2633
|
+
return out;
|
|
2171
2634
|
}
|
|
2172
2635
|
function initialStateClass(net, initialMarking, envPlaces, envMode) {
|
|
2173
|
-
const
|
|
2636
|
+
const found = findEnabledTransitions(net, initialMarking, envPlaces, envMode);
|
|
2637
|
+
const order = canonicalOrder(found);
|
|
2638
|
+
const enabledTransitions = order === null ? found : permute(found, order);
|
|
2174
2639
|
const clockNames = enabledTransitions.map((t) => t.name);
|
|
2175
2640
|
const lowerBounds = enabledTransitions.map((t) => earliest(t.timing) / 1e3);
|
|
2176
2641
|
const upperBounds = enabledTransitions.map((t) => latest(t.timing) / 1e3);
|
|
@@ -2215,14 +2680,19 @@ function computeSuccessor(net, current, fired, environmentPlaces, environmentMod
|
|
|
2215
2680
|
const newClockNames = newlyEnabled.map((t) => t.name);
|
|
2216
2681
|
const newLowerBounds = newlyEnabled.map((t) => earliest(t.timing) / 1e3);
|
|
2217
2682
|
const newUpperBounds = newlyEnabled.map((t) => latest(t.timing) / 1e3);
|
|
2218
|
-
|
|
2683
|
+
let firedDBM = current.firingDomain.fireTransition(
|
|
2219
2684
|
firedIdx,
|
|
2220
2685
|
newClockNames,
|
|
2221
2686
|
newLowerBounds,
|
|
2222
2687
|
newUpperBounds,
|
|
2223
2688
|
persistentIndices
|
|
2224
2689
|
);
|
|
2225
|
-
|
|
2690
|
+
let allEnabled = [...persistent, ...newlyEnabled];
|
|
2691
|
+
const order = canonicalOrder(allEnabled);
|
|
2692
|
+
if (order !== null) {
|
|
2693
|
+
allEnabled = permute(allEnabled, order);
|
|
2694
|
+
firedDBM = firedDBM.permuted(order);
|
|
2695
|
+
}
|
|
2226
2696
|
const readyEarliest = allEnabled.map((_, k) => firedDBM.getLowerBound(k));
|
|
2227
2697
|
const newDBM = firedDBM.letTimePass();
|
|
2228
2698
|
return new StateClass(newMarking, newDBM, allEnabled, readyEarliest);
|
|
@@ -2294,10 +2764,7 @@ function fireTransition(marking, transition, outputPlaces, environmentPlaces, en
|
|
|
2294
2764
|
consumeFromPlace(builder, spec.place, toConsume, environmentPlaces, environmentMode);
|
|
2295
2765
|
}
|
|
2296
2766
|
for (const arc of transition.resets) {
|
|
2297
|
-
|
|
2298
|
-
if (current > 0) {
|
|
2299
|
-
builder.removeTokens(arc.place, current);
|
|
2300
|
-
}
|
|
2767
|
+
builder.tokens(arc.place, 0);
|
|
2301
2768
|
}
|
|
2302
2769
|
for (const place of outputPlaces) {
|
|
2303
2770
|
builder.addTokens(place, 1);
|
|
@@ -2314,12 +2781,72 @@ function consumeFromPlace(builder, place, count, environmentPlaces, environmentM
|
|
|
2314
2781
|
}
|
|
2315
2782
|
}
|
|
2316
2783
|
|
|
2784
|
+
// src/verification/scg-verifier.ts
|
|
2785
|
+
function isUntimed(net) {
|
|
2786
|
+
for (const t of net.transitions) {
|
|
2787
|
+
if (t.timing.type !== "immediate") return false;
|
|
2788
|
+
}
|
|
2789
|
+
return true;
|
|
2790
|
+
}
|
|
2791
|
+
var NOTE_ENUMERATED = "\nNote: decided by bounded state-space enumeration \u2014 the state-class graph closed, so the verdict is sound AND complete: a `violated` is a real firing sequence, not a possibly-spurious over-approximation. The net is untimed, so this is the same claim the encoders make (VER-017).\n";
|
|
2792
|
+
function verifyViaStateClassGraph(net, initial, property, sinkPlaces, maxClasses, conditionalSinks = []) {
|
|
2793
|
+
const graph = StateClassGraph.build(net, initial, maxClasses);
|
|
2794
|
+
const classes = graph.stateClasses();
|
|
2795
|
+
if (!graph.isComplete()) return { kind: "truncated", classCount: classes.length };
|
|
2796
|
+
const violating = decideOverClasses(
|
|
2797
|
+
{
|
|
2798
|
+
count: classes.length,
|
|
2799
|
+
markingOf: (i) => classes[i].marking,
|
|
2800
|
+
isQuiescent: (i) => graph.successors(classes[i]).size === 0
|
|
2801
|
+
},
|
|
2802
|
+
property,
|
|
2803
|
+
sinkPlaces,
|
|
2804
|
+
conditionalSinks
|
|
2805
|
+
);
|
|
2806
|
+
if (violating >= 0) {
|
|
2807
|
+
const [trace, transitions] = counterexamplePath(graph, classes[violating]);
|
|
2808
|
+
return { kind: "decided", verdict: { type: "violated" }, trace, transitions, classCount: classes.length };
|
|
2809
|
+
}
|
|
2810
|
+
return {
|
|
2811
|
+
kind: "decided",
|
|
2812
|
+
verdict: { type: "proven", method: "state-space enumeration (VER-017)", inductiveInvariant: null },
|
|
2813
|
+
trace: [],
|
|
2814
|
+
transitions: [],
|
|
2815
|
+
classCount: classes.length
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
function counterexamplePath(graph, target) {
|
|
2819
|
+
const parent = /* @__PURE__ */ new Map();
|
|
2820
|
+
const via = /* @__PURE__ */ new Map();
|
|
2821
|
+
const seen = /* @__PURE__ */ new Set([graph.initialClass]);
|
|
2822
|
+
const queue = [graph.initialClass];
|
|
2823
|
+
while (queue.length > 0) {
|
|
2824
|
+
const current = queue.shift();
|
|
2825
|
+
if (current === target) break;
|
|
2826
|
+
for (const [transition, edges] of graph.outgoingBranchEdges(current)) {
|
|
2827
|
+
for (const edge of edges) {
|
|
2828
|
+
if (seen.has(edge.target)) continue;
|
|
2829
|
+
seen.add(edge.target);
|
|
2830
|
+
parent.set(edge.target, current);
|
|
2831
|
+
via.set(edge.target, transition.name);
|
|
2832
|
+
queue.push(edge.target);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
const chain = [];
|
|
2837
|
+
for (let cur = target; cur != null; cur = parent.get(cur)) {
|
|
2838
|
+
chain.push(cur);
|
|
2839
|
+
}
|
|
2840
|
+
chain.reverse();
|
|
2841
|
+
return [chain.map((sc) => sc.marking), chain.slice(1).map((sc) => via.get(sc))];
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2317
2844
|
// src/verification/z3/counterexample-decoder.ts
|
|
2318
|
-
function decode(answer, flatNet) {
|
|
2319
|
-
const states = decodeStateSet(answer, flatNet);
|
|
2845
|
+
function decode(answer, flatNet, counterCount = 0) {
|
|
2846
|
+
const states = decodeStateSet(answer, flatNet, counterCount);
|
|
2320
2847
|
return { states, note: states.size === 0 ? "no ground Reachable states in the z3 proof" : null };
|
|
2321
2848
|
}
|
|
2322
|
-
function decodeStateSet(answer, flatNet) {
|
|
2849
|
+
function decodeStateSet(answer, flatNet, counterCount = 0) {
|
|
2323
2850
|
const byKey = /* @__PURE__ */ new Map();
|
|
2324
2851
|
const P = flatNet.places.length;
|
|
2325
2852
|
for (const head of ["(Reachable", "(|Reachable|"]) {
|
|
@@ -2336,8 +2863,8 @@ function decodeStateSet(answer, flatNet) {
|
|
|
2336
2863
|
if (end < 0) break;
|
|
2337
2864
|
const inner = answer.slice(start + head.length, end - 1);
|
|
2338
2865
|
const args = parseGroundIntArgs(inner);
|
|
2339
|
-
if (args != null && args.length === P) {
|
|
2340
|
-
const marking = toMarking(args, flatNet);
|
|
2866
|
+
if (args != null && args.length === P + counterCount) {
|
|
2867
|
+
const marking = toMarking(counterCount === 0 ? args : args.slice(0, P), flatNet);
|
|
2341
2868
|
const key = marking.toString();
|
|
2342
2869
|
if (!byKey.has(key)) byKey.set(key, marking);
|
|
2343
2870
|
}
|
|
@@ -2511,20 +3038,41 @@ function enabledRelaxEnv(state, ft, envInj) {
|
|
|
2511
3038
|
}
|
|
2512
3039
|
return true;
|
|
2513
3040
|
}
|
|
2514
|
-
function
|
|
3041
|
+
function isQuiescent(index, state) {
|
|
2515
3042
|
for (const ft of index.flatNet.transitions) {
|
|
2516
3043
|
if (enabledRelaxEnv(state, ft, index.envInj)) return false;
|
|
2517
3044
|
}
|
|
2518
3045
|
return true;
|
|
2519
3046
|
}
|
|
2520
|
-
function
|
|
3047
|
+
function sinkIndices(flatNet, sinkPlaces) {
|
|
3048
|
+
const idx = /* @__PURE__ */ new Set();
|
|
3049
|
+
for (const sink of sinkPlaces) {
|
|
3050
|
+
const i = flatNetIndexOf(flatNet, sink);
|
|
3051
|
+
if (i >= 0) idx.add(i);
|
|
3052
|
+
}
|
|
3053
|
+
return idx;
|
|
3054
|
+
}
|
|
3055
|
+
function satisfiesBadIndexed(index, state, property, sinkPlaces, conditionalSinks) {
|
|
2521
3056
|
const flatNet = index.flatNet;
|
|
2522
3057
|
switch (property.type) {
|
|
3058
|
+
// DeadlockFree (VER-002): quiescent AND some marked place is not where resting
|
|
3059
|
+
// is permitted — a conditional sink (VER-014) counts only while every marker
|
|
3060
|
+
// that would excuse it is unmarked. Mirrors the encoder's `stranded` disjunction.
|
|
2523
3061
|
case "deadlock-free": {
|
|
2524
|
-
if (!
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
3062
|
+
if (!isQuiescent(index, state)) return false;
|
|
3063
|
+
const excuses = strandingExcuses(flatNet, sinkPlaces, conditionalSinks);
|
|
3064
|
+
for (let pid = 0; pid < flatNet.places.length; pid++) {
|
|
3065
|
+
const markers = excuses[pid];
|
|
3066
|
+
if (markers == null || state[pid] < 1) continue;
|
|
3067
|
+
if (markers.every((k) => state[k] === 0)) return true;
|
|
3068
|
+
}
|
|
3069
|
+
return false;
|
|
3070
|
+
}
|
|
3071
|
+
// TerminatesAtSink (VER-002): quiescent AND no declared sink marked.
|
|
3072
|
+
case "terminates-at-sink": {
|
|
3073
|
+
if (!isQuiescent(index, state)) return false;
|
|
3074
|
+
for (const pid of sinkIndices(flatNet, sinkPlaces)) {
|
|
3075
|
+
if (state[pid] !== 0) return false;
|
|
2528
3076
|
}
|
|
2529
3077
|
return true;
|
|
2530
3078
|
}
|
|
@@ -2540,10 +3088,12 @@ function satisfiesBadIndexed(index, state, property, sinkPlaces) {
|
|
|
2540
3088
|
if (idx < 0) return false;
|
|
2541
3089
|
return state[idx] > property.bound;
|
|
2542
3090
|
}
|
|
3091
|
+
// JoinedOrDeadLettered (NU-040 AC4): quiescent AND `pending` marked. No sink
|
|
3092
|
+
// clause — a marked sink must not excuse a stranded group.
|
|
2543
3093
|
case "joined-or-dead-lettered": {
|
|
2544
3094
|
const idx = flatNetIndexOf(flatNet, property.pending);
|
|
2545
3095
|
if (idx < 0) return false;
|
|
2546
|
-
return
|
|
3096
|
+
return isQuiescent(index, state) && state[idx] >= 1;
|
|
2547
3097
|
}
|
|
2548
3098
|
case "unreachable": {
|
|
2549
3099
|
let resolved = 0;
|
|
@@ -2557,7 +3107,7 @@ function satisfiesBadIndexed(index, state, property, sinkPlaces) {
|
|
|
2557
3107
|
}
|
|
2558
3108
|
}
|
|
2559
3109
|
}
|
|
2560
|
-
function replayCounterexample(flatNet, initial, decodedStates, property, sinkPlaces, options = {}) {
|
|
3110
|
+
function replayCounterexample(flatNet, initial, decodedStates, property, sinkPlaces, options = {}, conditionalSinks = []) {
|
|
2561
3111
|
const segmentBudget = options.segmentBudget ?? 3;
|
|
2562
3112
|
const nodeBudget = options.nodeBudget ?? 1e4;
|
|
2563
3113
|
const anchors = /* @__PURE__ */ new Set();
|
|
@@ -2574,7 +3124,7 @@ function replayCounterexample(flatNet, initial, decodedStates, property, sinkPla
|
|
|
2574
3124
|
};
|
|
2575
3125
|
}
|
|
2576
3126
|
const index = buildIndex(flatNet);
|
|
2577
|
-
if (satisfiesBadIndexed(index, initial, property, sinkPlaces)) {
|
|
3127
|
+
if (satisfiesBadIndexed(index, initial, property, sinkPlaces, conditionalSinks)) {
|
|
2578
3128
|
return { kind: "confirmed", states: [initial], steps: [], nodesExplored: 1 };
|
|
2579
3129
|
}
|
|
2580
3130
|
const nodes = [{ state: initial, step: null, parent: -1, segment: 0 }];
|
|
@@ -2603,7 +3153,7 @@ function replayCounterexample(flatNet, initial, decodedStates, property, sinkPla
|
|
|
2603
3153
|
}
|
|
2604
3154
|
nodes.push({ state: succ.state, step: succ.step, parent: idx, segment });
|
|
2605
3155
|
const childIdx = nodes.length - 1;
|
|
2606
|
-
if (satisfiesBadIndexed(index, succ.state, property, sinkPlaces)) {
|
|
3156
|
+
if (satisfiesBadIndexed(index, succ.state, property, sinkPlaces, conditionalSinks)) {
|
|
2607
3157
|
const chain = reconstruct(nodes, childIdx);
|
|
2608
3158
|
return { kind: "confirmed", ...chain, nodesExplored: nodes.length };
|
|
2609
3159
|
}
|
|
@@ -2751,7 +3301,7 @@ function buildLayout(plan, P) {
|
|
|
2751
3301
|
function quantified2(names) {
|
|
2752
3302
|
return names.map((v) => `(${v} Int)`).join(" ");
|
|
2753
3303
|
}
|
|
2754
|
-
function encodeColoured(plan, flat, initial, property, invariants, sinkPlaces) {
|
|
3304
|
+
function encodeColoured(plan, flat, initial, property, invariants, sinkPlaces, conditionalSinks = []) {
|
|
2755
3305
|
const P = flat.places.length;
|
|
2756
3306
|
const k = plan.k;
|
|
2757
3307
|
const lay = buildLayout(plan, P);
|
|
@@ -2820,13 +3370,13 @@ function encodeColoured(plan, flat, initial, property, invariants, sinkPlaces) {
|
|
|
2820
3370
|
}
|
|
2821
3371
|
}
|
|
2822
3372
|
lines.push("");
|
|
2823
|
-
const error = encodeError(plan, lay, flat, property, sinkPlaces, injectionMap(flat));
|
|
3373
|
+
const error = encodeError(plan, lay, flat, property, sinkPlaces, injectionMap(flat), conditionalSinks);
|
|
2824
3374
|
if (error == null) return null;
|
|
2825
3375
|
lines.push(error);
|
|
2826
3376
|
lines.push("");
|
|
2827
3377
|
lines.push("(assert (not Error))");
|
|
2828
3378
|
lines.push("(check-sat)");
|
|
2829
|
-
return { smt2: lines.join("\n"), placeCount: P };
|
|
3379
|
+
return { smt2: lines.join("\n"), placeCount: P, counterCount: 0 };
|
|
2830
3380
|
}
|
|
2831
3381
|
function encodeRule(plan, lay, invariants, fill) {
|
|
2832
3382
|
const enab = [];
|
|
@@ -2888,17 +3438,17 @@ function liftedInvariant(inv, plan, lay, names) {
|
|
|
2888
3438
|
terms.push(w === 1 ? agg : `(* ${w} ${agg})`);
|
|
2889
3439
|
}
|
|
2890
3440
|
if (terms.length === 0) return null;
|
|
2891
|
-
const
|
|
2892
|
-
return `(= ${
|
|
3441
|
+
const sum2 = terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
|
|
3442
|
+
return `(= ${sum2} ${inv.constant})`;
|
|
2893
3443
|
}
|
|
2894
|
-
function encodeError(plan, lay, flat, property, sinkPlaces, envInj) {
|
|
2895
|
-
const violation = encodeViolation(plan, lay, flat, property, sinkPlaces, envInj);
|
|
3444
|
+
function encodeError(plan, lay, flat, property, sinkPlaces, envInj, conditionalSinks) {
|
|
3445
|
+
const violation = encodeViolation(plan, lay, flat, property, sinkPlaces, envInj, conditionalSinks);
|
|
2896
3446
|
if (violation == null) return null;
|
|
2897
3447
|
return `(assert (forall (${quantified2(lay.cur)})
|
|
2898
3448
|
(=> (and (Reachable ${lay.cur.join(" ")}) ${violation})
|
|
2899
3449
|
Error)))`;
|
|
2900
3450
|
}
|
|
2901
|
-
function encodeViolation(plan, lay, flat, property, sinkPlaces, envInj) {
|
|
3451
|
+
function encodeViolation(plan, lay, flat, property, sinkPlaces, envInj, conditionalSinks) {
|
|
2902
3452
|
const anyPlacePresent = (places) => {
|
|
2903
3453
|
const conds = indexOrdered(flat, places).map((pid) => `(>= ${aggregate(plan, lay, pid, lay.cur)} 1)`);
|
|
2904
3454
|
return conds.length === 0 ? "false" : `(and ${conds.join(" ")})`;
|
|
@@ -2914,13 +3464,37 @@ function encodeViolation(plan, lay, flat, property, sinkPlaces, envInj) {
|
|
|
2914
3464
|
return anyPlacePresent([property.p1, property.p2]);
|
|
2915
3465
|
case "unreachable":
|
|
2916
3466
|
return anyPlacePresent(property.places);
|
|
2917
|
-
|
|
2918
|
-
|
|
3467
|
+
// DeadlockFree (VER-002): quiescent AND some marked place is not where resting
|
|
3468
|
+
// is permitted (VER-014). Mirrors the flat encoder's `stranded` disjunction over
|
|
3469
|
+
// the aggregate (all-colour) count of each place.
|
|
3470
|
+
case "deadlock-free": {
|
|
3471
|
+
const conds = encodeColouredQuiescent(plan, lay, flat, envInj);
|
|
3472
|
+
if (conds == null) return "false";
|
|
3473
|
+
const counts = [];
|
|
3474
|
+
for (let pid = 0; pid < flat.places.length; pid++) counts.push(aggregate(plan, lay, pid, lay.cur));
|
|
3475
|
+
const stranded = strandedConditions(strandingExcuses(flat, sinkPlaces, conditionalSinks), counts);
|
|
3476
|
+
if (stranded.length === 0) return "false";
|
|
3477
|
+
conds.push(`(or ${stranded.join(" ")})`);
|
|
3478
|
+
return joinColoured(conds);
|
|
3479
|
+
}
|
|
3480
|
+
// TerminatesAtSink (VER-002): quiescent AND no declared sink marked.
|
|
3481
|
+
case "terminates-at-sink": {
|
|
3482
|
+
const conds = encodeColouredQuiescent(plan, lay, flat, envInj);
|
|
3483
|
+
if (conds == null) return "false";
|
|
3484
|
+
for (const pid of indexOrdered(flat, sinkPlaces)) {
|
|
3485
|
+
conds.push(`(= ${aggregate(plan, lay, pid, lay.cur)} 0)`);
|
|
3486
|
+
}
|
|
3487
|
+
return joinColoured(conds);
|
|
3488
|
+
}
|
|
3489
|
+
// JoinedOrDeadLettered (NU-040 AC4): quiescent AND `pending` marked, with NO
|
|
3490
|
+
// sink clause.
|
|
2919
3491
|
case "joined-or-dead-lettered": {
|
|
2920
3492
|
const pid = flat.placeIndex.get(property.pending.name);
|
|
2921
3493
|
if (pid == null) return null;
|
|
2922
|
-
const
|
|
2923
|
-
|
|
3494
|
+
const conds = encodeColouredQuiescent(plan, lay, flat, envInj);
|
|
3495
|
+
if (conds == null) return "false";
|
|
3496
|
+
conds.push(`(>= ${aggregate(plan, lay, pid, lay.cur)} 1)`);
|
|
3497
|
+
return joinColoured(conds);
|
|
2924
3498
|
}
|
|
2925
3499
|
}
|
|
2926
3500
|
}
|
|
@@ -2978,7 +3552,10 @@ function colouredDisabledTerm(cls, plan, lay) {
|
|
|
2978
3552
|
}
|
|
2979
3553
|
}
|
|
2980
3554
|
}
|
|
2981
|
-
function
|
|
3555
|
+
function joinColoured(conds) {
|
|
3556
|
+
return conds.length === 0 ? "true" : `(and ${conds.join(" ")})`;
|
|
3557
|
+
}
|
|
3558
|
+
function encodeColouredQuiescent(plan, lay, flat, envInj) {
|
|
2982
3559
|
const disabledConditions = [];
|
|
2983
3560
|
for (let ti = 0; ti < plan.classes.length; ti++) {
|
|
2984
3561
|
const cls = plan.classes[ti];
|
|
@@ -2989,15 +3566,12 @@ function encodeColouredDeadlock(plan, lay, flat, sinkPlaces, envInj) {
|
|
|
2989
3566
|
disabledConditions.push("true");
|
|
2990
3567
|
continue;
|
|
2991
3568
|
}
|
|
2992
|
-
const
|
|
2993
|
-
if (
|
|
2994
|
-
if (reasons.length === 0) return
|
|
3569
|
+
const term2 = colouredDisabledTerm(cls, plan, lay);
|
|
3570
|
+
if (term2 != null) reasons.push(term2);
|
|
3571
|
+
if (reasons.length === 0) return null;
|
|
2995
3572
|
disabledConditions.push(reasons.length === 1 ? reasons[0] : `(or ${reasons.join(" ")})`);
|
|
2996
3573
|
}
|
|
2997
|
-
|
|
2998
|
-
disabledConditions.push(`(= ${aggregate(plan, lay, pid, lay.cur)} 0)`);
|
|
2999
|
-
}
|
|
3000
|
-
return disabledConditions.length === 0 ? "true" : `(and ${disabledConditions.join(" ")})`;
|
|
3574
|
+
return disabledConditions;
|
|
3001
3575
|
}
|
|
3002
3576
|
|
|
3003
3577
|
// src/verification/analysis/name-fragment.ts
|
|
@@ -3189,7 +3763,7 @@ var NameStateClass = class {
|
|
|
3189
3763
|
}
|
|
3190
3764
|
};
|
|
3191
3765
|
function baseKeyOf(base) {
|
|
3192
|
-
return `${base.marking.toString()}|${base.firingDomain.
|
|
3766
|
+
return `${base.marking.toString()}|${base.firingDomain.zoneKey()}`;
|
|
3193
3767
|
}
|
|
3194
3768
|
|
|
3195
3769
|
// src/verification/analysis/name-state-class-graph.ts
|
|
@@ -3420,7 +3994,7 @@ function enablingSymbols(names, colouredIn) {
|
|
|
3420
3994
|
|
|
3421
3995
|
// src/verification/nu-scg-verifier.ts
|
|
3422
3996
|
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";
|
|
3423
|
-
function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces, environmentMode, maxClasses, fragmentMode, carrierPlaces, prioritySemantics) {
|
|
3997
|
+
function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces, environmentMode, maxClasses, fragmentMode, carrierPlaces, prioritySemantics, conditionalSinks = []) {
|
|
3424
3998
|
const fragment = classify(net, fragmentMode, carrierPlaces);
|
|
3425
3999
|
if (fragment === null) return null;
|
|
3426
4000
|
for (const p of initial.placesWithTokens()) {
|
|
@@ -3447,9 +4021,9 @@ function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces,
|
|
|
3447
4021
|
classCount: scg.classCount()
|
|
3448
4022
|
};
|
|
3449
4023
|
}
|
|
3450
|
-
const violating = decide(scg, property, sinkPlaces);
|
|
4024
|
+
const violating = decide(scg, property, sinkPlaces, conditionalSinks);
|
|
3451
4025
|
if (violating >= 0) {
|
|
3452
|
-
const [trace, transitions] =
|
|
4026
|
+
const [trace, transitions] = counterexamplePath2(scg, violating);
|
|
3453
4027
|
return { verdict: { type: "violated" }, trace, transitions, note: NOTE_EXACT, classCount: scg.classCount() };
|
|
3454
4028
|
}
|
|
3455
4029
|
return {
|
|
@@ -3460,45 +4034,19 @@ function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces,
|
|
|
3460
4034
|
classCount: scg.classCount()
|
|
3461
4035
|
};
|
|
3462
4036
|
}
|
|
3463
|
-
function decide(scg, property, sinkPlaces) {
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
case "unreachable":
|
|
3475
|
-
return firstWhere((i) => {
|
|
3476
|
-
const m = scg.markingOf(i);
|
|
3477
|
-
for (const p of property.places) {
|
|
3478
|
-
if (!m.hasTokens(p)) return false;
|
|
3479
|
-
}
|
|
3480
|
-
return true;
|
|
3481
|
-
});
|
|
3482
|
-
case "mutual-exclusion":
|
|
3483
|
-
return firstWhere((i) => {
|
|
3484
|
-
const m = scg.markingOf(i);
|
|
3485
|
-
return m.hasTokens(property.p1) && m.hasTokens(property.p2);
|
|
3486
|
-
});
|
|
3487
|
-
case "deadlock-free":
|
|
3488
|
-
return firstWhere((i) => scg.successorsOf(i).length === 0 && !allTokensInSinks(scg.markingOf(i), sinkPlaces));
|
|
3489
|
-
case "joined-or-dead-lettered":
|
|
3490
|
-
return firstWhere((i) => scg.successorsOf(i).length === 0 && scg.markingOf(i).hasTokens(property.pending));
|
|
3491
|
-
}
|
|
3492
|
-
}
|
|
3493
|
-
function allTokensInSinks(m, sinks) {
|
|
3494
|
-
const sinkNames = /* @__PURE__ */ new Set();
|
|
3495
|
-
for (const s of sinks) sinkNames.add(s.name);
|
|
3496
|
-
for (const p of m.placesWithTokens()) {
|
|
3497
|
-
if (!sinkNames.has(p.name)) return false;
|
|
3498
|
-
}
|
|
3499
|
-
return true;
|
|
4037
|
+
function decide(scg, property, sinkPlaces, conditionalSinks) {
|
|
4038
|
+
return decideOverClasses(
|
|
4039
|
+
{
|
|
4040
|
+
count: scg.classCount(),
|
|
4041
|
+
markingOf: (i) => scg.markingOf(i),
|
|
4042
|
+
isQuiescent: (i) => scg.successorsOf(i).length === 0
|
|
4043
|
+
},
|
|
4044
|
+
property,
|
|
4045
|
+
sinkPlaces,
|
|
4046
|
+
conditionalSinks
|
|
4047
|
+
);
|
|
3500
4048
|
}
|
|
3501
|
-
function
|
|
4049
|
+
function counterexamplePath2(scg, target) {
|
|
3502
4050
|
const n = scg.classCount();
|
|
3503
4051
|
const parent = new Array(n).fill(-1);
|
|
3504
4052
|
const via = new Array(n).fill("");
|
|
@@ -3538,13 +4086,17 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3538
4086
|
_property = deadlockFree();
|
|
3539
4087
|
_environmentPlaces = /* @__PURE__ */ new Set();
|
|
3540
4088
|
_sinkPlaces = /* @__PURE__ */ new Set();
|
|
4089
|
+
_conditionalSinks = [];
|
|
3541
4090
|
_budgetPlaces = /* @__PURE__ */ new Set();
|
|
3542
4091
|
_environmentMode = alwaysAvailable();
|
|
3543
4092
|
_timeoutMs = 6e4;
|
|
3544
4093
|
_certificateCheck = true;
|
|
3545
4094
|
_counterexampleReplay = true;
|
|
3546
4095
|
_semiflowInvariants = false;
|
|
4096
|
+
_stateEquation = false;
|
|
4097
|
+
_linearBound = true;
|
|
3547
4098
|
_nuMaxClasses = 1e5;
|
|
4099
|
+
_enumerationMaxClasses = 5e4;
|
|
3548
4100
|
_fragmentMode = "base";
|
|
3549
4101
|
_carrierPlaces = /* @__PURE__ */ new Set();
|
|
3550
4102
|
_prioritySemantics = "none";
|
|
@@ -3574,13 +4126,47 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3574
4126
|
return this;
|
|
3575
4127
|
}
|
|
3576
4128
|
/**
|
|
3577
|
-
* Declares expected sink (terminal) places for deadlock-freedom analysis
|
|
3578
|
-
*
|
|
4129
|
+
* Declares expected sink (terminal) places for deadlock-freedom analysis
|
|
4130
|
+
* (VER-002): a token resting in one is never stranded, and `TerminatesAtSink`
|
|
4131
|
+
* asks whether one of them was reached.
|
|
3579
4132
|
*/
|
|
3580
4133
|
sinkPlaces(...places) {
|
|
3581
4134
|
for (const p of places) this._sinkPlaces.add(p);
|
|
3582
4135
|
return this;
|
|
3583
4136
|
}
|
|
4137
|
+
/**
|
|
4138
|
+
* Declares places where a token may rest **while `marker` holds a token**
|
|
4139
|
+
* (VER-014) — a designed terminal such as a halt or pause marker, under which
|
|
4140
|
+
* the work it interrupted legitimately stays where it was delivered.
|
|
4141
|
+
*
|
|
4142
|
+
* `DeadlockFree` then reads a quiescent marking against the union of the
|
|
4143
|
+
* declared sinks, the markers, and every conditional set whose marker is marked:
|
|
4144
|
+
* a token in `p` is stranded only when none of those excuse it. The marker
|
|
4145
|
+
* itself is at rest whenever it is marked, so `sinkPlacesWhen(halt)` with no
|
|
4146
|
+
* further places excuses exactly the halt token. Repeated calls for one marker
|
|
4147
|
+
* accumulate; declarations for several markers union. `TerminatesAtSink` is
|
|
4148
|
+
* unaffected and reads only {@link sinkPlaces}.
|
|
4149
|
+
*
|
|
4150
|
+
* ```ts
|
|
4151
|
+
* SmtVerifier.forNet(net)
|
|
4152
|
+
* .property(deadlockFree())
|
|
4153
|
+
* .sinkPlaces(done) // may always rest
|
|
4154
|
+
* .sinkPlacesWhen(halt, inbox, pending) // may rest once the run halted
|
|
4155
|
+
* .sinkPlacesWhen(pause, inbox) // may rest while paused
|
|
4156
|
+
* ```
|
|
4157
|
+
*
|
|
4158
|
+
* An unresolved marker or place contributes nothing, as an unresolved sink
|
|
4159
|
+
* does: a mistyped marker makes the property stricter, never laxer.
|
|
4160
|
+
*/
|
|
4161
|
+
sinkPlacesWhen(marker, ...places) {
|
|
4162
|
+
let entry = this._conditionalSinks.find((c) => c.marker.name === marker.name);
|
|
4163
|
+
if (entry == null) {
|
|
4164
|
+
entry = { marker, places: /* @__PURE__ */ new Set() };
|
|
4165
|
+
this._conditionalSinks.push(entry);
|
|
4166
|
+
}
|
|
4167
|
+
for (const p of places) entry.places.add(p);
|
|
4168
|
+
return this;
|
|
4169
|
+
}
|
|
3584
4170
|
/**
|
|
3585
4171
|
* Declares ν-net budget places (NU-040): places whose token count bounds the
|
|
3586
4172
|
* live correlation pool (they gate fresh-name minting). Declaring at least one
|
|
@@ -3658,10 +4244,67 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3658
4244
|
* `Certificate check: not applicable (name-coloured encoding)`. Off by default so
|
|
3659
4245
|
* reports stay byte-equal.
|
|
3660
4246
|
*/
|
|
4247
|
+
/**
|
|
4248
|
+
* `'auto'` decides whether the semiflows would add **information to the
|
|
4249
|
+
* encoding**, which is not the same question as whether they would appear in
|
|
4250
|
+
* {@link SmtVerificationResult.invariants} for a caller who reads them.
|
|
4251
|
+
*
|
|
4252
|
+
* A complete basis spans every conservation law of the net, so a semiflow it
|
|
4253
|
+
* spans constrains nothing further and IC3 gains nothing from it — that is why
|
|
4254
|
+
* `'auto'` skips the enumeration there. But the basis is the *signed*
|
|
4255
|
+
* null-space, and a law it spans need not appear in it in **non-negative**
|
|
4256
|
+
* form; only the Farkas enumeration produces that. A caller inspecting the
|
|
4257
|
+
* invariant list for a law of a given shape — "a non-negative law weighting the
|
|
4258
|
+
* budget place and every running place positively" — can therefore find nothing
|
|
4259
|
+
* on a net that plainly has one. Such a caller should ask for the union
|
|
4260
|
+
* explicitly: `'auto'` is the setting to prefer for verification, not for
|
|
4261
|
+
* harvesting.
|
|
4262
|
+
*/
|
|
3661
4263
|
semiflowInvariants(enabled) {
|
|
3662
4264
|
this._semiflowInvariants = enabled;
|
|
3663
4265
|
return this;
|
|
3664
4266
|
}
|
|
4267
|
+
/**
|
|
4268
|
+
* Enables/disables the linear state-equation bound phase (VER-015; default:
|
|
4269
|
+
* enabled). A reachability-safety property whose violating markings exceed some
|
|
4270
|
+
* `y·M <= y·M0` with `y >= 0`, `y·C <= 0` is then proven structurally, from one
|
|
4271
|
+
* linear query re-checked in exact integer arithmetic, before any fixpoint search.
|
|
4272
|
+
* Disable it to force the IC3/PDR path — for its certificate, or to exercise the
|
|
4273
|
+
* fixpoint engine itself.
|
|
4274
|
+
*/
|
|
4275
|
+
linearBound(enabled) {
|
|
4276
|
+
this._linearBound = enabled;
|
|
4277
|
+
return this;
|
|
4278
|
+
}
|
|
4279
|
+
/**
|
|
4280
|
+
* Encodes the **state equation** with firing counters (VER-016; default:
|
|
4281
|
+
* disabled — the encoding then carries places only).
|
|
4282
|
+
*
|
|
4283
|
+
* The flat encoding gains one counter `n_t` per flat transition and every
|
|
4284
|
+
* transition rule conjoins the marking equation `M' = M0 + C·n'` for each place
|
|
4285
|
+
* whose column is exact (no consume-all / reset arc, not injected). Every linear
|
|
4286
|
+
* consequence of the marking equation — the equality laws of VER-005/VER-007
|
|
4287
|
+
* **and** the inequality laws `y·M ≤ y·M0` (`y ≥ 0, y·C ≤ 0`) and their mixed-sign
|
|
4288
|
+
* kin, which are what an *ordering* argument ("both join slots armed means every
|
|
4289
|
+
* upstream stage has run, so nothing can still halt") looks like in linear
|
|
4290
|
+
* arithmetic — is then available to Spacer as a fact rather than a lemma it has to
|
|
4291
|
+
* invent. On a 50-place agent-dispatch workflow, proper completion under conditional
|
|
4292
|
+
* sinks went from `unknown` after 120 s to `proven` in 1.5 s with this as the only
|
|
4293
|
+
* change; a 53-place pipeline stage before a join, `unknown` at 300 s, proves in
|
|
4294
|
+
* under a second.
|
|
4295
|
+
*
|
|
4296
|
+
* The cost is a larger state (places + transitions) and a slower witness search
|
|
4297
|
+
* on genuinely violated properties (about 1.5× on the nets above), so it is opt-in.
|
|
4298
|
+
* Soundness is unchanged: the counters are exact bookkeeping, the equation holds
|
|
4299
|
+
* on every reachable state by construction (`Strengthening.lean`, the same shape
|
|
4300
|
+
* as the equality laws), and the certificate check re-proves it against the raw
|
|
4301
|
+
* step relation, whose only counter knowledge is the increment. Not applied to the
|
|
4302
|
+
* name-coloured encoding or Route B, which the report says when it applies.
|
|
4303
|
+
*/
|
|
4304
|
+
stateEquation(enabled) {
|
|
4305
|
+
this._stateEquation = enabled;
|
|
4306
|
+
return this;
|
|
4307
|
+
}
|
|
3665
4308
|
/**
|
|
3666
4309
|
* Sets the class-count cap for the ν-aware state-class-graph analysis (NU-050,
|
|
3667
4310
|
* Route B). When the symbolic name-aware graph would exceed this, the analysis
|
|
@@ -3672,6 +4315,29 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3672
4315
|
this._nuMaxClasses = max;
|
|
3673
4316
|
return this;
|
|
3674
4317
|
}
|
|
4318
|
+
/**
|
|
4319
|
+
* Sets the class budget for the bounded state-space enumeration route
|
|
4320
|
+
* (VER-017; default 50 000). `0` disables the route, so every query goes to
|
|
4321
|
+
* the SMT pipeline.
|
|
4322
|
+
*
|
|
4323
|
+
* When the state-class graph closes within the budget the property is decided
|
|
4324
|
+
* exactly — sound and complete over the timed semantics — and no solver runs.
|
|
4325
|
+
* This is what makes a long pipeline tractable: IC3 needs a frame per stage and
|
|
4326
|
+
* its cost climbs with the cube of the length, while enumeration is linear in
|
|
4327
|
+
* the reachable state space. A forty-node chain (370 places, 1 967 classes)
|
|
4328
|
+
* takes 410 s on the fixpoint path and 0.11 s here.
|
|
4329
|
+
*
|
|
4330
|
+
* The route declines when the graph exceeds the budget, and the SMT pipeline
|
|
4331
|
+
* then runs unchanged — it can only add verdicts, never remove them. It is
|
|
4332
|
+
* skipped for ν-nets, which have their own exact route (NU-050, Route B), for
|
|
4333
|
+
* nets with environment places, whose injection the graph does not model, and
|
|
4334
|
+
* for **timed** nets, where its verdict would be the weaker timed claim rather
|
|
4335
|
+
* than the untimed one the encoders make (VER-004).
|
|
4336
|
+
*/
|
|
4337
|
+
enumerationMaxClasses(max) {
|
|
4338
|
+
this._enumerationMaxClasses = max;
|
|
4339
|
+
return this;
|
|
4340
|
+
}
|
|
3675
4341
|
/**
|
|
3676
4342
|
* Selects the ν-net coloured-place fragment for Route B (NU-051). `base`
|
|
3677
4343
|
* (default) admits the shipped mint → matched-join fragment only; `extended`
|
|
@@ -3754,7 +4420,8 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3754
4420
|
this._initialMarking,
|
|
3755
4421
|
this._property,
|
|
3756
4422
|
invariants,
|
|
3757
|
-
this._sinkPlaces
|
|
4423
|
+
this._sinkPlaces,
|
|
4424
|
+
this._conditionalSinks
|
|
3758
4425
|
)
|
|
3759
4426
|
};
|
|
3760
4427
|
}
|
|
@@ -3771,35 +4438,45 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3771
4438
|
requireOutputProducingActions(this.net);
|
|
3772
4439
|
const flatNet = flatten(this.net, this._environmentPlaces, this._environmentMode);
|
|
3773
4440
|
const matrix = IncidenceMatrix.from(flatNet);
|
|
3774
|
-
const { valid: basis } = validateInvariantsExact(
|
|
4441
|
+
const { valid: basis, dropped: basisDropped } = validateInvariantsExact(
|
|
3775
4442
|
matrix,
|
|
3776
4443
|
computePInvariants(matrix, flatNet, this._initialMarking),
|
|
3777
4444
|
flatNet,
|
|
3778
4445
|
this._initialMarking
|
|
3779
4446
|
);
|
|
3780
|
-
const
|
|
4447
|
+
const autoUnion = this._semiflowInvariants === "auto" && basisDropped.some((d) => d.reason.includes("Strengthening.lean H1"));
|
|
4448
|
+
const scriptsHasMatch = [...this.net.transitions].some((t) => t.matchSpec !== null);
|
|
4449
|
+
const { valid: semiflows } = this._semiflowInvariants === true || autoUnion || scriptsHasMatch && this._budgetPlaces.size > 0 ? validateInvariantsExact(
|
|
3781
4450
|
matrix,
|
|
3782
4451
|
computePSemiflows(matrix, flatNet, this._initialMarking),
|
|
3783
4452
|
flatNet,
|
|
3784
4453
|
this._initialMarking
|
|
3785
|
-
);
|
|
4454
|
+
) : { valid: [] };
|
|
3786
4455
|
let invariants = basis;
|
|
3787
|
-
if (this._semiflowInvariants) invariants = strengthenWithSemiflows(basis, semiflows).invariants;
|
|
4456
|
+
if (this._semiflowInvariants === true || autoUnion) invariants = strengthenWithSemiflows(basis, semiflows).invariants;
|
|
3788
4457
|
invariants = canonicalInvariantOrder(invariants);
|
|
3789
4458
|
const attempt = this.colouredAttempt(flatNet, invariants, semiflows);
|
|
4459
|
+
const bound = attempt.plan == null && this._linearBound && !(this._environmentPlaces.size > 0 && this._environmentMode.type === "ignore") ? encodeLinearBound(flatNet, this._initialMarking, this._property) : null;
|
|
3790
4460
|
if (attempt.encoding != null) {
|
|
3791
|
-
return { horn: attempt.encoding.smt2, certificate: null, coloured: true };
|
|
4461
|
+
return { horn: attempt.encoding.smt2, certificate: null, coloured: true, bound };
|
|
3792
4462
|
}
|
|
3793
|
-
const
|
|
4463
|
+
const flat = encodeNet(flatNet, this._initialMarking, this._property, invariants, {
|
|
4464
|
+
sinkPlaces: this._sinkPlaces,
|
|
4465
|
+
produceProofs: this._counterexampleReplay,
|
|
4466
|
+
conditionalSinks: this._conditionalSinks,
|
|
4467
|
+
stateEquation: this._stateEquation
|
|
4468
|
+
});
|
|
3794
4469
|
const certificate = vcScript(
|
|
3795
|
-
placeholderCertificate(flatNet.places.length),
|
|
4470
|
+
placeholderCertificate(flatNet.places.length + flat.counterCount),
|
|
3796
4471
|
flatNet,
|
|
3797
4472
|
this._initialMarking,
|
|
3798
4473
|
this._property,
|
|
3799
4474
|
this._sinkPlaces,
|
|
3800
|
-
invariants
|
|
4475
|
+
invariants,
|
|
4476
|
+
this._conditionalSinks,
|
|
4477
|
+
this._stateEquation
|
|
3801
4478
|
);
|
|
3802
|
-
return { horn, certificate, coloured: false };
|
|
4479
|
+
return { horn: flat.smt2, certificate, coloured: false, bound };
|
|
3803
4480
|
}
|
|
3804
4481
|
/**
|
|
3805
4482
|
* Runs the verification pipeline.
|
|
@@ -3812,10 +4489,34 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3812
4489
|
const report = [];
|
|
3813
4490
|
report.push("=== IC3/PDR SAFETY VERIFICATION ===\n");
|
|
3814
4491
|
report.push(`Net: ${this.net.name}`);
|
|
3815
|
-
const
|
|
4492
|
+
const sinkDesc = describeSinks(this._sinkPlaces, this._conditionalSinks);
|
|
4493
|
+
const propDesc = sinkDesc === null ? propertyDescription(this._property) : `${propertyDescription(this._property)} (${sinkDesc})`;
|
|
3816
4494
|
report.push(`Property: ${propDesc}`);
|
|
3817
4495
|
report.push(`Timeout: ${(this._timeoutMs / 1e3).toFixed(0)}s
|
|
3818
4496
|
`);
|
|
4497
|
+
const absent = unresolvedPropertyPlaceInNet(this.net, this._property);
|
|
4498
|
+
if (absent != null) {
|
|
4499
|
+
const reason = `property names a place that does not resolve in the net ('${absent}'); refusing to certify (the encoding would be vacuously proven)`;
|
|
4500
|
+
report.push("=== RESULT ===\n");
|
|
4501
|
+
report.push(`UNKNOWN: ${reason}`);
|
|
4502
|
+
return buildResult(
|
|
4503
|
+
{ type: "unknown", reason },
|
|
4504
|
+
report.join("\n"),
|
|
4505
|
+
[],
|
|
4506
|
+
[],
|
|
4507
|
+
[],
|
|
4508
|
+
[],
|
|
4509
|
+
performance.now() - start,
|
|
4510
|
+
{
|
|
4511
|
+
places: [...this.net.places].length,
|
|
4512
|
+
transitions: [...this.net.transitions].length,
|
|
4513
|
+
invariantsFound: 0,
|
|
4514
|
+
structuralResult: "n/a (unresolved property place)"
|
|
4515
|
+
},
|
|
4516
|
+
null,
|
|
4517
|
+
"unavailable"
|
|
4518
|
+
);
|
|
4519
|
+
}
|
|
3819
4520
|
const hasMatch = [...this.net.transitions].some((t) => t.matchSpec !== null);
|
|
3820
4521
|
const nuBounded = this._budgetPlaces.size > 0;
|
|
3821
4522
|
if (hasMatch && (!isReachabilitySafety(this._property) || !nuBounded)) {
|
|
@@ -3829,7 +4530,8 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3829
4530
|
this._nuMaxClasses,
|
|
3830
4531
|
this._fragmentMode,
|
|
3831
4532
|
this._carrierPlaces,
|
|
3832
|
-
this._prioritySemantics
|
|
4533
|
+
this._prioritySemantics,
|
|
4534
|
+
this._conditionalSinks
|
|
3833
4535
|
);
|
|
3834
4536
|
const deferToRouteA = outcome !== null && outcome.verdict.type === "unknown" && !isReachabilitySafety(this._property) && nuBounded;
|
|
3835
4537
|
if (outcome !== null && !deferToRouteA) {
|
|
@@ -3857,7 +4559,9 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3857
4559
|
transitions: [...this.net.transitions].length,
|
|
3858
4560
|
invariantsFound: 0,
|
|
3859
4561
|
structuralResult: "n/a (\u03BD name-partition SCG)"
|
|
3860
|
-
}
|
|
4562
|
+
},
|
|
4563
|
+
null,
|
|
4564
|
+
"nu-scg"
|
|
3861
4565
|
);
|
|
3862
4566
|
} else if (deferToRouteA) {
|
|
3863
4567
|
report.push(
|
|
@@ -3870,6 +4574,47 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3870
4574
|
);
|
|
3871
4575
|
}
|
|
3872
4576
|
}
|
|
4577
|
+
if (!hasMatch && this._environmentPlaces.size === 0 && this._enumerationMaxClasses > 0 && isUntimed(this.net)) {
|
|
4578
|
+
const enumerated = verifyViaStateClassGraph(
|
|
4579
|
+
this.net,
|
|
4580
|
+
this._initialMarking,
|
|
4581
|
+
this._property,
|
|
4582
|
+
this._sinkPlaces,
|
|
4583
|
+
this._enumerationMaxClasses,
|
|
4584
|
+
this._conditionalSinks
|
|
4585
|
+
);
|
|
4586
|
+
if (enumerated.kind === "decided") {
|
|
4587
|
+
report.push("=== Bounded state-space enumeration (VER-017) ===");
|
|
4588
|
+
report.push(` State classes: ${enumerated.classCount}`);
|
|
4589
|
+
report.push(" P-invariants: not computed (no encoding is built on this route)");
|
|
4590
|
+
report.push(NOTE_ENUMERATED);
|
|
4591
|
+
if (enumerated.transitions.length > 0) {
|
|
4592
|
+
report.push(` Counterexample trace: ${enumerated.trace.length} states, ${enumerated.transitions.length} transitions`);
|
|
4593
|
+
}
|
|
4594
|
+
return buildResult(
|
|
4595
|
+
enumerated.verdict,
|
|
4596
|
+
report.join("\n"),
|
|
4597
|
+
[],
|
|
4598
|
+
[],
|
|
4599
|
+
enumerated.trace,
|
|
4600
|
+
enumerated.transitions,
|
|
4601
|
+
performance.now() - start,
|
|
4602
|
+
{
|
|
4603
|
+
places: [...this.net.places].length,
|
|
4604
|
+
transitions: [...this.net.transitions].length,
|
|
4605
|
+
invariantsFound: 0,
|
|
4606
|
+
structuralResult: "n/a (state-space enumeration)"
|
|
4607
|
+
},
|
|
4608
|
+
// The graph path IS a firing sequence, so a violation is ordered and
|
|
4609
|
+
// confirmed by construction; there is nothing left to replay.
|
|
4610
|
+
enumerated.verdict.type === "violated" ? true : null,
|
|
4611
|
+
"enumeration"
|
|
4612
|
+
);
|
|
4613
|
+
}
|
|
4614
|
+
report.push(
|
|
4615
|
+
`Bounded state-space enumeration truncated at ${this._enumerationMaxClasses} classes (VER-017); verifying via the SMT pipeline.`
|
|
4616
|
+
);
|
|
4617
|
+
}
|
|
3873
4618
|
report.push("Phase 1: Flattening net...");
|
|
3874
4619
|
const flatNet = flatten(this.net, this._environmentPlaces, this._environmentMode);
|
|
3875
4620
|
report.push(` Places: ${flatNet.places.length}`);
|
|
@@ -3894,7 +4639,7 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3894
4639
|
}
|
|
3895
4640
|
report.push(` Result: ${structResultStr}
|
|
3896
4641
|
`);
|
|
3897
|
-
if (this._property.type === "deadlock-free" && !hasMatch && this._sinkPlaces.size === 0 && structResult.type === "no-potential-deadlock" && this._environmentPlaces.size === 0) {
|
|
4642
|
+
if (this._property.type === "deadlock-free" && !hasMatch && commonerApplies(flatNet) && this._sinkPlaces.size === 0 && this._conditionalSinks.length === 0 && structResult.type === "no-potential-deadlock" && this._environmentPlaces.size === 0) {
|
|
3898
4643
|
report.push("=== RESULT ===\n");
|
|
3899
4644
|
report.push("PROVEN (structural): Deadlock-freedom verified by Commoner's theorem.");
|
|
3900
4645
|
report.push(" All siphons contain initially marked traps.");
|
|
@@ -3907,7 +4652,9 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3907
4652
|
[],
|
|
3908
4653
|
[],
|
|
3909
4654
|
performance.now() - start,
|
|
3910
|
-
{ places: flatNet.places.length, transitions: flatNet.transitions.length, invariantsFound: 0, structuralResult: structResultStr }
|
|
4655
|
+
{ places: flatNet.places.length, transitions: flatNet.transitions.length, invariantsFound: 0, structuralResult: structResultStr },
|
|
4656
|
+
null,
|
|
4657
|
+
"structural"
|
|
3911
4658
|
);
|
|
3912
4659
|
}
|
|
3913
4660
|
report.push("Phase 3: Computing P-invariants...");
|
|
@@ -3918,15 +4665,21 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3918
4665
|
flatNet,
|
|
3919
4666
|
this._initialMarking
|
|
3920
4667
|
);
|
|
3921
|
-
const
|
|
4668
|
+
const basisLostALaw = droppedInvariants.some((d) => d.reason.includes("Strengthening.lean H1"));
|
|
4669
|
+
const semiflowsWanted = this._semiflowInvariants === true || this._semiflowInvariants === "auto" && basisLostALaw || hasMatch && nuBounded;
|
|
4670
|
+
const { valid: semiflows, dropped: droppedSemiflows } = semiflowsWanted ? validateInvariantsExact(
|
|
3922
4671
|
matrix,
|
|
3923
4672
|
computePSemiflows(matrix, flatNet, this._initialMarking),
|
|
3924
4673
|
flatNet,
|
|
3925
4674
|
this._initialMarking
|
|
3926
|
-
);
|
|
4675
|
+
) : { valid: [], dropped: [] };
|
|
3927
4676
|
report.push(` Found: ${basisInvariants.length} P-invariant(s)`);
|
|
4677
|
+
if (this._semiflowInvariants === "auto") {
|
|
4678
|
+
report.push(basisLostALaw ? " Semiflow union: ON (auto \u2014 the basis lost a law to the H1 guard)" : " Semiflow union: off (auto \u2014 the basis is complete, so the semiflows would add no constraint the encoding does not already have; they may still differ in FORM)");
|
|
4679
|
+
}
|
|
4680
|
+
const unionWanted = this._semiflowInvariants === true || this._semiflowInvariants === "auto" && basisLostALaw;
|
|
3928
4681
|
let invariants = basisInvariants;
|
|
3929
|
-
if (
|
|
4682
|
+
if (unionWanted) {
|
|
3930
4683
|
const { invariants: strengthened, added } = strengthenWithSemiflows(basisInvariants, semiflows);
|
|
3931
4684
|
invariants = strengthened;
|
|
3932
4685
|
report.push(` Semiflows encoded as invariants: ${added}`);
|
|
@@ -3950,6 +4703,11 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3950
4703
|
report.push(` Dropped: ${droppedSemiflows.length} semiflow(s) failed the exact re-check`);
|
|
3951
4704
|
}
|
|
3952
4705
|
report.push("");
|
|
4706
|
+
if (!isReachabilitySafety(this._property) && quiescenceUnreachable(flatNet, resolveEnvInjection(flatNet))) {
|
|
4707
|
+
report.push(
|
|
4708
|
+
" NOTE: no marking of this net can be quiescent \u2014 a transition is enabled in every marking (an environment-gated one under modelled injection, VER-006). Every quiescence property is therefore vacuously true here, and a `proven` says nothing about the net."
|
|
4709
|
+
);
|
|
4710
|
+
}
|
|
3953
4711
|
report.push("Phase 4: IC3/PDR verification via Z3 Spacer...");
|
|
3954
4712
|
const stats = {
|
|
3955
4713
|
places: flatNet.places.length,
|
|
@@ -3961,6 +4719,7 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3961
4719
|
try {
|
|
3962
4720
|
solver = resolveZ3();
|
|
3963
4721
|
} catch (e) {
|
|
4722
|
+
rethrowIfProgrammingError(e);
|
|
3964
4723
|
const reason = e instanceof Z3Unavailable ? e.message : String(e?.message ?? e);
|
|
3965
4724
|
report.push(` Solver: z3 unavailable (${reason})`);
|
|
3966
4725
|
report.push(` Status: UNKNOWN (${reason})
|
|
@@ -3968,11 +4727,35 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3968
4727
|
report.push("=== RESULT ===\n");
|
|
3969
4728
|
report.push(`UNKNOWN: Could not determine ${propDesc}`);
|
|
3970
4729
|
report.push(` Reason: ${reason}`);
|
|
3971
|
-
return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats);
|
|
4730
|
+
return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats, null, "unavailable");
|
|
3972
4731
|
}
|
|
3973
4732
|
report.push(` Solver: z3 ${formatZ3Version(solver.version)}`);
|
|
3974
4733
|
const colouredAttempt = this.colouredAttempt(flatNet, invariants, semiflows);
|
|
3975
4734
|
const colouredPlan = colouredAttempt.plan;
|
|
4735
|
+
if (this._linearBound && colouredPlan == null && isReachabilitySafety(this._property) && !(this._environmentPlaces.size > 0 && this._environmentMode.type === "ignore")) {
|
|
4736
|
+
const proof = await this.linearBoundProof(flatNet, solver, report);
|
|
4737
|
+
if (proof != null) {
|
|
4738
|
+
report.push(" Certificate check: not applicable (structural proof)");
|
|
4739
|
+
report.push("");
|
|
4740
|
+
report.push("=== RESULT ===\n");
|
|
4741
|
+
report.push(`PROVEN (structural): ${propDesc}`);
|
|
4742
|
+
report.push(" Linear state-equation bound: y >= 0 with y.C <= 0 gives y.M <= y.M0 on every");
|
|
4743
|
+
report.push(" reachable marking, and the violating markings exceed it (VER-015).");
|
|
4744
|
+
report.push(` ${proof}`);
|
|
4745
|
+
return this.applyNuGuard(buildResult(
|
|
4746
|
+
{ type: "proven", method: "structural", inductiveInvariant: null },
|
|
4747
|
+
report.join("\n"),
|
|
4748
|
+
invariants,
|
|
4749
|
+
[],
|
|
4750
|
+
[],
|
|
4751
|
+
[],
|
|
4752
|
+
performance.now() - start,
|
|
4753
|
+
stats,
|
|
4754
|
+
null,
|
|
4755
|
+
"structural"
|
|
4756
|
+
), hasMatch, nuBounded, false);
|
|
4757
|
+
}
|
|
4758
|
+
}
|
|
3976
4759
|
let encoding;
|
|
3977
4760
|
if (colouredPlan != null) {
|
|
3978
4761
|
report.push(
|
|
@@ -3984,7 +4767,7 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3984
4767
|
report.push(" Status: UNKNOWN (unresolved property place)\n");
|
|
3985
4768
|
report.push("=== RESULT ===\n");
|
|
3986
4769
|
report.push(`UNKNOWN: ${reason}`);
|
|
3987
|
-
return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats);
|
|
4770
|
+
return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats, null, "unavailable");
|
|
3988
4771
|
}
|
|
3989
4772
|
encoding = coloured;
|
|
3990
4773
|
} else {
|
|
@@ -3994,9 +4777,20 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
3994
4777
|
report.push(" Status: UNKNOWN (unresolved property place)\n");
|
|
3995
4778
|
report.push("=== RESULT ===\n");
|
|
3996
4779
|
report.push(`UNKNOWN: ${reason}`);
|
|
3997
|
-
return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats);
|
|
4780
|
+
return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats, null, "unavailable");
|
|
3998
4781
|
}
|
|
3999
|
-
encoding =
|
|
4782
|
+
encoding = encodeNet(flatNet, this._initialMarking, this._property, invariants, {
|
|
4783
|
+
sinkPlaces: this._sinkPlaces,
|
|
4784
|
+
produceProofs: this._counterexampleReplay,
|
|
4785
|
+
conditionalSinks: this._conditionalSinks,
|
|
4786
|
+
stateEquation: this._stateEquation
|
|
4787
|
+
});
|
|
4788
|
+
if (this._stateEquation) {
|
|
4789
|
+
report.push(` State equation: encoded over ${encoding.counterCount} firing counters (VER-016)`);
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
if (this._stateEquation && colouredPlan != null) {
|
|
4793
|
+
report.push(" State equation: not applied (name-coloured encoding)");
|
|
4000
4794
|
}
|
|
4001
4795
|
const queryResult = await runZ3Spacer(
|
|
4002
4796
|
solver,
|
|
@@ -4028,7 +4822,9 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
4028
4822
|
invariants,
|
|
4029
4823
|
this._sinkPlaces,
|
|
4030
4824
|
solver,
|
|
4031
|
-
this._timeoutMs
|
|
4825
|
+
this._timeoutMs,
|
|
4826
|
+
this._conditionalSinks,
|
|
4827
|
+
this._stateEquation
|
|
4032
4828
|
);
|
|
4033
4829
|
const reason = certificateDowngradeReason(certificate);
|
|
4034
4830
|
if (reason != null) {
|
|
@@ -4072,7 +4868,7 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
4072
4868
|
}
|
|
4073
4869
|
case "violated": {
|
|
4074
4870
|
report.push(" Status: SAT (counterexample found)\n");
|
|
4075
|
-
const decoded = decode(queryResult.answer, flatNet);
|
|
4871
|
+
const decoded = decode(queryResult.answer, flatNet, encoding.counterCount);
|
|
4076
4872
|
if (decoded.note != null) report.push(` Counterexample decoding: ${decoded.note}`);
|
|
4077
4873
|
let confirmed = null;
|
|
4078
4874
|
let trace = [...decoded.states];
|
|
@@ -4084,7 +4880,8 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
4084
4880
|
this._initialMarking,
|
|
4085
4881
|
decoded.states,
|
|
4086
4882
|
this._property,
|
|
4087
|
-
this._sinkPlaces
|
|
4883
|
+
this._sinkPlaces,
|
|
4884
|
+
this._conditionalSinks
|
|
4088
4885
|
);
|
|
4089
4886
|
if (assessment.kind === "confirmed") {
|
|
4090
4887
|
confirmed = true;
|
|
@@ -4157,6 +4954,49 @@ var SmtVerifier = class _SmtVerifier {
|
|
|
4157
4954
|
}
|
|
4158
4955
|
}
|
|
4159
4956
|
}
|
|
4957
|
+
/**
|
|
4958
|
+
* Runs the linear state-equation bound query (VER-015) and re-checks its answer in
|
|
4959
|
+
* exact integer arithmetic. Returns the bound as the report prints it when one
|
|
4960
|
+
* separates the violation, `null` otherwise (no bound, solver inconclusive, or a
|
|
4961
|
+
* model that failed the re-check — each named in the report). Never the last word:
|
|
4962
|
+
* `null` hands over to the fixpoint query.
|
|
4963
|
+
*/
|
|
4964
|
+
async linearBoundProof(flatNet, solver, report) {
|
|
4965
|
+
const script2 = encodeLinearBound(flatNet, this._initialMarking, this._property);
|
|
4966
|
+
if (script2 == null) return null;
|
|
4967
|
+
let reply;
|
|
4968
|
+
try {
|
|
4969
|
+
reply = await runZ3Text(solver, script2, "bound", this._timeoutMs, []);
|
|
4970
|
+
} catch (e) {
|
|
4971
|
+
rethrowIfProgrammingError(e);
|
|
4972
|
+
report.push(` Linear state-equation bound: inconclusive (${String(e?.message ?? e)})`);
|
|
4973
|
+
return null;
|
|
4974
|
+
}
|
|
4975
|
+
const stdout = reply.stdout.trim();
|
|
4976
|
+
switch (classifyFirstLine(stdout)) {
|
|
4977
|
+
case "sat": {
|
|
4978
|
+
const y = decodeLinearBound(stdout, flatNet.places.length);
|
|
4979
|
+
const bound = y == null ? null : checkLinearBoundExact(flatNet, this._initialMarking, this._property, y);
|
|
4980
|
+
if (bound == null) {
|
|
4981
|
+
report.push(" Linear state-equation bound: inconclusive (solver model failed the exact re-check)");
|
|
4982
|
+
return null;
|
|
4983
|
+
}
|
|
4984
|
+
const rendered = `${formatLinearBound(flatNet, bound)}; violation needs ${formatLinearDemand(flatNet, this._property, bound)}`;
|
|
4985
|
+
report.push(` Linear state-equation bound: ${rendered}`);
|
|
4986
|
+
report.push(" Status: bound excludes every violating marking (re-checked in exact integer arithmetic)");
|
|
4987
|
+
return rendered;
|
|
4988
|
+
}
|
|
4989
|
+
case "unsat":
|
|
4990
|
+
report.push(" Linear state-equation bound: none separates the violation");
|
|
4991
|
+
return null;
|
|
4992
|
+
case "unknown":
|
|
4993
|
+
report.push(" Linear state-equation bound: inconclusive (Z3 answered unknown)");
|
|
4994
|
+
return null;
|
|
4995
|
+
default:
|
|
4996
|
+
report.push(` Linear state-equation bound: inconclusive (${failureReason(reply, timeoutBudget(this._timeoutMs))})`);
|
|
4997
|
+
return null;
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
4160
5000
|
/**
|
|
4161
5001
|
* ν-net soundness guard (NU-040, NU-050). Applied only when the net contains
|
|
4162
5002
|
* match (ν-join) transitions, and only to a proven/violated verdict (an
|
|
@@ -4206,11 +5046,12 @@ function isReachabilitySafety(property) {
|
|
|
4206
5046
|
case "unreachable":
|
|
4207
5047
|
return true;
|
|
4208
5048
|
case "deadlock-free":
|
|
5049
|
+
case "terminates-at-sink":
|
|
4209
5050
|
case "joined-or-dead-lettered":
|
|
4210
5051
|
return false;
|
|
4211
5052
|
}
|
|
4212
5053
|
}
|
|
4213
|
-
function assessCounterexample(flatNet, initialMarking, decodedStates, property, sinkPlaces) {
|
|
5054
|
+
function assessCounterexample(flatNet, initialMarking, decodedStates, property, sinkPlaces, conditionalSinks = []) {
|
|
4214
5055
|
if (decodedStates.size === 0) {
|
|
4215
5056
|
return {
|
|
4216
5057
|
kind: "unconfirmed",
|
|
@@ -4224,9 +5065,12 @@ function assessCounterexample(flatNet, initialMarking, decodedStates, property,
|
|
|
4224
5065
|
vectorize(initialMarking, flatNet),
|
|
4225
5066
|
[...decodedStates].map((m) => vectorize(m, flatNet)),
|
|
4226
5067
|
property,
|
|
4227
|
-
sinkPlaces
|
|
5068
|
+
sinkPlaces,
|
|
5069
|
+
{},
|
|
5070
|
+
conditionalSinks
|
|
4228
5071
|
);
|
|
4229
5072
|
} catch (e) {
|
|
5073
|
+
rethrowIfProgrammingError(e);
|
|
4230
5074
|
outcome = { kind: "exhausted", reason: `replay threw: ${e?.message ?? e}`, nodesExplored: 0 };
|
|
4231
5075
|
}
|
|
4232
5076
|
switch (outcome.kind) {
|
|
@@ -4277,11 +5121,47 @@ Downgraded to UNKNOWN: ${reason}
|
|
|
4277
5121
|
function truncate(s, max) {
|
|
4278
5122
|
return s.length <= max ? s : `${s.slice(0, max)}\u2026 (${s.length - max} chars truncated)`;
|
|
4279
5123
|
}
|
|
5124
|
+
function unresolvedPropertyPlaceInNet(net, property) {
|
|
5125
|
+
const declared = /* @__PURE__ */ new Set();
|
|
5126
|
+
for (const p of net.places) declared.add(p.name);
|
|
5127
|
+
for (const place of propertyPlaces(property)) {
|
|
5128
|
+
if (!declared.has(place.name)) return place.name;
|
|
5129
|
+
}
|
|
5130
|
+
return null;
|
|
5131
|
+
}
|
|
5132
|
+
function commonerApplies(flatNet) {
|
|
5133
|
+
for (const ft of flatNet.transitions) {
|
|
5134
|
+
if (ft.readPlaces.length > 0 || ft.inhibitorPlaces.length > 0 || ft.resetPlaces.length > 0) return false;
|
|
5135
|
+
if (ft.consumeAll.some(Boolean)) return false;
|
|
5136
|
+
if (ft.preVector.some((w) => w > 1)) return false;
|
|
5137
|
+
}
|
|
5138
|
+
return true;
|
|
5139
|
+
}
|
|
5140
|
+
function propertyPlaces(property) {
|
|
5141
|
+
switch (property.type) {
|
|
5142
|
+
case "deadlock-free":
|
|
5143
|
+
return [];
|
|
5144
|
+
case "terminates-at-sink":
|
|
5145
|
+
return [];
|
|
5146
|
+
case "mutual-exclusion":
|
|
5147
|
+
return [property.p1, property.p2];
|
|
5148
|
+
case "place-bound":
|
|
5149
|
+
return [property.place];
|
|
5150
|
+
case "branch-place-bound":
|
|
5151
|
+
return [property.place];
|
|
5152
|
+
case "unreachable":
|
|
5153
|
+
return [...property.places];
|
|
5154
|
+
case "joined-or-dead-lettered":
|
|
5155
|
+
return [property.pending];
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
4280
5158
|
function unresolvedPropertyPlace(flatNet, property) {
|
|
4281
5159
|
const named = (() => {
|
|
4282
5160
|
switch (property.type) {
|
|
4283
5161
|
case "deadlock-free":
|
|
4284
5162
|
return [];
|
|
5163
|
+
case "terminates-at-sink":
|
|
5164
|
+
return [];
|
|
4285
5165
|
case "mutual-exclusion":
|
|
4286
5166
|
return [property.p1, property.p2];
|
|
4287
5167
|
case "place-bound":
|
|
@@ -4310,8 +5190,8 @@ function formatInvariant(inv, flatNet) {
|
|
|
4310
5190
|
}
|
|
4311
5191
|
return `${parts.length === 0 ? "0" : parts.join(" + ")} = ${inv.constant}`;
|
|
4312
5192
|
}
|
|
4313
|
-
function buildResult(verdict, report, invariants, discoveredInvariants, trace, transitions, elapsedMs, statistics, counterexampleConfirmed = null) {
|
|
4314
|
-
return { verdict, report, invariants, discoveredInvariants, counterexampleTrace: trace, counterexampleTransitions: transitions, counterexampleConfirmed, elapsedMs, statistics };
|
|
5193
|
+
function buildResult(verdict, report, invariants, discoveredInvariants, trace, transitions, elapsedMs, statistics, counterexampleConfirmed = null, route = "smt") {
|
|
5194
|
+
return { verdict, route, report, invariants, discoveredInvariants, counterexampleTrace: trace, counterexampleTransitions: transitions, counterexampleConfirmed, elapsedMs, statistics };
|
|
4315
5195
|
}
|
|
4316
5196
|
|
|
4317
5197
|
// src/verification/smt-verification-result.ts
|
|
@@ -4347,15 +5227,20 @@ export {
|
|
|
4347
5227
|
transformAsync,
|
|
4348
5228
|
produce,
|
|
4349
5229
|
withTimeout,
|
|
5230
|
+
rethrowIfProgrammingError,
|
|
4350
5231
|
MarkingState,
|
|
4351
5232
|
MarkingStateBuilder,
|
|
4352
5233
|
deadlockFree,
|
|
5234
|
+
terminatesAtSink,
|
|
4353
5235
|
mutualExclusion,
|
|
4354
5236
|
placeBound,
|
|
4355
5237
|
unreachable,
|
|
4356
5238
|
branchPlaceBound,
|
|
4357
5239
|
joinedOrDeadLettered,
|
|
4358
5240
|
propertyDescription,
|
|
5241
|
+
strandingExcuses,
|
|
5242
|
+
strandsToken,
|
|
5243
|
+
describeSinks,
|
|
4359
5244
|
flatTransition,
|
|
4360
5245
|
alwaysAvailable,
|
|
4361
5246
|
bounded,
|
|
@@ -4385,13 +5270,24 @@ export {
|
|
|
4385
5270
|
runZ3Text,
|
|
4386
5271
|
runZ3Spacer,
|
|
4387
5272
|
encode,
|
|
5273
|
+
encodeNet,
|
|
4388
5274
|
encodeStepRelationSmt2,
|
|
4389
5275
|
checkCertificate,
|
|
4390
5276
|
vcScript,
|
|
5277
|
+
violationDemand,
|
|
5278
|
+
encodeLinearBound,
|
|
5279
|
+
decodeLinearBound,
|
|
5280
|
+
checkLinearBoundExact,
|
|
5281
|
+
formatLinearBound,
|
|
5282
|
+
formatLinearDemand,
|
|
5283
|
+
decideOverClasses,
|
|
4391
5284
|
DBM,
|
|
4392
5285
|
StateClass,
|
|
4393
5286
|
requireOutputProducingActions,
|
|
4394
5287
|
StateClassGraph,
|
|
5288
|
+
isUntimed,
|
|
5289
|
+
NOTE_ENUMERATED,
|
|
5290
|
+
verifyViaStateClassGraph,
|
|
4395
5291
|
decode,
|
|
4396
5292
|
decodeStateSet,
|
|
4397
5293
|
flatNetPlaceCount,
|
|
@@ -4403,4 +5299,4 @@ export {
|
|
|
4403
5299
|
isProven,
|
|
4404
5300
|
isViolated
|
|
4405
5301
|
};
|
|
4406
|
-
//# sourceMappingURL=chunk-
|
|
5302
|
+
//# sourceMappingURL=chunk-EL4E6LVO.js.map
|