libpetri 5.1.0 → 6.0.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-BPGE7GZR.js → chunk-5LE2M5PW.js} +2 -2
- package/dist/{chunk-SXK2Z45Z.js → chunk-H2KAMPGN.js} +1 -1
- package/dist/chunk-H2KAMPGN.js.map +1 -0
- package/dist/{chunk-EL4E6LVO.js → chunk-MQZ6IM63.js} +3792 -711
- package/dist/chunk-MQZ6IM63.js.map +1 -0
- package/dist/debug/index.d.ts +2 -2
- package/dist/debug/index.js +1 -1
- package/dist/doclet/index.d.ts +1 -1
- package/dist/doclet/resources/petrinet-diagrams.js +1 -1
- package/dist/{event-store-CUyHiIH7.d.ts → event-store-D6i4u41W.d.ts} +7 -1
- package/dist/export/index.d.ts +1 -1
- package/dist/index.d.ts +43 -10
- package/dist/index.js +301 -2163
- package/dist/index.js.map +1 -1
- package/dist/{petri-net-6G5r1Wwb.d.ts → petri-net-34SkD5RT.d.ts} +27 -2
- package/dist/render-dom/index.js +1 -1
- package/dist/verification/index.d.ts +411 -5
- package/dist/verification/index.js +725 -1
- package/dist/verification/index.js.map +1 -1
- package/dist/viewer/index.js +1 -1
- package/dist/viewer/viewer.iife.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-EL4E6LVO.js.map +0 -1
- package/dist/chunk-SXK2Z45Z.js.map +0 -1
- /package/dist/{chunk-BPGE7GZR.js.map → chunk-5LE2M5PW.js.map} +0 -0
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
MarkingState,
|
|
7
7
|
MarkingStateBuilder,
|
|
8
8
|
NOTE_ENUMERATED,
|
|
9
|
+
PetriNet,
|
|
9
10
|
SmtVerifier,
|
|
10
11
|
StateClass,
|
|
11
12
|
StateClassGraph,
|
|
13
|
+
Transition,
|
|
12
14
|
Z3ProcessError,
|
|
13
15
|
Z3Unavailable,
|
|
14
16
|
Z3_ENV,
|
|
@@ -18,8 +20,12 @@ import {
|
|
|
18
20
|
canonicalInvariantOrder,
|
|
19
21
|
checkCertificate,
|
|
20
22
|
checkLinearBoundExact,
|
|
23
|
+
compareCodePoints,
|
|
21
24
|
computePInvariants,
|
|
22
25
|
computePSemiflows,
|
|
26
|
+
countAcross,
|
|
27
|
+
countPhrase,
|
|
28
|
+
countViolation,
|
|
23
29
|
deadlockFree,
|
|
24
30
|
decideOverClasses,
|
|
25
31
|
decode,
|
|
@@ -31,6 +37,8 @@ import {
|
|
|
31
37
|
encodeNet,
|
|
32
38
|
encodeStepRelationSmt2,
|
|
33
39
|
enumerateBranches,
|
|
40
|
+
failureReason,
|
|
41
|
+
findFiringBound,
|
|
34
42
|
findMaximalTrapIn,
|
|
35
43
|
findMinimalSiphons,
|
|
36
44
|
flatNetIndexOf,
|
|
@@ -38,39 +46,50 @@ import {
|
|
|
38
46
|
flatNetTransitionCount,
|
|
39
47
|
flatTransition,
|
|
40
48
|
flatten,
|
|
49
|
+
fork,
|
|
41
50
|
formatLinearBound,
|
|
42
51
|
formatLinearDemand,
|
|
52
|
+
formatRanking,
|
|
43
53
|
formatZ3Version,
|
|
44
54
|
ignore,
|
|
45
55
|
isCoveredByInvariants,
|
|
56
|
+
isPassthrough,
|
|
46
57
|
isProven,
|
|
47
58
|
isUntimed,
|
|
48
59
|
isViolated,
|
|
49
60
|
joinedOrDeadLettered,
|
|
50
61
|
mutualExclusion,
|
|
62
|
+
one,
|
|
63
|
+
outPlace,
|
|
51
64
|
pInvariant,
|
|
52
65
|
pInvariantToString,
|
|
53
66
|
parseZ3Version,
|
|
67
|
+
place,
|
|
54
68
|
placeBound,
|
|
55
69
|
placeholderCertificate,
|
|
56
70
|
propertyDescription,
|
|
71
|
+
quiescentCount,
|
|
57
72
|
replayCounterexample,
|
|
58
73
|
resolveZ3,
|
|
59
74
|
rethrowIfProgrammingError,
|
|
60
75
|
runZ3Spacer,
|
|
61
76
|
runZ3Text,
|
|
77
|
+
strandedPlaces,
|
|
62
78
|
strandingExcuses,
|
|
63
79
|
strandsToken,
|
|
64
80
|
strengthenWithSemiflows,
|
|
65
81
|
structuralCheck,
|
|
66
82
|
terminatesAtSink,
|
|
83
|
+
timeoutBudget,
|
|
84
|
+
tokensAcross,
|
|
85
|
+
transform,
|
|
67
86
|
unreachable,
|
|
68
87
|
vcScript,
|
|
69
88
|
verifyViaStateClassGraph,
|
|
70
89
|
violationDemand,
|
|
71
90
|
z3Available,
|
|
72
91
|
z3SolverAt
|
|
73
|
-
} from "../chunk-
|
|
92
|
+
} from "../chunk-MQZ6IM63.js";
|
|
74
93
|
import "../chunk-ATT7U5H5.js";
|
|
75
94
|
|
|
76
95
|
// src/verification/analysis/scc-analyzer.ts
|
|
@@ -420,6 +439,705 @@ var TimePetriNetAnalyzerBuilder = class {
|
|
|
420
439
|
);
|
|
421
440
|
}
|
|
422
441
|
};
|
|
442
|
+
|
|
443
|
+
// src/verification/open-net/contract.ts
|
|
444
|
+
var CONTRACT_KEY = /* @__PURE__ */ Symbol("OpenNetContract.internal");
|
|
445
|
+
var OpenNetContract = class {
|
|
446
|
+
/** Tokens the subnet holds before anything arrives: its own resources and any shared pool it borrows from. */
|
|
447
|
+
initialMarking;
|
|
448
|
+
arrivals;
|
|
449
|
+
clauses;
|
|
450
|
+
/** Places that may hold any number of tokens at quiescence. */
|
|
451
|
+
rest;
|
|
452
|
+
terminals;
|
|
453
|
+
/** Transitions the environment fires: neighbours that react to what the subnet sends. */
|
|
454
|
+
environment;
|
|
455
|
+
/** Whether every run must come to rest. */
|
|
456
|
+
requiresTermination;
|
|
457
|
+
/** @internal Use {@link OpenNetContract.builder}. */
|
|
458
|
+
constructor(key, initialMarking, arrivals, clauses, rest, terminals, environment, requiresTermination) {
|
|
459
|
+
if (key !== CONTRACT_KEY) throw new Error("Use OpenNetContract.builder() to create instances");
|
|
460
|
+
this.initialMarking = initialMarking;
|
|
461
|
+
this.arrivals = arrivals;
|
|
462
|
+
this.clauses = clauses;
|
|
463
|
+
this.rest = rest;
|
|
464
|
+
this.terminals = terminals;
|
|
465
|
+
this.environment = environment;
|
|
466
|
+
this.requiresTermination = requiresTermination;
|
|
467
|
+
}
|
|
468
|
+
static builder() {
|
|
469
|
+
return new OpenNetContractBuilder();
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Every place the initial marking, an arrival group, a clause, the rest set or a terminal
|
|
473
|
+
* marker names, then every place an environment transition touches, in first-mention
|
|
474
|
+
* order: the places a port trace reports. A terminal's excused places are not included;
|
|
475
|
+
* `closeOpenNet` adds them to the closed net itself.
|
|
476
|
+
*/
|
|
477
|
+
places() {
|
|
478
|
+
const seen = /* @__PURE__ */ new Map();
|
|
479
|
+
const add = (p) => {
|
|
480
|
+
if (!seen.has(p.name)) seen.set(p.name, p);
|
|
481
|
+
};
|
|
482
|
+
for (const p of this.initialMarking.placesWithTokens()) add(p);
|
|
483
|
+
for (const g of this.arrivals) g.places.forEach(add);
|
|
484
|
+
for (const c of this.clauses) c.places.forEach(add);
|
|
485
|
+
this.rest.forEach(add);
|
|
486
|
+
for (const t of this.terminals) add(t.marker);
|
|
487
|
+
for (const t of this.environment) transitionPlaces(t).forEach(add);
|
|
488
|
+
return [...seen.values()];
|
|
489
|
+
}
|
|
490
|
+
/** The contract as the report prints it, one line per part. */
|
|
491
|
+
describe() {
|
|
492
|
+
const names = (ps) => ps.map((p) => p.name).join(", ");
|
|
493
|
+
const lines = [` Initial marking: ${this.initialMarking.toString()}`];
|
|
494
|
+
lines.push(this.arrivals.length === 0 ? " Arrivals: none" : ` Arrivals: ${this.arrivals.map((g) => `${countPhrase(g.min, g.max)} onto {${names(g.places)}}`).join("; ")}`);
|
|
495
|
+
lines.push(this.clauses.length === 0 ? " At quiescence: no count clauses" : ` At quiescence: ${this.clauses.map((c) => `${c.name} = ${countPhrase(c.min, c.max)} across {${names(c.places)}}`).join("; ")}`);
|
|
496
|
+
if (this.rest.length > 0) lines.push(` Rest: ${names(this.rest)}`);
|
|
497
|
+
for (const t of this.terminals) {
|
|
498
|
+
lines.push(t.excused.length === 0 ? ` Terminal: when ${t.marker.name}` : ` Terminal: when ${t.marker.name}: ${names(t.excused)}`);
|
|
499
|
+
}
|
|
500
|
+
if (this.environment.length > 0) {
|
|
501
|
+
lines.push(` Environment transitions: ${this.environment.map((t) => t.name).join(", ")}`);
|
|
502
|
+
}
|
|
503
|
+
lines.push(` Termination: ${this.requiresTermination ? "every run comes to rest" : "not required"}`);
|
|
504
|
+
return lines;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
function transitionPlaces(t) {
|
|
508
|
+
return [
|
|
509
|
+
...t.inputSpecs.map((s) => s.place),
|
|
510
|
+
...t.reads.map((a) => a.place),
|
|
511
|
+
...t.inhibitors.map((a) => a.place),
|
|
512
|
+
...t.resets.map((a) => a.place),
|
|
513
|
+
...t.outputPlaces()
|
|
514
|
+
];
|
|
515
|
+
}
|
|
516
|
+
var OpenNetContractBuilder = class {
|
|
517
|
+
_initialMarking = MarkingState.empty();
|
|
518
|
+
_arrivals = [];
|
|
519
|
+
_clauses = [];
|
|
520
|
+
_rest = /* @__PURE__ */ new Map();
|
|
521
|
+
_terminals = [];
|
|
522
|
+
_environment = [];
|
|
523
|
+
_requiresTermination = true;
|
|
524
|
+
initialMarking(arg) {
|
|
525
|
+
if (arg instanceof MarkingState) {
|
|
526
|
+
this._initialMarking = arg;
|
|
527
|
+
} else {
|
|
528
|
+
const builder = MarkingState.builder();
|
|
529
|
+
arg(builder);
|
|
530
|
+
this._initialMarking = builder.build();
|
|
531
|
+
}
|
|
532
|
+
return this;
|
|
533
|
+
}
|
|
534
|
+
/** The environment delivers exactly `count` tokens, each onto one of `places`, at any point of the run. */
|
|
535
|
+
arrive(count, ...places) {
|
|
536
|
+
return this.arriveBetween(count, count, ...places);
|
|
537
|
+
}
|
|
538
|
+
/** The environment delivers at most `max` tokens, possibly none. `arriveAtMost(1, halt)` is "never or once". */
|
|
539
|
+
arriveAtMost(max, ...places) {
|
|
540
|
+
return this.arriveBetween(0, max, ...places);
|
|
541
|
+
}
|
|
542
|
+
/** The environment delivers between `min` and `max` tokens in total, each onto one of `places`, at any point of the run. */
|
|
543
|
+
arriveBetween(min, max, ...places) {
|
|
544
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 0 || max < min || max < 1) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`OpenNetContract: an arrival group needs whole bounds with 0 <= min <= max and max >= 1, got ${min}..${max}. A bound is both the runtime cap and the width of the claim, so it is finite.`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
this._arrivals.push({ places: distinct(places, "an arrival group"), min, max });
|
|
550
|
+
return this;
|
|
551
|
+
}
|
|
552
|
+
/** At every quiescent marking, exactly `count` tokens across `places`. */
|
|
553
|
+
expect(name, count, ...places) {
|
|
554
|
+
return this.expectBetween(name, count, count, ...places);
|
|
555
|
+
}
|
|
556
|
+
/** At every quiescent marking, between `min` and `max` tokens across `places`; `max` may be `Infinity`. */
|
|
557
|
+
expectBetween(name, min, max, ...places) {
|
|
558
|
+
if (name.length === 0) throw new Error("OpenNetContract: a clause needs a name");
|
|
559
|
+
if (this._clauses.some((c) => c.name === name)) {
|
|
560
|
+
throw new Error(`OpenNetContract: duplicate clause name '${name}'`);
|
|
561
|
+
}
|
|
562
|
+
if (!Number.isInteger(min) || min < 0 || !(max === Infinity || Number.isInteger(max)) || max < min) {
|
|
563
|
+
throw new Error(`OpenNetContract: clause '${name}' needs whole bounds with 0 <= min <= max, got ${min}..${max}`);
|
|
564
|
+
}
|
|
565
|
+
this._clauses.push({ name, places: distinct(places, `clause '${name}'`), min, max });
|
|
566
|
+
return this;
|
|
567
|
+
}
|
|
568
|
+
/** Places that may hold any number of tokens at quiescence. */
|
|
569
|
+
rest(...places) {
|
|
570
|
+
for (const p of places) if (!this._rest.has(p.name)) this._rest.set(p.name, p);
|
|
571
|
+
return this;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* A designed terminal: while `marker` holds a token, lower bounds are waived and tokens may
|
|
575
|
+
* rest on `excused`. Repeated calls for one marker accumulate.
|
|
576
|
+
*/
|
|
577
|
+
terminal(marker, ...excused) {
|
|
578
|
+
let entry = this._terminals.find((t) => t.marker.name === marker.name);
|
|
579
|
+
if (entry == null) {
|
|
580
|
+
entry = { marker, excused: /* @__PURE__ */ new Map() };
|
|
581
|
+
this._terminals.push(entry);
|
|
582
|
+
}
|
|
583
|
+
for (const p of excused) if (!entry.excused.has(p.name)) entry.excused.set(p.name, p);
|
|
584
|
+
return this;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Transitions the environment fires: a neighbour that reacts to what the subnet sends, such
|
|
588
|
+
* as a tool answering a request. An arrival group cannot say that: its tokens do not wait
|
|
589
|
+
* for a request.
|
|
590
|
+
*
|
|
591
|
+
* They join the closed net unchanged and are marked as environment steps in the port trace.
|
|
592
|
+
* A place only they touch belongs to the environment and may hold tokens at quiescence; a
|
|
593
|
+
* place they share with the subnet is a port, judged like any other. Their actions never
|
|
594
|
+
* run, so one that declares outputs may keep `passthrough()`.
|
|
595
|
+
*/
|
|
596
|
+
environment(...transitions) {
|
|
597
|
+
for (const t of transitions) {
|
|
598
|
+
if (this._environment.some((e) => e.name === t.name)) {
|
|
599
|
+
throw new Error(`OpenNetContract: duplicate environment transition '${t.name}'`);
|
|
600
|
+
}
|
|
601
|
+
this._environment.push(t);
|
|
602
|
+
}
|
|
603
|
+
return this;
|
|
604
|
+
}
|
|
605
|
+
/** Whether every run must come to rest (default `true`). */
|
|
606
|
+
requireTermination(required) {
|
|
607
|
+
this._requiresTermination = required;
|
|
608
|
+
return this;
|
|
609
|
+
}
|
|
610
|
+
build() {
|
|
611
|
+
return new OpenNetContract(
|
|
612
|
+
CONTRACT_KEY,
|
|
613
|
+
this._initialMarking,
|
|
614
|
+
[...this._arrivals],
|
|
615
|
+
[...this._clauses],
|
|
616
|
+
[...this._rest.values()],
|
|
617
|
+
this._terminals.map((t) => ({ marker: t.marker, excused: [...t.excused.values()] })),
|
|
618
|
+
[...this._environment],
|
|
619
|
+
this._requiresTermination
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
function distinct(places, what) {
|
|
624
|
+
if (places.length === 0) throw new Error(`OpenNetContract: ${what} names no place`);
|
|
625
|
+
const byName = /* @__PURE__ */ new Map();
|
|
626
|
+
for (const p of places) if (!byName.has(p.name)) byName.set(p.name, p);
|
|
627
|
+
return [...byName.values()];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/verification/open-net/closure.ts
|
|
631
|
+
var ENVIRONMENT_ACTION = transform(() => null);
|
|
632
|
+
function closeOpenNet(net, contract) {
|
|
633
|
+
const byName = /* @__PURE__ */ new Map();
|
|
634
|
+
for (const p of net.places) if (!byName.has(p.name)) byName.set(p.name, p);
|
|
635
|
+
const taken = new Set(byName.keys());
|
|
636
|
+
for (const t of net.transitions) taken.add(t.name);
|
|
637
|
+
const fresh = (name) => {
|
|
638
|
+
if (taken.has(name)) {
|
|
639
|
+
throw new Error(`VER-022: closing the net would add '${name}', which the net already declares`);
|
|
640
|
+
}
|
|
641
|
+
taken.add(name);
|
|
642
|
+
return name;
|
|
643
|
+
};
|
|
644
|
+
const environment = /* @__PURE__ */ new Map();
|
|
645
|
+
const environmentPlaces = /* @__PURE__ */ new Map();
|
|
646
|
+
for (const t of contract.environment) {
|
|
647
|
+
environment.set(fresh(t.name), { kind: "transition" });
|
|
648
|
+
for (const p of transitionPlaces(t)) {
|
|
649
|
+
if (!byName.has(p.name) && !environmentPlaces.has(p.name)) environmentPlaces.set(p.name, p);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
for (const [name, p] of environmentPlaces) {
|
|
653
|
+
taken.add(name);
|
|
654
|
+
byName.set(name, p);
|
|
655
|
+
}
|
|
656
|
+
const undeclared = [];
|
|
657
|
+
const extra = [];
|
|
658
|
+
const named = [...contract.places(), ...contract.terminals.flatMap((t) => t.excused)];
|
|
659
|
+
for (const p of named) {
|
|
660
|
+
if (byName.has(p.name)) continue;
|
|
661
|
+
undeclared.push(p.name);
|
|
662
|
+
byName.set(p.name, p);
|
|
663
|
+
extra.push(p);
|
|
664
|
+
}
|
|
665
|
+
const canonical = (p) => byName.get(p.name) ?? p;
|
|
666
|
+
const envTransitions = [];
|
|
667
|
+
const marking = MarkingState.builder().copyFrom(contract.initialMarking);
|
|
668
|
+
const inject = (group, source, target, optional) => {
|
|
669
|
+
const name = fresh(`env:arrive${optional ? "?" : ""}[${group}]:${target.name}`);
|
|
670
|
+
envTransitions.push(
|
|
671
|
+
Transition.builder(name).inputs(one(source)).outputs(outPlace(target)).action(fork()).build()
|
|
672
|
+
);
|
|
673
|
+
environment.set(name, { kind: "arrival", group, place: target.name });
|
|
674
|
+
};
|
|
675
|
+
contract.arrivals.forEach((group, i) => {
|
|
676
|
+
if (group.min > 0) {
|
|
677
|
+
const source = place(fresh(`env:arrivals[${i}]`));
|
|
678
|
+
marking.tokens(source, group.min);
|
|
679
|
+
for (const p of group.places) inject(i, source, canonical(p), false);
|
|
680
|
+
}
|
|
681
|
+
if (group.max > group.min) {
|
|
682
|
+
const source = place(fresh(`env:optional[${i}]`));
|
|
683
|
+
marking.tokens(source, group.max - group.min);
|
|
684
|
+
for (const p of group.places) inject(i, source, canonical(p), true);
|
|
685
|
+
const name = fresh(`env:decline[${i}]`);
|
|
686
|
+
envTransitions.push(Transition.builder(name).inputs(one(source)).build());
|
|
687
|
+
environment.set(name, { kind: "decline", group: i });
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
const placeholder = new Set(contract.environment.filter((t) => t.outputSpec !== null && isPassthrough(t.action)).map((t) => t.name));
|
|
691
|
+
const closed = PetriNet.builder(`${net.name}+environment`).places(...net.places, ...extra).transitions(...net.transitions, ...contract.environment, ...envTransitions).build().bindActionsWithResolver((name) => placeholder.has(name) ? ENVIRONMENT_ACTION : null);
|
|
692
|
+
return {
|
|
693
|
+
net: closed,
|
|
694
|
+
initialMarking: marking.build(),
|
|
695
|
+
environment,
|
|
696
|
+
environmentPlaces: [...environmentPlaces.values()],
|
|
697
|
+
undeclared
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/verification/open-net/predicate.ts
|
|
702
|
+
function restDeclarationOf(contract, closed) {
|
|
703
|
+
const sinks = /* @__PURE__ */ new Map();
|
|
704
|
+
for (const c of contract.clauses) for (const p of c.places) if (!sinks.has(p.name)) sinks.set(p.name, p);
|
|
705
|
+
for (const p of contract.rest) if (!sinks.has(p.name)) sinks.set(p.name, p);
|
|
706
|
+
for (const p of closed.environmentPlaces) if (!sinks.has(p.name)) sinks.set(p.name, p);
|
|
707
|
+
return {
|
|
708
|
+
sinks: new Set(sinks.values()),
|
|
709
|
+
conditional: contract.terminals.map((t) => ({ marker: t.marker, places: new Set(t.excused) }))
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function waiverMarkers(contract) {
|
|
713
|
+
return contract.terminals.map((t) => t.marker);
|
|
714
|
+
}
|
|
715
|
+
function strandedNames(m, rest) {
|
|
716
|
+
return strandedPlaces(m, rest.sinks, rest.conditional).sort((a, b) => compareCodePoints(a.name, b.name));
|
|
717
|
+
}
|
|
718
|
+
function quiescenceFindings(m, contract, rest) {
|
|
719
|
+
const findings = [];
|
|
720
|
+
const markers = waiverMarkers(contract);
|
|
721
|
+
for (const clause of contract.clauses) {
|
|
722
|
+
const bound = countViolation(m, clause.places, clause.min, clause.max, markers);
|
|
723
|
+
if (bound !== null) findings.push({ kind: "clause", clause, count: tokensAcross(m, clause.places), bound });
|
|
724
|
+
}
|
|
725
|
+
for (const place2 of strandedNames(m, rest)) {
|
|
726
|
+
findings.push({ kind: "stranded", place: place2.name, count: m.tokens(place2) });
|
|
727
|
+
}
|
|
728
|
+
return findings;
|
|
729
|
+
}
|
|
730
|
+
function subjectOf(f) {
|
|
731
|
+
return f.kind === "clause" ? f.clause.name : f.place;
|
|
732
|
+
}
|
|
733
|
+
function describeFinding(f) {
|
|
734
|
+
if (f.kind === "stranded") {
|
|
735
|
+
return `${f.place} holds ${f.count} at quiescence, and nothing in the contract lets a token rest there`;
|
|
736
|
+
}
|
|
737
|
+
const { clause } = f;
|
|
738
|
+
const expected = countAcross(clause.min, clause.max, clause.places);
|
|
739
|
+
return f.bound === "upper" ? `${expected} at quiescence, found ${f.count} (an upper bound holds under a terminal too)` : `${expected} at quiescence, found ${f.count}`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/verification/open-net/result.ts
|
|
743
|
+
function contractViolation(closed, tracedPlaces, violation) {
|
|
744
|
+
const { transitions, markings } = violation;
|
|
745
|
+
const portTrace = [];
|
|
746
|
+
if (markings.length === transitions.length + 1) {
|
|
747
|
+
for (let i = 0; i < transitions.length; i++) {
|
|
748
|
+
const before = markings[i];
|
|
749
|
+
const after = markings[i + 1];
|
|
750
|
+
const changes = [];
|
|
751
|
+
for (const p of tracedPlaces) {
|
|
752
|
+
const delta = after.tokens(p) - before.tokens(p);
|
|
753
|
+
if (delta !== 0) changes.push({ place: p.name, delta });
|
|
754
|
+
}
|
|
755
|
+
const env = closed.environment.get(transitions[i]);
|
|
756
|
+
if (changes.length > 0 || env !== void 0) {
|
|
757
|
+
portTrace.push({ step: i + 1, transition: transitions[i], environment: env?.kind ?? null, changes });
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return { ...violation, portTrace };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/verification/open-net/graph-route.ts
|
|
765
|
+
function decideOnGraph(closed, contract, maxClasses, tracedPlaces) {
|
|
766
|
+
const graph = StateClassGraph.build(
|
|
767
|
+
closed.net,
|
|
768
|
+
closed.initialMarking,
|
|
769
|
+
maxClasses,
|
|
770
|
+
void 0,
|
|
771
|
+
void 0,
|
|
772
|
+
{ untimed: true }
|
|
773
|
+
);
|
|
774
|
+
const classes = graph.stateClasses();
|
|
775
|
+
const rest = restDeclarationOf(contract, closed);
|
|
776
|
+
const tree = bfsTree(graph);
|
|
777
|
+
const first = /* @__PURE__ */ new Map();
|
|
778
|
+
for (const sc of classes) {
|
|
779
|
+
if (sc.enabledTransitions.length > 0) continue;
|
|
780
|
+
for (const finding of quiescenceFindings(sc.marking, contract, rest)) {
|
|
781
|
+
const key = `${finding.kind}:${subjectOf(finding)}`;
|
|
782
|
+
if (!first.has(key)) first.set(key, { finding, target: sc });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const clauseOrder = new Map(contract.clauses.map((c, i) => [c.name, i]));
|
|
786
|
+
const rank = (f) => f.kind === "clause" ? clauseOrder.get(f.clause.name) : contract.clauses.length;
|
|
787
|
+
const ordered = [...first.values()].sort((a, b) => rank(a.finding) - rank(b.finding) || compareCodePoints(subjectOf(a.finding), subjectOf(b.finding)));
|
|
788
|
+
const violations = ordered.map(({ finding, target }) => {
|
|
789
|
+
const path = pathTo(tree, target);
|
|
790
|
+
return contractViolation(closed, tracedPlaces, {
|
|
791
|
+
kind: finding.kind,
|
|
792
|
+
subject: subjectOf(finding),
|
|
793
|
+
detail: describeFinding(finding),
|
|
794
|
+
transitions: path.transitions,
|
|
795
|
+
markings: path.classes.map((c) => c.marking),
|
|
796
|
+
cycleStart: null,
|
|
797
|
+
confirmed: true
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
if (contract.requiresTermination) {
|
|
801
|
+
const cycle = findCycle(graph, tree);
|
|
802
|
+
if (cycle !== null) violations.push(contractViolation(closed, tracedPlaces, cycle));
|
|
803
|
+
}
|
|
804
|
+
return { complete: graph.isComplete(), classCount: classes.length, violations };
|
|
805
|
+
}
|
|
806
|
+
function bfsTree(graph) {
|
|
807
|
+
const tree = /* @__PURE__ */ new Map();
|
|
808
|
+
const seen = /* @__PURE__ */ new Set([graph.initialClass]);
|
|
809
|
+
const queue = [graph.initialClass];
|
|
810
|
+
for (let head = 0; head < queue.length; head++) {
|
|
811
|
+
const current = queue[head];
|
|
812
|
+
for (const [transition, edges] of graph.outgoingBranchEdges(current)) {
|
|
813
|
+
for (const edge of edges) {
|
|
814
|
+
if (seen.has(edge.target)) continue;
|
|
815
|
+
seen.add(edge.target);
|
|
816
|
+
tree.set(edge.target, { parent: current, via: transition.name });
|
|
817
|
+
queue.push(edge.target);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return tree;
|
|
822
|
+
}
|
|
823
|
+
function pathTo(tree, target) {
|
|
824
|
+
const classes = [];
|
|
825
|
+
const transitions = [];
|
|
826
|
+
for (let cur = target; cur !== void 0; ) {
|
|
827
|
+
classes.push(cur);
|
|
828
|
+
const edge = tree.get(cur);
|
|
829
|
+
if (edge === void 0) break;
|
|
830
|
+
transitions.push(edge.via);
|
|
831
|
+
cur = edge.parent;
|
|
832
|
+
}
|
|
833
|
+
return { classes: classes.reverse(), transitions: transitions.reverse() };
|
|
834
|
+
}
|
|
835
|
+
function findCycle(graph, tree) {
|
|
836
|
+
const edgesOf = (sc) => {
|
|
837
|
+
const out = [];
|
|
838
|
+
for (const [t, edges] of graph.outgoingBranchEdges(sc)) for (const e of edges) out.push([t.name, e.target]);
|
|
839
|
+
return out;
|
|
840
|
+
};
|
|
841
|
+
const position = /* @__PURE__ */ new Map([[graph.initialClass, 0]]);
|
|
842
|
+
const stack = [{ node: graph.initialClass, edges: edgesOf(graph.initialClass), next: 0, via: null }];
|
|
843
|
+
while (stack.length > 0) {
|
|
844
|
+
const top = stack[stack.length - 1];
|
|
845
|
+
if (top.next >= top.edges.length) {
|
|
846
|
+
position.set(top.node, -1);
|
|
847
|
+
stack.pop();
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
const [via, target] = top.edges[top.next++];
|
|
851
|
+
const at = position.get(target);
|
|
852
|
+
if (at === void 0) {
|
|
853
|
+
position.set(target, stack.length);
|
|
854
|
+
stack.push({ node: target, edges: edgesOf(target), next: 0, via });
|
|
855
|
+
} else if (at >= 0) {
|
|
856
|
+
const loop = stack.slice(at);
|
|
857
|
+
const stem = pathTo(tree, target);
|
|
858
|
+
const cycle = [...loop.slice(1).map((f) => f.via), via];
|
|
859
|
+
return {
|
|
860
|
+
kind: "termination",
|
|
861
|
+
subject: "termination",
|
|
862
|
+
detail: `a run can repeat ${cycle.join(" \u2192 ")} forever without coming to rest`,
|
|
863
|
+
transitions: [...stem.transitions, ...cycle],
|
|
864
|
+
markings: [...stem.classes.map((c) => c.marking), ...loop.slice(1).map((f) => f.node.marking), target.marking],
|
|
865
|
+
cycleStart: stem.transitions.length,
|
|
866
|
+
confirmed: true
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/verification/open-net/report.ts
|
|
874
|
+
function renderReport(input) {
|
|
875
|
+
const { net, closed, contract, graph, smtLines, verdict, violations } = input;
|
|
876
|
+
const lines = ["=== OPEN-NET CONTRACT VERIFICATION (VER-022) ===", ""];
|
|
877
|
+
lines.push(`Net: ${net.name}, closed by ${closed.environment.size} environment transitions over ${contract.arrivals.length} arrival groups`);
|
|
878
|
+
lines.push("Contract:", ...contract.describe());
|
|
879
|
+
if (closed.undeclared.length > 0) {
|
|
880
|
+
lines.push(`Not declared by the net: ${closed.undeclared.join(", ")} (no arc touches them; a clause there counts zero)`);
|
|
881
|
+
}
|
|
882
|
+
lines.push("");
|
|
883
|
+
if (graph === null) {
|
|
884
|
+
lines.push(`State-class graph: skipped (${input.graphSkipped ?? "class budget 0"})`);
|
|
885
|
+
} else {
|
|
886
|
+
lines.push("=== State-class graph (untimed, priority-blind) ===");
|
|
887
|
+
lines.push(graph.complete ? ` Classes: ${graph.classCount}, closed` : ` Classes: ${graph.classCount}, truncated at the class budget of ${input.maxClasses}`);
|
|
888
|
+
}
|
|
889
|
+
if (smtLines !== null) {
|
|
890
|
+
lines.push("", "=== SMT route ===", ...smtLines);
|
|
891
|
+
}
|
|
892
|
+
lines.push("", "=== RESULT ===");
|
|
893
|
+
switch (verdict.type) {
|
|
894
|
+
case "proven":
|
|
895
|
+
lines.push(`PROVEN: every quiescent marking meets the contract${contract.requiresTermination ? " and every run comes to rest" : ""} (${verdict.method})`);
|
|
896
|
+
break;
|
|
897
|
+
case "unknown":
|
|
898
|
+
lines.push(`UNKNOWN: ${verdict.reason}`);
|
|
899
|
+
break;
|
|
900
|
+
case "violated":
|
|
901
|
+
lines.push(`VIOLATED: ${violations.length} ${violations.length === 1 ? "part" : "parts"} of the contract broken`);
|
|
902
|
+
for (const v of violations) lines.push(...renderViolation(v));
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
return lines.join("\n");
|
|
906
|
+
}
|
|
907
|
+
function renderViolation(v) {
|
|
908
|
+
const lines = [` [${v.subject}] ${v.kind}: ${v.detail}`];
|
|
909
|
+
if (!v.confirmed) lines.push(" (the solver's counterexample did not replay as an ordered firing sequence)");
|
|
910
|
+
if (v.portTrace.length > 0) {
|
|
911
|
+
lines.push(" Port trace:");
|
|
912
|
+
for (const s of v.portTrace) {
|
|
913
|
+
if (v.cycleStart !== null && s.step === v.cycleStart + 1) lines.push(" -- the cycle starts here --");
|
|
914
|
+
const env = s.environment === null ? "" : s.environment === "transition" ? " [environment]" : ` [environment ${s.environment}]`;
|
|
915
|
+
const changes = s.changes.map((c) => `${c.place} ${c.delta > 0 ? "+" : ""}${c.delta}`).join(", ");
|
|
916
|
+
lines.push(` ${s.step}. ${s.transition}${env}${changes.length > 0 ? ` ${changes}` : ""}`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (v.cycleStart !== null) {
|
|
920
|
+
const stem = v.transitions.slice(0, v.cycleStart);
|
|
921
|
+
const cycle = `then repeating ${v.transitions.slice(v.cycleStart).join(", ")}`;
|
|
922
|
+
lines.push(` Firing sequence: ${stem.length === 0 ? cycle : `${stem.join(", ")}, ${cycle}`}`);
|
|
923
|
+
} else if (v.transitions.length > 0) {
|
|
924
|
+
lines.push(` Firing sequence: ${v.transitions.join(", ")}`);
|
|
925
|
+
}
|
|
926
|
+
const last = v.markings.at(-1);
|
|
927
|
+
if (last !== void 0) lines.push(` ${v.kind === "termination" ? "Marking on the cycle" : "Quiescent marking"}: ${last.toString()}`);
|
|
928
|
+
return lines;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/verification/open-net/smt-route.ts
|
|
932
|
+
async function decideViaSmt(closed, contract, tracedPlaces, configure, terminationTimeoutMs) {
|
|
933
|
+
const violations = [];
|
|
934
|
+
const undecided = [];
|
|
935
|
+
const lines = [];
|
|
936
|
+
const certificates = [];
|
|
937
|
+
const net = untimed(closed.net);
|
|
938
|
+
for (const part of partsFor(closed, contract)) {
|
|
939
|
+
if (part.kind === "vacuous") {
|
|
940
|
+
lines.push(` [${part.subject}] ${part.detail}: proven (no query needed)`);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const q = part.query;
|
|
944
|
+
let verifier = SmtVerifier.forNet(net).initialMarking(closed.initialMarking).property(q.property).sinkPlaces(...q.sinks).enumerationMaxClasses(0);
|
|
945
|
+
for (const c of q.conditional) verifier = verifier.sinkPlacesWhen(c.marker, ...c.places);
|
|
946
|
+
const result = await configure(verifier).verify();
|
|
947
|
+
const verdict = result.verdict;
|
|
948
|
+
lines.push(` [${q.subject}] ${propertyDescription(q.property)}: ${verdict.type}` + (verdict.type === "unknown" ? ` (${verdict.reason})` : ""));
|
|
949
|
+
if (verdict.type === "unknown") undecided.push(`${q.subject}: ${verdict.reason}`);
|
|
950
|
+
else if (verdict.type === "violated") violations.push(contractViolation(closed, tracedPlaces, q.onViolated(result)));
|
|
951
|
+
else if (verdict.inductiveInvariant !== null) {
|
|
952
|
+
certificates.push({ subject: q.subject, invariant: verdict.inductiveInvariant });
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (contract.requiresTermination) {
|
|
956
|
+
const termination = await terminationByRanking(closed, terminationTimeoutMs);
|
|
957
|
+
if (termination.proven) {
|
|
958
|
+
lines.push(` [termination] Firing bound (VER-019): ${termination.detail}`);
|
|
959
|
+
} else {
|
|
960
|
+
lines.push(` [termination] Firing bound (VER-019): undecided (${termination.reason})`);
|
|
961
|
+
undecided.push(`termination: ${termination.reason}`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return { violations, undecided, lines, certificates };
|
|
965
|
+
}
|
|
966
|
+
function untimed(net) {
|
|
967
|
+
if (isUntimed(net)) return net;
|
|
968
|
+
const transitions = [...net.transitions].map((t) => {
|
|
969
|
+
if (t.timing.type === "immediate") return t;
|
|
970
|
+
const b = Transition.builder(t.name).inputs(...t.inputSpecs).priority(t.priority).action(t.action);
|
|
971
|
+
if (t.outputSpec !== null) b.outputs(t.outputSpec);
|
|
972
|
+
for (const arc of t.inhibitors) b.inhibitor(arc.place);
|
|
973
|
+
for (const arc of t.reads) b.read(arc.place);
|
|
974
|
+
for (const arc of t.resets) b.reset(arc.place);
|
|
975
|
+
if (t.matchSpec !== null) b.match(t.matchSpec);
|
|
976
|
+
return b.build();
|
|
977
|
+
});
|
|
978
|
+
return PetriNet.builder(net.name).places(...net.places).transitions(...transitions).build();
|
|
979
|
+
}
|
|
980
|
+
function partsFor(closed, contract) {
|
|
981
|
+
const rest = restDeclarationOf(contract, closed);
|
|
982
|
+
const markers = waiverMarkers(contract);
|
|
983
|
+
const quiescentMarking = (result) => result.counterexampleConfirmed === true ? result.counterexampleTrace.at(-1) : void 0;
|
|
984
|
+
const witness = (result) => ({
|
|
985
|
+
transitions: [...result.counterexampleTransitions],
|
|
986
|
+
markings: [...result.counterexampleTrace],
|
|
987
|
+
cycleStart: null,
|
|
988
|
+
confirmed: result.counterexampleConfirmed === true
|
|
989
|
+
});
|
|
990
|
+
const stranding = {
|
|
991
|
+
subject: "stranding",
|
|
992
|
+
property: deadlockFree(),
|
|
993
|
+
sinks: [...rest.sinks],
|
|
994
|
+
conditional: rest.conditional,
|
|
995
|
+
onViolated: (result) => {
|
|
996
|
+
const last = quiescentMarking(result);
|
|
997
|
+
const stranded = last === void 0 ? [] : strandedNames(last, rest).map((p) => p.name);
|
|
998
|
+
return {
|
|
999
|
+
kind: "stranded",
|
|
1000
|
+
subject: stranded.length === 0 ? "stranding" : stranded.join(", "),
|
|
1001
|
+
detail: stranded.length === 0 ? "the solver found a reachable quiescent marking that leaves a token where the contract lets none rest" : `${stranded.join(", ")} ${stranded.length === 1 ? "holds" : "hold"} a token at quiescence, and nothing in the contract lets one rest there`,
|
|
1002
|
+
...witness(result)
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
const parts = [{ kind: "query", query: stranding }];
|
|
1007
|
+
for (const clause of contract.clauses) {
|
|
1008
|
+
const across = countAcross(clause.min, clause.max, clause.places);
|
|
1009
|
+
if (clause.min === 0 && clause.max === Infinity) {
|
|
1010
|
+
parts.push({ kind: "vacuous", subject: clause.name, detail: `${across} at quiescence` });
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
const query = {
|
|
1014
|
+
subject: clause.name,
|
|
1015
|
+
property: quiescentCount(clause.places, clause.min, clause.max, markers),
|
|
1016
|
+
sinks: [],
|
|
1017
|
+
conditional: [],
|
|
1018
|
+
onViolated: (result) => {
|
|
1019
|
+
const last = quiescentMarking(result);
|
|
1020
|
+
return {
|
|
1021
|
+
kind: "clause",
|
|
1022
|
+
subject: clause.name,
|
|
1023
|
+
detail: last === void 0 ? `${across} at quiescence: the solver found a quiescent marking outside it` : `${across} at quiescence, found ${tokensAcross(last, clause.places)}`,
|
|
1024
|
+
...witness(result)
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
parts.push({ kind: "query", query });
|
|
1029
|
+
}
|
|
1030
|
+
return parts;
|
|
1031
|
+
}
|
|
1032
|
+
async function terminationByRanking(closed, timeoutMs) {
|
|
1033
|
+
const flat = flatten(closed.net);
|
|
1034
|
+
const initial = flat.places.map((p) => closed.initialMarking.tokens(p));
|
|
1035
|
+
let solver;
|
|
1036
|
+
try {
|
|
1037
|
+
solver = resolveZ3();
|
|
1038
|
+
} catch (e) {
|
|
1039
|
+
if (e instanceof Z3Unavailable) return { proven: false, reason: e.message };
|
|
1040
|
+
throw e;
|
|
1041
|
+
}
|
|
1042
|
+
const ask = async (script) => {
|
|
1043
|
+
try {
|
|
1044
|
+
const reply = await runZ3Text(solver, script, "ranking", timeoutMs, []);
|
|
1045
|
+
const answer = firstLine(reply.stdout);
|
|
1046
|
+
if (answer === "sat" || answer === "unsat" || answer === "unknown") return reply.stdout;
|
|
1047
|
+
return new Error(failureReason(reply, timeoutBudget(timeoutMs)));
|
|
1048
|
+
} catch (e) {
|
|
1049
|
+
rethrowIfProgrammingError(e);
|
|
1050
|
+
return new Error(String(e?.message ?? e));
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
const ranking = await findFiringBound(flat, initial, ask);
|
|
1054
|
+
switch (ranking.kind) {
|
|
1055
|
+
case "bound":
|
|
1056
|
+
return {
|
|
1057
|
+
proven: true,
|
|
1058
|
+
detail: `every run has at most ${ranking.bound.bound} firings (${formatRanking(flat, ranking.bound)} drops on every firing)`
|
|
1059
|
+
};
|
|
1060
|
+
case "unbounded":
|
|
1061
|
+
return {
|
|
1062
|
+
proven: false,
|
|
1063
|
+
reason: ranking.repeatable === null ? "no firing bound: no weights drop on every firing" : `no firing bound: the marking equation lets ${ranking.repeatable.map((t) => flat.transitions[t].name).join(", ")} repeat`
|
|
1064
|
+
};
|
|
1065
|
+
case "rejected":
|
|
1066
|
+
return { proven: false, reason: "the firing-bound ranking failed the exact re-check" };
|
|
1067
|
+
case "unknown":
|
|
1068
|
+
return { proven: false, reason: "the firing-bound query answered unknown" };
|
|
1069
|
+
case "failed":
|
|
1070
|
+
return { proven: false, reason: ranking.reason };
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function firstLine(stdout) {
|
|
1074
|
+
return stdout.split("\n").find((line) => line.trim() !== "")?.trim() ?? "";
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/verification/open-net/verify-open-net.ts
|
|
1078
|
+
var DEFAULT_MAX_CLASSES = 5e4;
|
|
1079
|
+
var METHOD_ENUMERATION = "open-net contract by state-space enumeration (VER-022)";
|
|
1080
|
+
var METHOD_SMT = "open-net contract by the SMT pipeline (VER-022)";
|
|
1081
|
+
var SKIPPED_BY_BUDGET = "class budget 0";
|
|
1082
|
+
var SKIPPED_BY_MATCH = "the closed net declares match (\u03BD-join) transitions, which the graph does not model";
|
|
1083
|
+
async function verifyOpenNet(net, contract, options = {}) {
|
|
1084
|
+
const start = performance.now();
|
|
1085
|
+
const closed = closeOpenNet(net, contract);
|
|
1086
|
+
const maxClasses = options.maxClasses ?? DEFAULT_MAX_CLASSES;
|
|
1087
|
+
const useSmt = options.smt ?? true;
|
|
1088
|
+
const tracedPlaces = contract.places();
|
|
1089
|
+
const graphSkipped = [...closed.net.transitions].some((t) => t.matchSpec !== null) ? SKIPPED_BY_MATCH : maxClasses > 0 ? null : SKIPPED_BY_BUDGET;
|
|
1090
|
+
const graph = graphSkipped === null ? decideOnGraph(closed, contract, maxClasses, tracedPlaces) : null;
|
|
1091
|
+
const result = (verdict, route, violations, smtLines) => ({
|
|
1092
|
+
verdict,
|
|
1093
|
+
violations,
|
|
1094
|
+
route,
|
|
1095
|
+
classCount: graph?.classCount ?? 0,
|
|
1096
|
+
graphComplete: graph?.complete ?? false,
|
|
1097
|
+
report: renderReport({ net, closed, contract, maxClasses, graph, graphSkipped, smtLines, verdict, violations }),
|
|
1098
|
+
closedNet: closed.net,
|
|
1099
|
+
closedMarking: closed.initialMarking,
|
|
1100
|
+
elapsedMs: performance.now() - start
|
|
1101
|
+
});
|
|
1102
|
+
if (graph !== null) {
|
|
1103
|
+
if (graph.violations.length > 0) {
|
|
1104
|
+
return result({ type: "violated" }, "enumeration", graph.violations, null);
|
|
1105
|
+
}
|
|
1106
|
+
if (graph.complete) {
|
|
1107
|
+
return result({ type: "proven", method: METHOD_ENUMERATION, inductiveInvariant: null }, "enumeration", [], null);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const why = graph !== null ? `the state-class graph did not close within ${maxClasses} classes` : graphSkipped === SKIPPED_BY_BUDGET ? "the state-class graph was skipped" : `the state-class graph was skipped: ${graphSkipped}`;
|
|
1111
|
+
if (!useSmt) {
|
|
1112
|
+
return result({ type: "unknown", reason: `${why}, and the SMT route is disabled` }, "enumeration", [], null);
|
|
1113
|
+
}
|
|
1114
|
+
const smt = await decideViaSmt(
|
|
1115
|
+
closed,
|
|
1116
|
+
contract,
|
|
1117
|
+
tracedPlaces,
|
|
1118
|
+
options.configureSmt ?? ((v) => v),
|
|
1119
|
+
options.terminationTimeoutMs ?? 6e4
|
|
1120
|
+
);
|
|
1121
|
+
if (smt.violations.length > 0) return result({ type: "violated" }, "smt", smt.violations, smt.lines);
|
|
1122
|
+
if (smt.undecided.length === 0) {
|
|
1123
|
+
return result(
|
|
1124
|
+
{ type: "proven", method: METHOD_SMT, inductiveInvariant: combineCertificates(smt.certificates) },
|
|
1125
|
+
"smt",
|
|
1126
|
+
[],
|
|
1127
|
+
smt.lines
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
return result(
|
|
1131
|
+
{ type: "unknown", reason: `${why}; left undecided by the SMT route: ${smt.undecided.join("; ")}` },
|
|
1132
|
+
"smt",
|
|
1133
|
+
[],
|
|
1134
|
+
smt.lines
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
function combineCertificates(certificates) {
|
|
1138
|
+
if (certificates.length === 0) return null;
|
|
1139
|
+
return certificates.map((c) => `[${c.subject}] ${c.invariant}`).join("\n");
|
|
1140
|
+
}
|
|
423
1141
|
export {
|
|
424
1142
|
DBM,
|
|
425
1143
|
DUMP_ENV,
|
|
@@ -428,6 +1146,8 @@ export {
|
|
|
428
1146
|
MarkingState,
|
|
429
1147
|
MarkingStateBuilder,
|
|
430
1148
|
NOTE_ENUMERATED,
|
|
1149
|
+
OpenNetContract,
|
|
1150
|
+
OpenNetContractBuilder,
|
|
431
1151
|
SmtVerifier,
|
|
432
1152
|
StateClass,
|
|
433
1153
|
StateClassGraph,
|
|
@@ -445,6 +1165,7 @@ export {
|
|
|
445
1165
|
canonicalInvariantOrder,
|
|
446
1166
|
checkCertificate,
|
|
447
1167
|
checkLinearBoundExact,
|
|
1168
|
+
closeOpenNet,
|
|
448
1169
|
computePInvariants,
|
|
449
1170
|
computePSemiflows,
|
|
450
1171
|
computeSCCs,
|
|
@@ -482,11 +1203,13 @@ export {
|
|
|
482
1203
|
placeBound,
|
|
483
1204
|
placeholderCertificate,
|
|
484
1205
|
propertyDescription,
|
|
1206
|
+
quiescentCount,
|
|
485
1207
|
replayCounterexample,
|
|
486
1208
|
resolveZ3,
|
|
487
1209
|
rethrowIfProgrammingError,
|
|
488
1210
|
runZ3Spacer,
|
|
489
1211
|
runZ3Text,
|
|
1212
|
+
strandedPlaces,
|
|
490
1213
|
strandingExcuses,
|
|
491
1214
|
strandsToken,
|
|
492
1215
|
strengthenWithSemiflows,
|
|
@@ -494,6 +1217,7 @@ export {
|
|
|
494
1217
|
terminatesAtSink,
|
|
495
1218
|
unreachable,
|
|
496
1219
|
vcScript,
|
|
1220
|
+
verifyOpenNet,
|
|
497
1221
|
verifyViaStateClassGraph,
|
|
498
1222
|
violationDemand,
|
|
499
1223
|
z3Available,
|