libpetri 5.0.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-4HMFNYTH.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-MQZ6IM63.js +8383 -0
- 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 +4892 -4896
- package/dist/{event-store-hlH3-bzJ.d.ts → event-store-D6i4u41W.d.ts} +7 -1
- package/dist/export/index.d.ts +1 -1
- package/dist/index.d.ts +62 -10
- package/dist/index.js +359 -2156
- package/dist/index.js.map +1 -1
- package/dist/{petri-net-CH7UvjOW.d.ts → petri-net-34SkD5RT.d.ts} +85 -3
- package/dist/render-dom/index.js +1 -1
- package/dist/verification/index.d.ts +843 -16
- package/dist/verification/index.js +755 -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-75KEJQGC.js +0 -4505
- package/dist/chunk-75KEJQGC.js.map +0 -1
- package/dist/chunk-SXK2Z45Z.js.map +0 -1
- /package/dist/{chunk-4HMFNYTH.js.map → chunk-5LE2M5PW.js.map} +0 -0
package/dist/chunk-75KEJQGC.js
DELETED
|
@@ -1,4505 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
earliest,
|
|
3
|
-
latest
|
|
4
|
-
} from "./chunk-ATT7U5H5.js";
|
|
5
|
-
|
|
6
|
-
// src/core/in.ts
|
|
7
|
-
function one(place) {
|
|
8
|
-
return { type: "one", place };
|
|
9
|
-
}
|
|
10
|
-
function exactly(count, place) {
|
|
11
|
-
if (count < 1) {
|
|
12
|
-
throw new Error(`count must be >= 1, got: ${count}`);
|
|
13
|
-
}
|
|
14
|
-
return { type: "exactly", place, count };
|
|
15
|
-
}
|
|
16
|
-
function all(place) {
|
|
17
|
-
return { type: "all", place };
|
|
18
|
-
}
|
|
19
|
-
function atLeast(minimum, place) {
|
|
20
|
-
if (minimum < 1) {
|
|
21
|
-
throw new Error(`minimum must be >= 1, got: ${minimum}`);
|
|
22
|
-
}
|
|
23
|
-
return { type: "at-least", place, minimum };
|
|
24
|
-
}
|
|
25
|
-
function requiredCount(spec) {
|
|
26
|
-
switch (spec.type) {
|
|
27
|
-
case "one":
|
|
28
|
-
return 1;
|
|
29
|
-
case "exactly":
|
|
30
|
-
return spec.count;
|
|
31
|
-
case "all":
|
|
32
|
-
return 1;
|
|
33
|
-
case "at-least":
|
|
34
|
-
return spec.minimum;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function consumptionCount(spec, available) {
|
|
38
|
-
if (available < requiredCount(spec)) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
`Cannot consume from '${spec.place.name}': available=${available}, required=${requiredCount(spec)}`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
switch (spec.type) {
|
|
44
|
-
case "one":
|
|
45
|
-
return 1;
|
|
46
|
-
case "exactly":
|
|
47
|
-
return spec.count;
|
|
48
|
-
case "all":
|
|
49
|
-
return available;
|
|
50
|
-
case "at-least":
|
|
51
|
-
return available;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// src/core/out.ts
|
|
56
|
-
function and(...children) {
|
|
57
|
-
if (children.length === 0) {
|
|
58
|
-
throw new Error("AND requires at least 1 child");
|
|
59
|
-
}
|
|
60
|
-
return { type: "and", children };
|
|
61
|
-
}
|
|
62
|
-
function andPlaces(...places) {
|
|
63
|
-
return and(...places.map(outPlace));
|
|
64
|
-
}
|
|
65
|
-
function xor(...children) {
|
|
66
|
-
if (children.length < 2) {
|
|
67
|
-
throw new Error("XOR requires at least 2 children");
|
|
68
|
-
}
|
|
69
|
-
return { type: "xor", children };
|
|
70
|
-
}
|
|
71
|
-
function xorPlaces(...places) {
|
|
72
|
-
return xor(...places.map(outPlace));
|
|
73
|
-
}
|
|
74
|
-
function outPlace(p) {
|
|
75
|
-
return { type: "place", place: p };
|
|
76
|
-
}
|
|
77
|
-
function timeout(afterMs, child) {
|
|
78
|
-
if (afterMs <= 0) {
|
|
79
|
-
throw new Error(`Timeout must be positive: ${afterMs}`);
|
|
80
|
-
}
|
|
81
|
-
return { type: "timeout", afterMs, child };
|
|
82
|
-
}
|
|
83
|
-
function timeoutPlace(afterMs, p) {
|
|
84
|
-
return timeout(afterMs, outPlace(p));
|
|
85
|
-
}
|
|
86
|
-
function forwardInput(from, to) {
|
|
87
|
-
return { type: "forward-input", from, to };
|
|
88
|
-
}
|
|
89
|
-
function allPlaces(out) {
|
|
90
|
-
const result = /* @__PURE__ */ new Set();
|
|
91
|
-
collectPlaces(out, result);
|
|
92
|
-
return result;
|
|
93
|
-
}
|
|
94
|
-
function collectPlaces(out, result) {
|
|
95
|
-
switch (out.type) {
|
|
96
|
-
case "place":
|
|
97
|
-
result.add(out.place);
|
|
98
|
-
break;
|
|
99
|
-
case "forward-input":
|
|
100
|
-
result.add(out.to);
|
|
101
|
-
break;
|
|
102
|
-
case "and":
|
|
103
|
-
case "xor":
|
|
104
|
-
for (const child of out.children) {
|
|
105
|
-
collectPlaces(child, result);
|
|
106
|
-
}
|
|
107
|
-
break;
|
|
108
|
-
case "timeout":
|
|
109
|
-
collectPlaces(out.child, result);
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
function enumerateBranches(out) {
|
|
114
|
-
switch (out.type) {
|
|
115
|
-
case "place":
|
|
116
|
-
return [/* @__PURE__ */ new Set([out.place])];
|
|
117
|
-
case "forward-input":
|
|
118
|
-
return [/* @__PURE__ */ new Set([out.to])];
|
|
119
|
-
case "and": {
|
|
120
|
-
let result = [/* @__PURE__ */ new Set()];
|
|
121
|
-
for (const child of out.children) {
|
|
122
|
-
result = crossProduct(result, enumerateBranches(child));
|
|
123
|
-
}
|
|
124
|
-
return result;
|
|
125
|
-
}
|
|
126
|
-
case "xor": {
|
|
127
|
-
const result = [];
|
|
128
|
-
for (const child of out.children) {
|
|
129
|
-
result.push(...enumerateBranches(child));
|
|
130
|
-
}
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
133
|
-
case "timeout":
|
|
134
|
-
return enumerateBranches(out.child);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function crossProduct(a, b) {
|
|
138
|
-
const result = [];
|
|
139
|
-
for (const setA of a) {
|
|
140
|
-
for (const setB of b) {
|
|
141
|
-
const merged = new Set(setA);
|
|
142
|
-
for (const p of setB) merged.add(p);
|
|
143
|
-
result.push(merged);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return result;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// src/core/transition-action.ts
|
|
150
|
-
function passthrough() {
|
|
151
|
-
return PASSTHROUGH;
|
|
152
|
-
}
|
|
153
|
-
var PASSTHROUGH = async () => {
|
|
154
|
-
};
|
|
155
|
-
function isPassthrough(action) {
|
|
156
|
-
return action === PASSTHROUGH;
|
|
157
|
-
}
|
|
158
|
-
function transform(fn) {
|
|
159
|
-
return async (ctx) => {
|
|
160
|
-
const result = fn(ctx);
|
|
161
|
-
for (const outputPlace of ctx.outputPlaces()) {
|
|
162
|
-
ctx.output(outputPlace, result);
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
function fork() {
|
|
167
|
-
return transform((ctx) => {
|
|
168
|
-
const inputPlaces = ctx.inputPlaces();
|
|
169
|
-
if (inputPlaces.size !== 1) {
|
|
170
|
-
throw new Error(`Fork requires exactly 1 input place, found ${inputPlaces.size}`);
|
|
171
|
-
}
|
|
172
|
-
const inputPlace = inputPlaces.values().next().value;
|
|
173
|
-
return ctx.input(inputPlace);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
function transformFrom(inputPlace, fn) {
|
|
177
|
-
return transform((ctx) => fn(ctx.input(inputPlace)));
|
|
178
|
-
}
|
|
179
|
-
function transformAsync(fn) {
|
|
180
|
-
return async (ctx) => {
|
|
181
|
-
const result = await fn(ctx);
|
|
182
|
-
for (const outputPlace of ctx.outputPlaces()) {
|
|
183
|
-
ctx.output(outputPlace, result);
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
function produce(place, value) {
|
|
188
|
-
return async (ctx) => {
|
|
189
|
-
ctx.output(place, value);
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
function withTimeout(action, timeoutMs, timeoutPlace2, timeoutValue) {
|
|
193
|
-
return (ctx) => {
|
|
194
|
-
return new Promise((resolve, reject) => {
|
|
195
|
-
let completed = false;
|
|
196
|
-
const timer = setTimeout(() => {
|
|
197
|
-
if (!completed) {
|
|
198
|
-
completed = true;
|
|
199
|
-
ctx.output(timeoutPlace2, timeoutValue);
|
|
200
|
-
resolve();
|
|
201
|
-
}
|
|
202
|
-
}, timeoutMs);
|
|
203
|
-
action(ctx).then(
|
|
204
|
-
() => {
|
|
205
|
-
if (!completed) {
|
|
206
|
-
completed = true;
|
|
207
|
-
clearTimeout(timer);
|
|
208
|
-
resolve();
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
(err) => {
|
|
212
|
-
if (!completed) {
|
|
213
|
-
completed = true;
|
|
214
|
-
clearTimeout(timer);
|
|
215
|
-
reject(err);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
);
|
|
219
|
-
});
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// src/verification/marking-state.ts
|
|
224
|
-
var MARKING_STATE_KEY = /* @__PURE__ */ Symbol("MarkingState.internal");
|
|
225
|
-
var MarkingState = class _MarkingState {
|
|
226
|
-
tokenCounts;
|
|
227
|
-
placesByName;
|
|
228
|
-
/** @internal Use {@link MarkingState.builder} or {@link MarkingState.empty} to create instances. */
|
|
229
|
-
constructor(key, tokenCounts, placesByName) {
|
|
230
|
-
if (key !== MARKING_STATE_KEY) throw new Error("Use MarkingState.builder() to create instances");
|
|
231
|
-
this.tokenCounts = tokenCounts;
|
|
232
|
-
this.placesByName = placesByName;
|
|
233
|
-
}
|
|
234
|
-
/** Returns the token count for a place (0 if absent). */
|
|
235
|
-
tokens(place) {
|
|
236
|
-
return this.tokenCounts.get(place.name) ?? 0;
|
|
237
|
-
}
|
|
238
|
-
/** Checks if a place has at least one token. */
|
|
239
|
-
hasTokens(place) {
|
|
240
|
-
return this.tokens(place) > 0;
|
|
241
|
-
}
|
|
242
|
-
/** Checks if any of the given places has tokens. */
|
|
243
|
-
hasTokensInAny(places) {
|
|
244
|
-
for (const p of places) {
|
|
245
|
-
if (this.hasTokens(p)) return true;
|
|
246
|
-
}
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
/** Returns all places with tokens > 0. */
|
|
250
|
-
placesWithTokens() {
|
|
251
|
-
return [...this.placesByName.values()];
|
|
252
|
-
}
|
|
253
|
-
/** Returns the total number of tokens. */
|
|
254
|
-
totalTokens() {
|
|
255
|
-
let sum = 0;
|
|
256
|
-
for (const count of this.tokenCounts.values()) sum += count;
|
|
257
|
-
return sum;
|
|
258
|
-
}
|
|
259
|
-
/** Checks if no tokens exist anywhere. */
|
|
260
|
-
isEmpty() {
|
|
261
|
-
return this.tokenCounts.size === 0;
|
|
262
|
-
}
|
|
263
|
-
toString() {
|
|
264
|
-
if (this.tokenCounts.size === 0) return "{}";
|
|
265
|
-
const entries = [...this.tokenCounts.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, count]) => `${name}:${count}`);
|
|
266
|
-
return `{${entries.join(", ")}}`;
|
|
267
|
-
}
|
|
268
|
-
static empty() {
|
|
269
|
-
return new _MarkingState(MARKING_STATE_KEY, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map());
|
|
270
|
-
}
|
|
271
|
-
static builder() {
|
|
272
|
-
return new MarkingStateBuilder();
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
var MarkingStateBuilder = class {
|
|
276
|
-
tokenCounts = /* @__PURE__ */ new Map();
|
|
277
|
-
placesByName = /* @__PURE__ */ new Map();
|
|
278
|
-
/** Sets the token count for a place. */
|
|
279
|
-
tokens(place, count) {
|
|
280
|
-
if (count < 0) throw new Error(`Token count cannot be negative: ${count}`);
|
|
281
|
-
if (count > 0) {
|
|
282
|
-
this.tokenCounts.set(place.name, count);
|
|
283
|
-
this.placesByName.set(place.name, place);
|
|
284
|
-
} else {
|
|
285
|
-
this.tokenCounts.delete(place.name);
|
|
286
|
-
this.placesByName.delete(place.name);
|
|
287
|
-
}
|
|
288
|
-
return this;
|
|
289
|
-
}
|
|
290
|
-
/** Adds tokens to a place. */
|
|
291
|
-
addTokens(place, count) {
|
|
292
|
-
if (count < 0) throw new Error(`Token count cannot be negative: ${count}`);
|
|
293
|
-
if (count > 0) {
|
|
294
|
-
const current = this.tokenCounts.get(place.name) ?? 0;
|
|
295
|
-
this.tokenCounts.set(place.name, current + count);
|
|
296
|
-
this.placesByName.set(place.name, place);
|
|
297
|
-
}
|
|
298
|
-
return this;
|
|
299
|
-
}
|
|
300
|
-
/** Removes tokens from a place. Throws if insufficient. */
|
|
301
|
-
removeTokens(place, count) {
|
|
302
|
-
const current = this.tokenCounts.get(place.name) ?? 0;
|
|
303
|
-
const newCount = current - count;
|
|
304
|
-
if (newCount < 0) {
|
|
305
|
-
throw new Error(
|
|
306
|
-
`Cannot remove ${count} tokens from ${place.name} (has ${current})`
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
if (newCount === 0) {
|
|
310
|
-
this.tokenCounts.delete(place.name);
|
|
311
|
-
this.placesByName.delete(place.name);
|
|
312
|
-
} else {
|
|
313
|
-
this.tokenCounts.set(place.name, newCount);
|
|
314
|
-
}
|
|
315
|
-
return this;
|
|
316
|
-
}
|
|
317
|
-
/** Copies all token counts from another marking state. */
|
|
318
|
-
copyFrom(other) {
|
|
319
|
-
for (const p of other.placesWithTokens()) {
|
|
320
|
-
this.tokenCounts.set(p.name, other.tokens(p));
|
|
321
|
-
this.placesByName.set(p.name, p);
|
|
322
|
-
}
|
|
323
|
-
return this;
|
|
324
|
-
}
|
|
325
|
-
build() {
|
|
326
|
-
return new MarkingState(MARKING_STATE_KEY, new Map(this.tokenCounts), new Map(this.placesByName));
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
// src/verification/smt-property.ts
|
|
331
|
-
function deadlockFree() {
|
|
332
|
-
return { type: "deadlock-free" };
|
|
333
|
-
}
|
|
334
|
-
function terminatesAtSink() {
|
|
335
|
-
return { type: "terminates-at-sink" };
|
|
336
|
-
}
|
|
337
|
-
function mutualExclusion(p1, p2) {
|
|
338
|
-
return { type: "mutual-exclusion", p1, p2 };
|
|
339
|
-
}
|
|
340
|
-
function placeBound(place, bound) {
|
|
341
|
-
return { type: "place-bound", place, bound };
|
|
342
|
-
}
|
|
343
|
-
function unreachable(places) {
|
|
344
|
-
return { type: "unreachable", places: new Set(places) };
|
|
345
|
-
}
|
|
346
|
-
function branchPlaceBound(place, bound) {
|
|
347
|
-
return { type: "branch-place-bound", place, bound };
|
|
348
|
-
}
|
|
349
|
-
function joinedOrDeadLettered(pending) {
|
|
350
|
-
return { type: "joined-or-dead-lettered", pending };
|
|
351
|
-
}
|
|
352
|
-
function propertyDescription(prop) {
|
|
353
|
-
switch (prop.type) {
|
|
354
|
-
case "deadlock-free":
|
|
355
|
-
return "Deadlock-freedom";
|
|
356
|
-
case "terminates-at-sink":
|
|
357
|
-
return "Terminates at a declared sink";
|
|
358
|
-
case "mutual-exclusion":
|
|
359
|
-
return `Mutual exclusion of ${prop.p1.name} and ${prop.p2.name}`;
|
|
360
|
-
case "place-bound":
|
|
361
|
-
return `Place ${prop.place.name} bounded by ${prop.bound}`;
|
|
362
|
-
case "unreachable":
|
|
363
|
-
return `Unreachability of marking with tokens in {${[...prop.places].map((p) => p.name).join(", ")}}`;
|
|
364
|
-
case "branch-place-bound":
|
|
365
|
-
return `Branch place bound (\u03BD-budget): ${prop.place.name} <= ${prop.bound}`;
|
|
366
|
-
case "joined-or-dead-lettered":
|
|
367
|
-
return `Joined-or-dead-lettered: ${prop.pending.name} = 0 at quiescence`;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// src/verification/encoding/flat-transition.ts
|
|
372
|
-
function flatTransition(name, source, branchIndex, preVector, postVector, inhibitorPlaces, readPlaces, resetPlaces, consumeAll) {
|
|
373
|
-
return {
|
|
374
|
-
name,
|
|
375
|
-
source,
|
|
376
|
-
branchIndex,
|
|
377
|
-
preVector,
|
|
378
|
-
postVector,
|
|
379
|
-
inhibitorPlaces,
|
|
380
|
-
readPlaces,
|
|
381
|
-
resetPlaces,
|
|
382
|
-
consumeAll
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// src/verification/analysis/environment-analysis-mode.ts
|
|
387
|
-
function alwaysAvailable() {
|
|
388
|
-
return { type: "always-available" };
|
|
389
|
-
}
|
|
390
|
-
function bounded(maxTokens) {
|
|
391
|
-
if (maxTokens < 0) throw new Error("maxTokens must be non-negative");
|
|
392
|
-
return { type: "bounded", maxTokens };
|
|
393
|
-
}
|
|
394
|
-
function ignore() {
|
|
395
|
-
return { type: "ignore" };
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// src/verification/encoding/net-flattener.ts
|
|
399
|
-
function flatten(net, environmentPlaces = /* @__PURE__ */ new Set(), environmentMode = alwaysAvailable()) {
|
|
400
|
-
const allPlacesSet = /* @__PURE__ */ new Map();
|
|
401
|
-
for (const p of net.places) {
|
|
402
|
-
allPlacesSet.set(p.name, p);
|
|
403
|
-
}
|
|
404
|
-
for (const t of net.transitions) {
|
|
405
|
-
for (const inSpec of t.inputSpecs) {
|
|
406
|
-
allPlacesSet.set(inSpec.place.name, inSpec.place);
|
|
407
|
-
}
|
|
408
|
-
if (t.outputSpec !== null) {
|
|
409
|
-
for (const p of allPlaces(t.outputSpec)) {
|
|
410
|
-
allPlacesSet.set(p.name, p);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
for (const arc of t.inhibitors) allPlacesSet.set(arc.place.name, arc.place);
|
|
414
|
-
for (const arc of t.reads) allPlacesSet.set(arc.place.name, arc.place);
|
|
415
|
-
for (const arc of t.resets) allPlacesSet.set(arc.place.name, arc.place);
|
|
416
|
-
}
|
|
417
|
-
const places = [...allPlacesSet.values()].sort((a, b) => compareCodePoints(a.name, b.name));
|
|
418
|
-
const placeIndex = /* @__PURE__ */ new Map();
|
|
419
|
-
for (let i = 0; i < places.length; i++) {
|
|
420
|
-
placeIndex.set(places[i].name, i);
|
|
421
|
-
}
|
|
422
|
-
const environmentBounds = /* @__PURE__ */ new Map();
|
|
423
|
-
const environmentInjection = /* @__PURE__ */ new Map();
|
|
424
|
-
switch (environmentMode.type) {
|
|
425
|
-
case "always-available":
|
|
426
|
-
for (const ep of environmentPlaces) {
|
|
427
|
-
environmentInjection.set(ep.place.name, null);
|
|
428
|
-
}
|
|
429
|
-
break;
|
|
430
|
-
case "bounded":
|
|
431
|
-
for (const ep of environmentPlaces) {
|
|
432
|
-
environmentBounds.set(ep.place.name, environmentMode.maxTokens);
|
|
433
|
-
environmentInjection.set(ep.place.name, environmentMode.maxTokens);
|
|
434
|
-
}
|
|
435
|
-
break;
|
|
436
|
-
case "ignore":
|
|
437
|
-
break;
|
|
438
|
-
}
|
|
439
|
-
const n = places.length;
|
|
440
|
-
const flatTransitions = [];
|
|
441
|
-
for (const transition of net.transitions) {
|
|
442
|
-
const branches = enumerateOutputBranches(transition);
|
|
443
|
-
for (let branchIdx = 0; branchIdx < branches.length; branchIdx++) {
|
|
444
|
-
const branchPlaces = branches[branchIdx];
|
|
445
|
-
const name = branches.length > 1 ? `${transition.name}_b${branchIdx}` : transition.name;
|
|
446
|
-
const preVector = new Array(n).fill(0);
|
|
447
|
-
const consumeAll = new Array(n).fill(false);
|
|
448
|
-
for (const inSpec of transition.inputSpecs) {
|
|
449
|
-
const idx = placeIndex.get(inSpec.place.name);
|
|
450
|
-
if (idx === void 0) continue;
|
|
451
|
-
switch (inSpec.type) {
|
|
452
|
-
case "one":
|
|
453
|
-
preVector[idx] = 1;
|
|
454
|
-
break;
|
|
455
|
-
case "exactly":
|
|
456
|
-
preVector[idx] = inSpec.count;
|
|
457
|
-
break;
|
|
458
|
-
case "all":
|
|
459
|
-
preVector[idx] = 1;
|
|
460
|
-
consumeAll[idx] = true;
|
|
461
|
-
break;
|
|
462
|
-
case "at-least":
|
|
463
|
-
preVector[idx] = inSpec.minimum;
|
|
464
|
-
consumeAll[idx] = true;
|
|
465
|
-
break;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
const postVector = new Array(n).fill(0);
|
|
469
|
-
for (const p of branchPlaces) {
|
|
470
|
-
const idx = placeIndex.get(p.name);
|
|
471
|
-
if (idx !== void 0) {
|
|
472
|
-
postVector[idx] = 1;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
const inhibitorPlaces = transition.inhibitors.map((arc) => placeIndex.get(arc.place.name)).filter((idx) => idx !== void 0);
|
|
476
|
-
const readPlaces = transition.reads.map((arc) => placeIndex.get(arc.place.name)).filter((idx) => idx !== void 0);
|
|
477
|
-
const resetPlaces = transition.resets.map((arc) => placeIndex.get(arc.place.name)).filter((idx) => idx !== void 0);
|
|
478
|
-
flatTransitions.push(flatTransition(
|
|
479
|
-
name,
|
|
480
|
-
transition,
|
|
481
|
-
branches.length > 1 ? branchIdx : -1,
|
|
482
|
-
preVector,
|
|
483
|
-
postVector,
|
|
484
|
-
inhibitorPlaces,
|
|
485
|
-
readPlaces,
|
|
486
|
-
resetPlaces,
|
|
487
|
-
consumeAll
|
|
488
|
-
));
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
return {
|
|
492
|
-
places,
|
|
493
|
-
placeIndex,
|
|
494
|
-
transitions: flatTransitions,
|
|
495
|
-
environmentBounds,
|
|
496
|
-
environmentInjection
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
function enumerateOutputBranches(t) {
|
|
500
|
-
if (t.outputSpec !== null) {
|
|
501
|
-
return enumerateBranches(t.outputSpec);
|
|
502
|
-
}
|
|
503
|
-
return [/* @__PURE__ */ new Set()];
|
|
504
|
-
}
|
|
505
|
-
function compareCodePoints(a, b) {
|
|
506
|
-
const ia = a[Symbol.iterator]();
|
|
507
|
-
const ib = b[Symbol.iterator]();
|
|
508
|
-
for (; ; ) {
|
|
509
|
-
const na = ia.next();
|
|
510
|
-
const nb = ib.next();
|
|
511
|
-
if (na.done && nb.done) return 0;
|
|
512
|
-
if (na.done) return -1;
|
|
513
|
-
if (nb.done) return 1;
|
|
514
|
-
const ca = na.value.codePointAt(0);
|
|
515
|
-
const cb = nb.value.codePointAt(0);
|
|
516
|
-
if (ca !== cb) return ca - cb;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// src/verification/encoding/incidence-matrix.ts
|
|
521
|
-
var IncidenceMatrix = class _IncidenceMatrix {
|
|
522
|
-
_pre;
|
|
523
|
-
_post;
|
|
524
|
-
_incidence;
|
|
525
|
-
_numTransitions;
|
|
526
|
-
_numPlaces;
|
|
527
|
-
constructor(pre, post, incidence, numTransitions, numPlaces) {
|
|
528
|
-
this._pre = pre;
|
|
529
|
-
this._post = post;
|
|
530
|
-
this._incidence = incidence;
|
|
531
|
-
this._numTransitions = numTransitions;
|
|
532
|
-
this._numPlaces = numPlaces;
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Computes the incidence matrix from a FlatNet.
|
|
536
|
-
*
|
|
537
|
-
* Environment-injected places (VER-006) each contribute one extra **injector
|
|
538
|
-
* column** (a virtual transition that produces one token into that place and
|
|
539
|
-
* consumes nothing). This makes P-invariant computation env-aware: a valid
|
|
540
|
-
* invariant `y` must satisfy `y^T·C = 0` for the injector column too, forcing
|
|
541
|
-
* `y[envPlace] = 0` and thereby discarding closed-net conservation laws (e.g.
|
|
542
|
-
* `IN + OUT = const`) that would otherwise vacuously bound an injectable place.
|
|
543
|
-
*/
|
|
544
|
-
static from(flatNet) {
|
|
545
|
-
const T = flatNet.transitions.length;
|
|
546
|
-
const P = flatNet.places.length;
|
|
547
|
-
const pre = [];
|
|
548
|
-
const post = [];
|
|
549
|
-
const incidence = [];
|
|
550
|
-
for (let t = 0; t < T; t++) {
|
|
551
|
-
const ft = flatNet.transitions[t];
|
|
552
|
-
const preRow = new Array(P);
|
|
553
|
-
const postRow = new Array(P);
|
|
554
|
-
const incRow = new Array(P);
|
|
555
|
-
for (let p = 0; p < P; p++) {
|
|
556
|
-
preRow[p] = ft.preVector[p];
|
|
557
|
-
postRow[p] = ft.postVector[p];
|
|
558
|
-
incRow[p] = postRow[p] - preRow[p];
|
|
559
|
-
}
|
|
560
|
-
pre.push(preRow);
|
|
561
|
-
post.push(postRow);
|
|
562
|
-
incidence.push(incRow);
|
|
563
|
-
}
|
|
564
|
-
let injectorCount = 0;
|
|
565
|
-
for (const name of flatNet.environmentInjection.keys()) {
|
|
566
|
-
const idx = flatNet.placeIndex.get(name);
|
|
567
|
-
if (idx == null) continue;
|
|
568
|
-
const preRow = new Array(P).fill(0);
|
|
569
|
-
const postRow = new Array(P).fill(0);
|
|
570
|
-
const incRow = new Array(P).fill(0);
|
|
571
|
-
postRow[idx] = 1;
|
|
572
|
-
incRow[idx] = 1;
|
|
573
|
-
pre.push(preRow);
|
|
574
|
-
post.push(postRow);
|
|
575
|
-
incidence.push(incRow);
|
|
576
|
-
injectorCount++;
|
|
577
|
-
}
|
|
578
|
-
return new _IncidenceMatrix(pre, post, incidence, T + injectorCount, P);
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Returns C^T (transpose of incidence matrix), dimensions [P][T].
|
|
582
|
-
* Used for P-invariant computation: null space of C^T gives P-invariants.
|
|
583
|
-
*/
|
|
584
|
-
transposedIncidence() {
|
|
585
|
-
const ct = [];
|
|
586
|
-
for (let p = 0; p < this._numPlaces; p++) {
|
|
587
|
-
const row = new Array(this._numTransitions);
|
|
588
|
-
for (let t = 0; t < this._numTransitions; t++) {
|
|
589
|
-
row[t] = this._incidence[t][p];
|
|
590
|
-
}
|
|
591
|
-
ct.push(row);
|
|
592
|
-
}
|
|
593
|
-
return ct;
|
|
594
|
-
}
|
|
595
|
-
/** Returns the pre-matrix (tokens consumed). T×P. */
|
|
596
|
-
pre() {
|
|
597
|
-
return this._pre;
|
|
598
|
-
}
|
|
599
|
-
/** Returns the post-matrix (tokens produced). T×P. */
|
|
600
|
-
post() {
|
|
601
|
-
return this._post;
|
|
602
|
-
}
|
|
603
|
-
/** Returns the incidence matrix C[t][p] = post - pre. T×P. */
|
|
604
|
-
incidence() {
|
|
605
|
-
return this._incidence;
|
|
606
|
-
}
|
|
607
|
-
numTransitions() {
|
|
608
|
-
return this._numTransitions;
|
|
609
|
-
}
|
|
610
|
-
numPlaces() {
|
|
611
|
-
return this._numPlaces;
|
|
612
|
-
}
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
// src/verification/invariant/p-invariant.ts
|
|
616
|
-
function pInvariant(weights, constant, support) {
|
|
617
|
-
return { weights, constant, support };
|
|
618
|
-
}
|
|
619
|
-
function pInvariantToString(inv) {
|
|
620
|
-
const parts = [];
|
|
621
|
-
for (const i of inv.support) {
|
|
622
|
-
if (inv.weights[i] !== 1) {
|
|
623
|
-
parts.push(`${inv.weights[i]}*p${i}`);
|
|
624
|
-
} else {
|
|
625
|
-
parts.push(`p${i}`);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
return `PInvariant[${parts.join(" + ")} = ${inv.constant}]`;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// src/verification/invariant/p-invariant-computer.ts
|
|
632
|
-
function computePInvariants(matrix, flatNet, initialMarking) {
|
|
633
|
-
const P = matrix.numPlaces();
|
|
634
|
-
const T = matrix.numTransitions();
|
|
635
|
-
if (P === 0 || T === 0) return [];
|
|
636
|
-
const ct = matrix.transposedIncidence();
|
|
637
|
-
const cols = T + P;
|
|
638
|
-
const augmented = [];
|
|
639
|
-
for (let i = 0; i < P; i++) {
|
|
640
|
-
const row = new Array(cols).fill(0);
|
|
641
|
-
for (let j = 0; j < T; j++) {
|
|
642
|
-
row[j] = ct[i][j];
|
|
643
|
-
}
|
|
644
|
-
row[T + i] = 1;
|
|
645
|
-
augmented.push(row);
|
|
646
|
-
}
|
|
647
|
-
let pivotRow = 0;
|
|
648
|
-
for (let col = 0; col < T && pivotRow < P; col++) {
|
|
649
|
-
let pivot = -1;
|
|
650
|
-
for (let row = pivotRow; row < P; row++) {
|
|
651
|
-
if (augmented[row][col] !== 0) {
|
|
652
|
-
pivot = row;
|
|
653
|
-
break;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (pivot === -1) continue;
|
|
657
|
-
if (pivot !== pivotRow) {
|
|
658
|
-
const tmp = augmented[pivotRow];
|
|
659
|
-
augmented[pivotRow] = augmented[pivot];
|
|
660
|
-
augmented[pivot] = tmp;
|
|
661
|
-
}
|
|
662
|
-
for (let row = 0; row < P; row++) {
|
|
663
|
-
if (row === pivotRow || augmented[row][col] === 0) continue;
|
|
664
|
-
const a = augmented[pivotRow][col];
|
|
665
|
-
const b = augmented[row][col];
|
|
666
|
-
for (let c = 0; c < cols; c++) {
|
|
667
|
-
augmented[row][c] = a * augmented[row][c] - b * augmented[pivotRow][c];
|
|
668
|
-
}
|
|
669
|
-
normalizeRow(augmented[row], cols);
|
|
670
|
-
}
|
|
671
|
-
pivotRow++;
|
|
672
|
-
}
|
|
673
|
-
const invariants = [];
|
|
674
|
-
for (let row = 0; row < P; row++) {
|
|
675
|
-
let isZero = true;
|
|
676
|
-
for (let col = 0; col < T; col++) {
|
|
677
|
-
if (augmented[row][col] !== 0) {
|
|
678
|
-
isZero = false;
|
|
679
|
-
break;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
if (!isZero) continue;
|
|
683
|
-
if (!rowIsExact(augmented[row], T, P)) {
|
|
684
|
-
invariants.push(rawInvariant(augmented[row], T, P, flatNet, initialMarking));
|
|
685
|
-
continue;
|
|
686
|
-
}
|
|
687
|
-
const weights = new Array(P);
|
|
688
|
-
let hasPositive = false;
|
|
689
|
-
let hasNegative = false;
|
|
690
|
-
for (let i = 0; i < P; i++) {
|
|
691
|
-
weights[i] = augmented[row][T + i];
|
|
692
|
-
if (weights[i] > 0) hasPositive = true;
|
|
693
|
-
if (weights[i] < 0) hasNegative = true;
|
|
694
|
-
}
|
|
695
|
-
if (!hasPositive && !hasNegative) continue;
|
|
696
|
-
if (!hasPositive) {
|
|
697
|
-
for (let i = 0; i < P; i++) weights[i] = -weights[i];
|
|
698
|
-
}
|
|
699
|
-
const support = /* @__PURE__ */ new Set();
|
|
700
|
-
let constant = 0;
|
|
701
|
-
for (let i = 0; i < P; i++) {
|
|
702
|
-
if (weights[i] !== 0) {
|
|
703
|
-
support.add(i);
|
|
704
|
-
const place = flatNet.places[i];
|
|
705
|
-
constant += weights[i] * initialMarking.tokens(place);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
invariants.push(pInvariant(weights, constant, support));
|
|
709
|
-
}
|
|
710
|
-
return invariants;
|
|
711
|
-
}
|
|
712
|
-
function rowIsExact(row, T, P) {
|
|
713
|
-
for (let i = 0; i < P; i++) {
|
|
714
|
-
if (!Number.isSafeInteger(row[T + i])) return false;
|
|
715
|
-
}
|
|
716
|
-
return true;
|
|
717
|
-
}
|
|
718
|
-
function rawInvariant(row, T, P, flatNet, initialMarking) {
|
|
719
|
-
const weights = new Array(P);
|
|
720
|
-
const support = /* @__PURE__ */ new Set();
|
|
721
|
-
let constant = 0;
|
|
722
|
-
for (let i = 0; i < P; i++) {
|
|
723
|
-
weights[i] = row[T + i];
|
|
724
|
-
if (weights[i] !== 0) {
|
|
725
|
-
support.add(i);
|
|
726
|
-
constant += weights[i] * initialMarking.tokens(flatNet.places[i]);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return pInvariant(weights, constant, support);
|
|
730
|
-
}
|
|
731
|
-
function validateInvariantsExact(matrix, invariants, flatNet, initialMarking) {
|
|
732
|
-
const nonlinear = nonlinearPlaces(flatNet);
|
|
733
|
-
const valid = [];
|
|
734
|
-
const dropped = [];
|
|
735
|
-
for (const inv of invariants) {
|
|
736
|
-
const reason = exactCheckFailure(matrix, inv, nonlinear, flatNet, initialMarking);
|
|
737
|
-
if (reason === null) {
|
|
738
|
-
valid.push(inv);
|
|
739
|
-
} else {
|
|
740
|
-
dropped.push({ invariant: inv, reason });
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
return { valid, dropped };
|
|
744
|
-
}
|
|
745
|
-
function nonlinearPlaces(flatNet) {
|
|
746
|
-
const nonlinear = /* @__PURE__ */ new Set();
|
|
747
|
-
for (const ft of flatNet.transitions) {
|
|
748
|
-
for (let p = 0; p < ft.consumeAll.length; p++) {
|
|
749
|
-
if (ft.consumeAll[p]) nonlinear.add(p);
|
|
750
|
-
}
|
|
751
|
-
for (const p of ft.resetPlaces) nonlinear.add(p);
|
|
752
|
-
}
|
|
753
|
-
return nonlinear;
|
|
754
|
-
}
|
|
755
|
-
function exactCheckFailure(matrix, inv, nonlinear, flatNet, initialMarking) {
|
|
756
|
-
const P = matrix.numPlaces();
|
|
757
|
-
const T = matrix.numTransitions();
|
|
758
|
-
if (inv.weights.length !== P) {
|
|
759
|
-
return `weight vector has ${inv.weights.length} entries, expected ${P}`;
|
|
760
|
-
}
|
|
761
|
-
for (let p = 0; p < P; p++) {
|
|
762
|
-
if (!Number.isSafeInteger(inv.weights[p])) {
|
|
763
|
-
return `weight overflow at place '${placeName(flatNet, p)}' (exact value outside this implementation's integer extraction range)`;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
if (!Number.isSafeInteger(inv.constant)) {
|
|
767
|
-
return `constant ${inv.constant} is outside the safe-integer range`;
|
|
768
|
-
}
|
|
769
|
-
for (let p = 0; p < inv.weights.length; p++) {
|
|
770
|
-
if (inv.weights[p] !== 0 && nonlinear.has(p)) {
|
|
771
|
-
return `support intersects consume-all/reset place '${placeName(flatNet, p)}' (non-linear consumption; see Strengthening.lean H1)`;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
const y = inv.weights.map((w) => BigInt(w));
|
|
775
|
-
const incidence = matrix.incidence();
|
|
776
|
-
for (let t = 0; t < T; t++) {
|
|
777
|
-
const row = incidence[t];
|
|
778
|
-
let dot = 0n;
|
|
779
|
-
for (let p = 0; p < P; p++) {
|
|
780
|
-
if (y[p] === 0n) continue;
|
|
781
|
-
if (!Number.isSafeInteger(row[p])) {
|
|
782
|
-
return `incidence entry ${row[p]} at [t=${t}][p=${p}] is outside the safe-integer range`;
|
|
783
|
-
}
|
|
784
|
-
dot += y[p] * BigInt(row[p]);
|
|
785
|
-
}
|
|
786
|
-
if (dot !== 0n) {
|
|
787
|
-
return `y*C is ${dot} (not 0) at ${columnName(flatNet, t)}`;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
let exact = 0n;
|
|
791
|
-
for (let p = 0; p < P; p++) {
|
|
792
|
-
if (y[p] === 0n) continue;
|
|
793
|
-
const tokens = initialMarking.tokens(flatNet.places[p]);
|
|
794
|
-
if (!Number.isSafeInteger(tokens)) {
|
|
795
|
-
return `initial marking of place ${p} (${tokens}) is outside the safe-integer range`;
|
|
796
|
-
}
|
|
797
|
-
exact += y[p] * BigInt(tokens);
|
|
798
|
-
}
|
|
799
|
-
if (exact !== BigInt(inv.constant)) {
|
|
800
|
-
return `constant ${inv.constant} does not match exact y*M0 = ${exact}`;
|
|
801
|
-
}
|
|
802
|
-
return null;
|
|
803
|
-
}
|
|
804
|
-
function placeName(flatNet, p) {
|
|
805
|
-
return flatNet.places[p]?.name ?? `#${p}`;
|
|
806
|
-
}
|
|
807
|
-
function columnName(flatNet, t) {
|
|
808
|
-
const ft = flatNet.transitions[t];
|
|
809
|
-
return ft != null ? `transition '${ft.name}'` : `env-injector column ${t - flatNet.transitions.length}`;
|
|
810
|
-
}
|
|
811
|
-
function sameInvariant(a, b) {
|
|
812
|
-
if (a.constant !== b.constant || a.weights.length !== b.weights.length) return false;
|
|
813
|
-
for (let i = 0; i < a.weights.length; i++) {
|
|
814
|
-
if (a.weights[i] !== b.weights[i]) return false;
|
|
815
|
-
}
|
|
816
|
-
return true;
|
|
817
|
-
}
|
|
818
|
-
function strengthenWithSemiflows(invariants, semiflows) {
|
|
819
|
-
const strengthened = [...invariants];
|
|
820
|
-
let added = 0;
|
|
821
|
-
for (const sf of semiflows) {
|
|
822
|
-
if (!strengthened.some((inv) => sameInvariant(inv, sf))) {
|
|
823
|
-
strengthened.push(sf);
|
|
824
|
-
added++;
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return { invariants: strengthened, added };
|
|
828
|
-
}
|
|
829
|
-
function computePSemiflows(matrix, flatNet, initialMarking) {
|
|
830
|
-
const np = matrix.numPlaces();
|
|
831
|
-
const nt = matrix.numTransitions();
|
|
832
|
-
if (np === 0) return [];
|
|
833
|
-
const incidence = matrix.incidence();
|
|
834
|
-
let rows = [];
|
|
835
|
-
for (let p = 0; p < np; p++) {
|
|
836
|
-
const sig = new Array(nt);
|
|
837
|
-
for (let t = 0; t < nt; t++) sig[t] = incidence[t][p];
|
|
838
|
-
const weight = new Array(np).fill(0);
|
|
839
|
-
weight[p] = 1;
|
|
840
|
-
rows.push({ sig, weight });
|
|
841
|
-
}
|
|
842
|
-
for (let t = 0; t < nt; t++) {
|
|
843
|
-
const next = rows.filter((r) => r.sig[t] === 0);
|
|
844
|
-
const pos = rows.filter((r) => r.sig[t] > 0);
|
|
845
|
-
const neg = rows.filter((r) => r.sig[t] < 0);
|
|
846
|
-
for (const rp of pos) {
|
|
847
|
-
for (const rn of neg) {
|
|
848
|
-
const cp = -rn.sig[t];
|
|
849
|
-
const cn = rp.sig[t];
|
|
850
|
-
const sig = combineRow(cp, rp.sig, cn, rn.sig);
|
|
851
|
-
const weight = combineRow(cp, rp.weight, cn, rn.weight);
|
|
852
|
-
if (sig === null || weight === null) continue;
|
|
853
|
-
reduceGcd(sig, weight);
|
|
854
|
-
next.push({ sig, weight });
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
rows = keepSupportMinimal(next);
|
|
858
|
-
if (rows.length > 8192) rows.length = 8192;
|
|
859
|
-
}
|
|
860
|
-
const semiflows = [];
|
|
861
|
-
for (const { weight } of rows) {
|
|
862
|
-
if (!weight.some((x) => x !== 0)) continue;
|
|
863
|
-
const support = /* @__PURE__ */ new Set();
|
|
864
|
-
let constant = 0;
|
|
865
|
-
for (let p = 0; p < np; p++) {
|
|
866
|
-
if (weight[p] !== 0) {
|
|
867
|
-
support.add(p);
|
|
868
|
-
constant += weight[p] * initialMarking.tokens(flatNet.places[p]);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
if (!Number.isSafeInteger(constant)) continue;
|
|
872
|
-
semiflows.push(pInvariant(weight, constant, support));
|
|
873
|
-
}
|
|
874
|
-
return semiflows;
|
|
875
|
-
}
|
|
876
|
-
function combineRow(cp, a, cn, b) {
|
|
877
|
-
const out = new Array(a.length);
|
|
878
|
-
for (let i = 0; i < a.length; i++) {
|
|
879
|
-
const v = cp * a[i] + cn * b[i];
|
|
880
|
-
if (!Number.isSafeInteger(v)) return null;
|
|
881
|
-
out[i] = v;
|
|
882
|
-
}
|
|
883
|
-
return out;
|
|
884
|
-
}
|
|
885
|
-
function reduceGcd(sig, weight) {
|
|
886
|
-
let g = 0;
|
|
887
|
-
for (const v of sig) g = gcd(g, Math.abs(v));
|
|
888
|
-
for (const v of weight) g = gcd(g, Math.abs(v));
|
|
889
|
-
if (g > 1) {
|
|
890
|
-
for (let i = 0; i < sig.length; i++) sig[i] = sig[i] / g;
|
|
891
|
-
for (let i = 0; i < weight.length; i++) weight[i] = weight[i] / g;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
function keepSupportMinimal(rows) {
|
|
895
|
-
const supports = rows.map((r) => {
|
|
896
|
-
const s = [];
|
|
897
|
-
for (let i = 0; i < r.weight.length; i++) if (r.weight[i] !== 0) s.push(i);
|
|
898
|
-
return s;
|
|
899
|
-
});
|
|
900
|
-
const keep = new Array(rows.length).fill(true);
|
|
901
|
-
for (let i = 0; i < rows.length; i++) {
|
|
902
|
-
if (!keep[i]) continue;
|
|
903
|
-
for (let j = 0; j < rows.length; j++) {
|
|
904
|
-
if (i === j || !keep[j]) continue;
|
|
905
|
-
if (supports[j].length < supports[i].length && supports[j].every((p) => supports[i].includes(p))) {
|
|
906
|
-
keep[i] = false;
|
|
907
|
-
break;
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
return rows.filter((_, i) => keep[i]);
|
|
912
|
-
}
|
|
913
|
-
function isCoveredByInvariants(invariants, numPlaces) {
|
|
914
|
-
const covered = new Array(numPlaces).fill(false);
|
|
915
|
-
for (const inv of invariants) {
|
|
916
|
-
if (inv.weights.some((w) => w < 0)) continue;
|
|
917
|
-
for (const idx of inv.support) {
|
|
918
|
-
if (idx < numPlaces) covered[idx] = true;
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
return covered.every((c) => c);
|
|
922
|
-
}
|
|
923
|
-
function normalizeRow(row, cols) {
|
|
924
|
-
let g = 0;
|
|
925
|
-
for (let c = 0; c < cols; c++) {
|
|
926
|
-
if (row[c] !== 0) {
|
|
927
|
-
g = gcd(g, Math.abs(row[c]));
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
if (g > 1) {
|
|
931
|
-
for (let c = 0; c < cols; c++) {
|
|
932
|
-
row[c] = row[c] / g;
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
function gcd(a, b) {
|
|
937
|
-
while (b !== 0) {
|
|
938
|
-
const t = b;
|
|
939
|
-
b = a % b;
|
|
940
|
-
a = t;
|
|
941
|
-
}
|
|
942
|
-
return a;
|
|
943
|
-
}
|
|
944
|
-
function canonicalInvariantOrder(invariants) {
|
|
945
|
-
const lex = (a, b) => {
|
|
946
|
-
const n = Math.min(a.length, b.length);
|
|
947
|
-
for (let i = 0; i < n; i++) {
|
|
948
|
-
if (a[i] !== b[i]) return a[i] - b[i];
|
|
949
|
-
}
|
|
950
|
-
return a.length - b.length;
|
|
951
|
-
};
|
|
952
|
-
const support = (inv) => [...inv.support].sort((x, y) => x - y);
|
|
953
|
-
return [...invariants].sort(
|
|
954
|
-
(a, b) => lex(support(a), support(b)) || lex(a.weights, b.weights) || a.constant - b.constant
|
|
955
|
-
);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// src/verification/invariant/structural-check.ts
|
|
959
|
-
var MAX_PLACES_FOR_SIPHON_ANALYSIS = 50;
|
|
960
|
-
function structuralCheck(flatNet, initialMarking) {
|
|
961
|
-
const P = flatNet.places.length;
|
|
962
|
-
if (P === 0) {
|
|
963
|
-
return { type: "no-potential-deadlock" };
|
|
964
|
-
}
|
|
965
|
-
if (P > MAX_PLACES_FOR_SIPHON_ANALYSIS) {
|
|
966
|
-
return { type: "inconclusive", reason: `Net has ${P} places, siphon enumeration skipped` };
|
|
967
|
-
}
|
|
968
|
-
const siphons = findMinimalSiphons(flatNet);
|
|
969
|
-
if (siphons.length === 0) {
|
|
970
|
-
return { type: "no-potential-deadlock" };
|
|
971
|
-
}
|
|
972
|
-
for (const siphon of siphons) {
|
|
973
|
-
const trap = findMaximalTrapIn(flatNet, siphon);
|
|
974
|
-
if (trap.size === 0 || !isMarked(trap, flatNet, initialMarking)) {
|
|
975
|
-
return { type: "potential-deadlock", siphon };
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
return { type: "no-potential-deadlock" };
|
|
979
|
-
}
|
|
980
|
-
function findMinimalSiphons(flatNet) {
|
|
981
|
-
const P = flatNet.places.length;
|
|
982
|
-
const siphons = [];
|
|
983
|
-
const placeAsOutput = [];
|
|
984
|
-
for (let p = 0; p < P; p++) {
|
|
985
|
-
placeAsOutput.push([]);
|
|
986
|
-
}
|
|
987
|
-
for (let t = 0; t < flatNet.transitions.length; t++) {
|
|
988
|
-
const ft = flatNet.transitions[t];
|
|
989
|
-
for (let p = 0; p < P; p++) {
|
|
990
|
-
if (ft.postVector[p] > 0) {
|
|
991
|
-
placeAsOutput[p].push(t);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
for (let startPlace = 0; startPlace < P; startPlace++) {
|
|
996
|
-
const siphon = computeSiphonContaining(startPlace, flatNet, placeAsOutput);
|
|
997
|
-
if (siphon !== null && siphon.size > 0) {
|
|
998
|
-
let isMinimal = true;
|
|
999
|
-
const toRemove = [];
|
|
1000
|
-
for (let i = 0; i < siphons.length; i++) {
|
|
1001
|
-
const existing = siphons[i];
|
|
1002
|
-
if (setsEqual(existing, siphon)) {
|
|
1003
|
-
isMinimal = false;
|
|
1004
|
-
break;
|
|
1005
|
-
}
|
|
1006
|
-
if (isSubsetOf(existing, siphon)) {
|
|
1007
|
-
isMinimal = false;
|
|
1008
|
-
break;
|
|
1009
|
-
}
|
|
1010
|
-
if (isSubsetOf(siphon, existing)) {
|
|
1011
|
-
toRemove.push(i);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
for (let i = toRemove.length - 1; i >= 0; i--) {
|
|
1015
|
-
siphons.splice(toRemove[i], 1);
|
|
1016
|
-
}
|
|
1017
|
-
if (isMinimal) {
|
|
1018
|
-
siphons.push(siphon);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
return siphons;
|
|
1023
|
-
}
|
|
1024
|
-
function computeSiphonContaining(startPlace, flatNet, placeAsOutput) {
|
|
1025
|
-
const siphon = /* @__PURE__ */ new Set();
|
|
1026
|
-
siphon.add(startPlace);
|
|
1027
|
-
let changed = true;
|
|
1028
|
-
while (changed) {
|
|
1029
|
-
changed = false;
|
|
1030
|
-
const snapshot = [...siphon];
|
|
1031
|
-
for (const p of snapshot) {
|
|
1032
|
-
for (const t of placeAsOutput[p]) {
|
|
1033
|
-
const ft = flatNet.transitions[t];
|
|
1034
|
-
let hasInputInSiphon = false;
|
|
1035
|
-
for (let q = 0; q < flatNet.places.length; q++) {
|
|
1036
|
-
if (ft.preVector[q] > 0 && siphon.has(q)) {
|
|
1037
|
-
hasInputInSiphon = true;
|
|
1038
|
-
break;
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
if (!hasInputInSiphon) {
|
|
1042
|
-
let added = false;
|
|
1043
|
-
for (let q = 0; q < flatNet.places.length; q++) {
|
|
1044
|
-
if (ft.preVector[q] > 0) {
|
|
1045
|
-
if (!siphon.has(q)) {
|
|
1046
|
-
siphon.add(q);
|
|
1047
|
-
changed = true;
|
|
1048
|
-
}
|
|
1049
|
-
added = true;
|
|
1050
|
-
break;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
if (!added) {
|
|
1054
|
-
return null;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
return siphon;
|
|
1061
|
-
}
|
|
1062
|
-
function findMaximalTrapIn(flatNet, places) {
|
|
1063
|
-
const trap = new Set(places);
|
|
1064
|
-
let changed = true;
|
|
1065
|
-
while (changed) {
|
|
1066
|
-
changed = false;
|
|
1067
|
-
const toRemove = [];
|
|
1068
|
-
for (const p of trap) {
|
|
1069
|
-
let satisfies = true;
|
|
1070
|
-
for (let t = 0; t < flatNet.transitions.length; t++) {
|
|
1071
|
-
const ft = flatNet.transitions[t];
|
|
1072
|
-
if (ft.preVector[p] > 0) {
|
|
1073
|
-
let outputsToTrap = false;
|
|
1074
|
-
for (const q of trap) {
|
|
1075
|
-
if (ft.postVector[q] > 0) {
|
|
1076
|
-
outputsToTrap = true;
|
|
1077
|
-
break;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
if (!outputsToTrap) {
|
|
1081
|
-
satisfies = false;
|
|
1082
|
-
break;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
if (!satisfies) {
|
|
1087
|
-
toRemove.push(p);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
if (toRemove.length > 0) {
|
|
1091
|
-
for (const p of toRemove) trap.delete(p);
|
|
1092
|
-
changed = true;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
return trap;
|
|
1096
|
-
}
|
|
1097
|
-
function isMarked(placeIndices, flatNet, marking) {
|
|
1098
|
-
for (const idx of placeIndices) {
|
|
1099
|
-
const place = flatNet.places[idx];
|
|
1100
|
-
if (marking.tokens(place) > 0) return true;
|
|
1101
|
-
}
|
|
1102
|
-
return false;
|
|
1103
|
-
}
|
|
1104
|
-
function setsEqual(a, b) {
|
|
1105
|
-
if (a.size !== b.size) return false;
|
|
1106
|
-
for (const v of a) {
|
|
1107
|
-
if (!b.has(v)) return false;
|
|
1108
|
-
}
|
|
1109
|
-
return true;
|
|
1110
|
-
}
|
|
1111
|
-
function isSubsetOf(sub, sup) {
|
|
1112
|
-
if (sub.size > sup.size) return false;
|
|
1113
|
-
for (const v of sub) {
|
|
1114
|
-
if (!sup.has(v)) return false;
|
|
1115
|
-
}
|
|
1116
|
-
return true;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
// src/verification/z3/z3-process.ts
|
|
1120
|
-
import { spawn, spawnSync } from "child_process";
|
|
1121
|
-
import { existsSync, mkdirSync, statSync, writeFileSync } from "fs";
|
|
1122
|
-
import * as path from "path";
|
|
1123
|
-
|
|
1124
|
-
// src/verification/z3/smt-text.ts
|
|
1125
|
-
function classifyFirstLine(stdout) {
|
|
1126
|
-
for (const raw of stdout.split("\n")) {
|
|
1127
|
-
const line = raw.trim();
|
|
1128
|
-
if (line === "sat" || line === "unsat" || line === "unknown") return line;
|
|
1129
|
-
}
|
|
1130
|
-
return null;
|
|
1131
|
-
}
|
|
1132
|
-
function timeoutLine(stdout) {
|
|
1133
|
-
return stdout.split("\n").some((l) => l.trim() === "timeout");
|
|
1134
|
-
}
|
|
1135
|
-
function errorLine(text) {
|
|
1136
|
-
for (const raw of text.split("\n")) {
|
|
1137
|
-
const line = raw.trim();
|
|
1138
|
-
if (line.startsWith("(error")) return line;
|
|
1139
|
-
}
|
|
1140
|
-
return null;
|
|
1141
|
-
}
|
|
1142
|
-
function sexprEnd(s, start) {
|
|
1143
|
-
let depth = 0;
|
|
1144
|
-
let inString = false;
|
|
1145
|
-
let inSymbol = false;
|
|
1146
|
-
for (let i = start; i < s.length; i++) {
|
|
1147
|
-
const c = s[i];
|
|
1148
|
-
if (inString) {
|
|
1149
|
-
if (c === '"') inString = false;
|
|
1150
|
-
} else if (inSymbol) {
|
|
1151
|
-
if (c === "|") inSymbol = false;
|
|
1152
|
-
} else if (c === '"') {
|
|
1153
|
-
inString = true;
|
|
1154
|
-
} else if (c === "|") {
|
|
1155
|
-
inSymbol = true;
|
|
1156
|
-
} else if (c === "(") {
|
|
1157
|
-
depth++;
|
|
1158
|
-
} else if (c === ")") {
|
|
1159
|
-
depth--;
|
|
1160
|
-
if (depth === 0) return i + 1;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
return -1;
|
|
1164
|
-
}
|
|
1165
|
-
function extractDefineFuns(output) {
|
|
1166
|
-
const defs = [];
|
|
1167
|
-
let from = 0;
|
|
1168
|
-
for (; ; ) {
|
|
1169
|
-
const pos = output.indexOf("(define-fun", from);
|
|
1170
|
-
if (pos < 0) break;
|
|
1171
|
-
const end = sexprEnd(output, pos);
|
|
1172
|
-
if (end < 0) break;
|
|
1173
|
-
defs.push(output.slice(pos, end));
|
|
1174
|
-
from = end;
|
|
1175
|
-
}
|
|
1176
|
-
return defs;
|
|
1177
|
-
}
|
|
1178
|
-
function extractInvariant(output) {
|
|
1179
|
-
const defs = extractDefineFuns(output);
|
|
1180
|
-
return defs.length === 0 ? null : defs.join("\n");
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// src/verification/z3/z3-process.ts
|
|
1184
|
-
var Z3_ENV = "LIBPETRI_Z3";
|
|
1185
|
-
var DUMP_ENV = "LIBPETRI_SMT_DUMP";
|
|
1186
|
-
var GRACE_MS = 1e3;
|
|
1187
|
-
var VERSION_PROBE_MS = 5e3;
|
|
1188
|
-
var MIN_Z3_VERSION = { major: 4, minor: 8, patch: 0 };
|
|
1189
|
-
function parseZ3Version(text) {
|
|
1190
|
-
const m = /Z3 version (\d+)\.(\d+)(?:\.(\d+))?/.exec(text);
|
|
1191
|
-
if (m == null) return null;
|
|
1192
|
-
return { major: Number(m[1]), minor: Number(m[2]), patch: m[3] == null ? 0 : Number(m[3]) };
|
|
1193
|
-
}
|
|
1194
|
-
function formatZ3Version(v) {
|
|
1195
|
-
return `${v.major}.${v.minor}.${v.patch}`;
|
|
1196
|
-
}
|
|
1197
|
-
function compareZ3Version(a, b) {
|
|
1198
|
-
return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
|
|
1199
|
-
}
|
|
1200
|
-
var Z3Unavailable = class extends Error {
|
|
1201
|
-
constructor(message) {
|
|
1202
|
-
super(message);
|
|
1203
|
-
this.name = "Z3Unavailable";
|
|
1204
|
-
}
|
|
1205
|
-
};
|
|
1206
|
-
var Z3ProcessError = class extends Error {
|
|
1207
|
-
constructor(message) {
|
|
1208
|
-
super(message);
|
|
1209
|
-
this.name = "Z3ProcessError";
|
|
1210
|
-
}
|
|
1211
|
-
};
|
|
1212
|
-
function replySucceeded(reply) {
|
|
1213
|
-
return reply.exit.kind === "exited" && reply.exit.code === 0;
|
|
1214
|
-
}
|
|
1215
|
-
function argsFor(timeoutMs) {
|
|
1216
|
-
return ["-smt2", "-in", `-t:${timeoutMs}`, `-T:${hardTimeoutSecs(timeoutMs)}`];
|
|
1217
|
-
}
|
|
1218
|
-
function hardTimeoutSecs(timeoutMs) {
|
|
1219
|
-
return Math.max(1, Math.ceil((timeoutMs + GRACE_MS) / 1e3));
|
|
1220
|
-
}
|
|
1221
|
-
function watchdogMs(timeoutMs) {
|
|
1222
|
-
return timeoutMs + 2 * GRACE_MS;
|
|
1223
|
-
}
|
|
1224
|
-
function timeoutBudget(timeoutMs) {
|
|
1225
|
-
return Math.max(1, Math.floor(Number.isFinite(timeoutMs) ? timeoutMs : 1));
|
|
1226
|
-
}
|
|
1227
|
-
function failureReason(reply, timeoutMs) {
|
|
1228
|
-
if (timeoutLine(reply.stdout)) {
|
|
1229
|
-
return `z3 hard timeout after ${hardTimeoutSecs(timeoutMs)}s`;
|
|
1230
|
-
}
|
|
1231
|
-
if (reply.exit.kind === "killed") {
|
|
1232
|
-
return `z3 did not exit within ${watchdogMs(timeoutMs)} ms and was killed`;
|
|
1233
|
-
}
|
|
1234
|
-
const err = errorLine(reply.stdout) ?? errorLine(reply.stderr);
|
|
1235
|
-
if (err != null) return `Z3 error: ${err}`;
|
|
1236
|
-
const stderr = reply.stderr.trim();
|
|
1237
|
-
if (stderr !== "") return `Z3 error: ${stderr}`;
|
|
1238
|
-
return `Unexpected Z3 output: ${reply.stdout.trim()}`;
|
|
1239
|
-
}
|
|
1240
|
-
function locateZ3(program, env = process.env) {
|
|
1241
|
-
const isFile = (p) => {
|
|
1242
|
-
try {
|
|
1243
|
-
return existsSync(p) && statSync(p).isFile();
|
|
1244
|
-
} catch {
|
|
1245
|
-
return false;
|
|
1246
|
-
}
|
|
1247
|
-
};
|
|
1248
|
-
if (program.includes("/") || program.includes(path.sep) || path.isAbsolute(program)) {
|
|
1249
|
-
return isFile(program) ? program : null;
|
|
1250
|
-
}
|
|
1251
|
-
const searchPath = env["PATH"] ?? "";
|
|
1252
|
-
const windows = process.platform === "win32";
|
|
1253
|
-
for (const dir of searchPath.split(path.delimiter)) {
|
|
1254
|
-
if (dir === "") continue;
|
|
1255
|
-
const candidate2 = path.join(dir, program);
|
|
1256
|
-
if (isFile(candidate2)) return candidate2;
|
|
1257
|
-
if (windows && isFile(candidate2 + ".exe")) return candidate2 + ".exe";
|
|
1258
|
-
}
|
|
1259
|
-
return null;
|
|
1260
|
-
}
|
|
1261
|
-
function z3SolverAt(program, env = process.env) {
|
|
1262
|
-
const located = locateZ3(program, env);
|
|
1263
|
-
if (located == null) {
|
|
1264
|
-
throw new Z3Unavailable(
|
|
1265
|
-
`z3 binary not found: ${program}; install z3 >= ${formatZ3Version(MIN_Z3_VERSION)} or set ${Z3_ENV}`
|
|
1266
|
-
);
|
|
1267
|
-
}
|
|
1268
|
-
const probe = spawnSync(located, ["--version"], {
|
|
1269
|
-
encoding: "utf8",
|
|
1270
|
-
timeout: VERSION_PROBE_MS,
|
|
1271
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1272
|
-
});
|
|
1273
|
-
if (probe.error != null) {
|
|
1274
|
-
if (probe.error.code === "ETIMEDOUT") {
|
|
1275
|
-
throw new Z3Unavailable(`${program} --version did not answer within ${VERSION_PROBE_MS} ms`);
|
|
1276
|
-
}
|
|
1277
|
-
throw new Z3Unavailable(`failed to spawn ${program}: ${probe.error.message}`);
|
|
1278
|
-
}
|
|
1279
|
-
const version = parseZ3Version(probe.stdout ?? "");
|
|
1280
|
-
if (version == null) {
|
|
1281
|
-
const line = `${probe.stdout ?? ""}
|
|
1282
|
-
${probe.stderr ?? ""}`.split("\n").map((l) => l.trim()).find((l) => l !== "") ?? "";
|
|
1283
|
-
throw new Z3Unavailable(`z3 --version did not report a version: ${line}`);
|
|
1284
|
-
}
|
|
1285
|
-
if (compareZ3Version(version, MIN_Z3_VERSION) < 0) {
|
|
1286
|
-
throw new Z3Unavailable(
|
|
1287
|
-
`z3 ${formatZ3Version(version)} is older than the minimum ${formatZ3Version(MIN_Z3_VERSION)}`
|
|
1288
|
-
);
|
|
1289
|
-
}
|
|
1290
|
-
return { program: located, version, dumpDir: null };
|
|
1291
|
-
}
|
|
1292
|
-
function resolveZ3(env = process.env) {
|
|
1293
|
-
const configured = env[Z3_ENV];
|
|
1294
|
-
const program = configured == null || configured.trim() === "" ? "z3" : configured;
|
|
1295
|
-
const dump = env[DUMP_ENV];
|
|
1296
|
-
const solver = z3SolverAt(program, env);
|
|
1297
|
-
return { ...solver, dumpDir: dump == null || dump.trim() === "" ? null : dump };
|
|
1298
|
-
}
|
|
1299
|
-
function z3Available(env = process.env) {
|
|
1300
|
-
try {
|
|
1301
|
-
resolveZ3(env);
|
|
1302
|
-
return true;
|
|
1303
|
-
} catch {
|
|
1304
|
-
return false;
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
var dumpCounter = 0;
|
|
1308
|
-
function dumpSlot(solver, phase, script2) {
|
|
1309
|
-
if (solver.dumpDir == null) return null;
|
|
1310
|
-
dumpCounter += 1;
|
|
1311
|
-
try {
|
|
1312
|
-
mkdirSync(solver.dumpDir, { recursive: true });
|
|
1313
|
-
const base = path.join(solver.dumpDir, `${String(dumpCounter).padStart(3, "0")}-${phase}`);
|
|
1314
|
-
writeFileSync(`${base}.smt2`, script2);
|
|
1315
|
-
return base;
|
|
1316
|
-
} catch {
|
|
1317
|
-
return null;
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
function dumpWrite(file, text) {
|
|
1321
|
-
try {
|
|
1322
|
-
writeFileSync(file, text);
|
|
1323
|
-
} catch {
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
function runZ3Text(solver, script2, phase, timeoutMs, extraArgs = []) {
|
|
1327
|
-
const budget = timeoutBudget(timeoutMs);
|
|
1328
|
-
const base = dumpSlot(solver, phase, script2);
|
|
1329
|
-
return new Promise((resolve, reject) => {
|
|
1330
|
-
const child = spawn(solver.program, [...argsFor(budget), ...extraArgs], {
|
|
1331
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1332
|
-
});
|
|
1333
|
-
const out = [];
|
|
1334
|
-
const err = [];
|
|
1335
|
-
let killed = false;
|
|
1336
|
-
let settled = false;
|
|
1337
|
-
child.stdout.on("data", (chunk) => out.push(chunk));
|
|
1338
|
-
child.stderr.on("data", (chunk) => err.push(chunk));
|
|
1339
|
-
child.stdin.on("error", () => {
|
|
1340
|
-
});
|
|
1341
|
-
const watchdog = setTimeout(() => {
|
|
1342
|
-
killed = true;
|
|
1343
|
-
child.kill("SIGKILL");
|
|
1344
|
-
}, watchdogMs(budget));
|
|
1345
|
-
child.on("error", (e) => {
|
|
1346
|
-
if (settled) return;
|
|
1347
|
-
settled = true;
|
|
1348
|
-
clearTimeout(watchdog);
|
|
1349
|
-
reject(new Z3ProcessError(`failed to spawn ${solver.program}: ${e.message}`));
|
|
1350
|
-
});
|
|
1351
|
-
child.on("close", (code) => {
|
|
1352
|
-
if (settled) return;
|
|
1353
|
-
settled = true;
|
|
1354
|
-
clearTimeout(watchdog);
|
|
1355
|
-
const reply = {
|
|
1356
|
-
stdout: Buffer.concat(out).toString("utf8"),
|
|
1357
|
-
stderr: Buffer.concat(err).toString("utf8"),
|
|
1358
|
-
exit: killed ? { kind: "killed" } : { kind: "exited", code }
|
|
1359
|
-
};
|
|
1360
|
-
if (base != null) {
|
|
1361
|
-
dumpWrite(`${base}.out`, reply.stdout);
|
|
1362
|
-
if (reply.stderr.trim() !== "") dumpWrite(`${base}.err`, reply.stderr);
|
|
1363
|
-
}
|
|
1364
|
-
resolve(reply);
|
|
1365
|
-
});
|
|
1366
|
-
child.stdin.end(script2);
|
|
1367
|
-
});
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
// src/verification/z3/spacer-runner.ts
|
|
1371
|
-
async function runZ3Spacer(solver, timeoutMs, smt2, phase) {
|
|
1372
|
-
let reply;
|
|
1373
|
-
try {
|
|
1374
|
-
reply = await runZ3Text(solver, smt2, phase, timeoutMs, ["fp.engine=spacer"]);
|
|
1375
|
-
} catch (e) {
|
|
1376
|
-
return { type: "unknown", reason: String(e?.message ?? e) };
|
|
1377
|
-
}
|
|
1378
|
-
const stdout = reply.stdout.trim();
|
|
1379
|
-
switch (classifyFirstLine(stdout)) {
|
|
1380
|
-
// unsat => no inductive invariant excludes the bad state => VIOLATED.
|
|
1381
|
-
case "unsat":
|
|
1382
|
-
return { type: "violated", answer: stdout };
|
|
1383
|
-
// sat => an inductive invariant exists => PROVEN.
|
|
1384
|
-
case "sat":
|
|
1385
|
-
return { type: "proven", invariantFormula: extractInvariant(stdout) };
|
|
1386
|
-
case "unknown":
|
|
1387
|
-
return { type: "unknown", reason: "Z3 answered unknown" };
|
|
1388
|
-
default:
|
|
1389
|
-
return { type: "unknown", reason: failureReason(reply, timeoutBudget(timeoutMs)) };
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
// src/verification/z3/smt-encoder.ts
|
|
1394
|
-
function encode(flatNet, initialMarking, property, invariants, sinkPlaces = /* @__PURE__ */ new Set(), produceProofs = false) {
|
|
1395
|
-
const P = flatNet.places.length;
|
|
1396
|
-
const lines = [];
|
|
1397
|
-
const envInject = resolveEnvInjection(flatNet);
|
|
1398
|
-
if (produceProofs) lines.push("(set-option :produce-proofs true)");
|
|
1399
|
-
lines.push("(set-logic HORN)");
|
|
1400
|
-
lines.push("");
|
|
1401
|
-
lines.push(`(declare-fun Reachable (${ints(P).join(" ")}) Bool)`);
|
|
1402
|
-
lines.push("(declare-fun Error () Bool)");
|
|
1403
|
-
lines.push("");
|
|
1404
|
-
const mVars = vars(P, "");
|
|
1405
|
-
const mpVars = vars(P, "p");
|
|
1406
|
-
const m0 = [];
|
|
1407
|
-
for (let i = 0; i < P; i++) m0.push(String(initialMarking.tokens(flatNet.places[i])));
|
|
1408
|
-
lines.push(`(assert (Reachable ${m0.join(" ")}))`);
|
|
1409
|
-
lines.push("");
|
|
1410
|
-
for (const ft of flatNet.transitions) {
|
|
1411
|
-
lines.push(encodeTransitionRule(flatNet, ft, mVars, mpVars, invariants));
|
|
1412
|
-
}
|
|
1413
|
-
for (const inj of envInject) {
|
|
1414
|
-
lines.push(encodeInjectionRule(P, inj.pid, inj.bound, mVars, mpVars));
|
|
1415
|
-
}
|
|
1416
|
-
lines.push("");
|
|
1417
|
-
lines.push(encodeErrorRule(flatNet, property, mVars, sinkPlaces, envInject));
|
|
1418
|
-
lines.push("");
|
|
1419
|
-
lines.push("(assert (not Error))");
|
|
1420
|
-
lines.push("(check-sat)");
|
|
1421
|
-
if (produceProofs) lines.push("(get-proof)");
|
|
1422
|
-
lines.push("(get-model)");
|
|
1423
|
-
return { smt2: lines.join("\n"), placeCount: P };
|
|
1424
|
-
}
|
|
1425
|
-
function resolveEnvInjection(flatNet) {
|
|
1426
|
-
const out = [];
|
|
1427
|
-
for (const [name, bound] of flatNet.environmentInjection) {
|
|
1428
|
-
const pid = flatNet.placeIndex.get(name);
|
|
1429
|
-
if (pid != null) out.push({ pid, bound });
|
|
1430
|
-
}
|
|
1431
|
-
out.sort((a, b) => a.pid - b.pid);
|
|
1432
|
-
return out;
|
|
1433
|
-
}
|
|
1434
|
-
function envBounds(flatNet) {
|
|
1435
|
-
const out = [];
|
|
1436
|
-
for (const [name, max] of flatNet.environmentBounds) {
|
|
1437
|
-
const pid = flatNet.placeIndex.get(name);
|
|
1438
|
-
if (pid != null) out.push([pid, max]);
|
|
1439
|
-
}
|
|
1440
|
-
out.sort((a, b) => a[0] - b[0]);
|
|
1441
|
-
return out;
|
|
1442
|
-
}
|
|
1443
|
-
function ints(n) {
|
|
1444
|
-
return new Array(n).fill("Int");
|
|
1445
|
-
}
|
|
1446
|
-
function vars(P, suffix) {
|
|
1447
|
-
const out = [];
|
|
1448
|
-
for (let i = 0; i < P; i++) out.push(`m${i}${suffix}`);
|
|
1449
|
-
return out;
|
|
1450
|
-
}
|
|
1451
|
-
function quantified(names) {
|
|
1452
|
-
return names.map((v) => `(${v} Int)`).join(" ");
|
|
1453
|
-
}
|
|
1454
|
-
function firingConditions(flatNet, ft, mVars, mpVars) {
|
|
1455
|
-
const P = flatNet.places.length;
|
|
1456
|
-
const conditions = [];
|
|
1457
|
-
for (let i = 0; i < P; i++) {
|
|
1458
|
-
if (ft.preVector[i] > 0) conditions.push(`(>= ${mVars[i]} ${ft.preVector[i]})`);
|
|
1459
|
-
}
|
|
1460
|
-
for (const inh of ft.inhibitorPlaces) conditions.push(`(= ${mVars[inh]} 0)`);
|
|
1461
|
-
for (const rd of ft.readPlaces) conditions.push(`(>= ${mVars[rd]} 1)`);
|
|
1462
|
-
for (let i = 0; i < P; i++) {
|
|
1463
|
-
if (ft.resetPlaces.includes(i) || ft.consumeAll[i]) {
|
|
1464
|
-
conditions.push(`(= ${mpVars[i]} ${ft.postVector[i]})`);
|
|
1465
|
-
} else {
|
|
1466
|
-
const delta = ft.postVector[i] - ft.preVector[i];
|
|
1467
|
-
if (delta > 0) conditions.push(`(= ${mpVars[i]} (+ ${mVars[i]} ${delta}))`);
|
|
1468
|
-
else if (delta < 0) conditions.push(`(= ${mpVars[i]} (- ${mVars[i]} ${-delta}))`);
|
|
1469
|
-
else conditions.push(`(= ${mpVars[i]} ${mVars[i]})`);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
for (let i = 0; i < P; i++) conditions.push(`(>= ${mpVars[i]} 0)`);
|
|
1473
|
-
return conditions;
|
|
1474
|
-
}
|
|
1475
|
-
function invariantConditions(invariants, names) {
|
|
1476
|
-
const conditions = [];
|
|
1477
|
-
for (const inv of invariants) {
|
|
1478
|
-
const terms = [...inv.support].sort((a, b) => a - b).map((i) => `(* ${inv.weights[i]} ${names[i]})`);
|
|
1479
|
-
if (terms.length === 0) continue;
|
|
1480
|
-
const sum = terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
|
|
1481
|
-
conditions.push(`(= ${sum} ${inv.constant})`);
|
|
1482
|
-
}
|
|
1483
|
-
return conditions;
|
|
1484
|
-
}
|
|
1485
|
-
function envBoundConditions(flatNet, mpVars) {
|
|
1486
|
-
return envBounds(flatNet).map(([pid, max]) => `(<= ${mpVars[pid]} ${max})`);
|
|
1487
|
-
}
|
|
1488
|
-
function injectionConditions(P, pid, bound, mVars, mpVars) {
|
|
1489
|
-
const conditions = [];
|
|
1490
|
-
if (bound != null) conditions.push(`(< ${mVars[pid]} ${bound})`);
|
|
1491
|
-
for (let i = 0; i < P; i++) {
|
|
1492
|
-
if (i === pid) conditions.push(`(= ${mpVars[i]} (+ ${mVars[i]} 1))`);
|
|
1493
|
-
else conditions.push(`(= ${mpVars[i]} ${mVars[i]})`);
|
|
1494
|
-
}
|
|
1495
|
-
return conditions;
|
|
1496
|
-
}
|
|
1497
|
-
function encodeTransitionRule(flatNet, ft, mVars, mpVars, invariants) {
|
|
1498
|
-
const conditions = [`(Reachable ${mVars.join(" ")})`];
|
|
1499
|
-
conditions.push(...firingConditions(flatNet, ft, mVars, mpVars));
|
|
1500
|
-
conditions.push(...invariantConditions(invariants, mpVars));
|
|
1501
|
-
conditions.push(...envBoundConditions(flatNet, mpVars));
|
|
1502
|
-
const body = `(and ${conditions.join("\n ")})`;
|
|
1503
|
-
return `(assert (forall (${quantified([...mVars, ...mpVars])})
|
|
1504
|
-
(=> ${body}
|
|
1505
|
-
(Reachable ${mpVars.join(" ")}))))`;
|
|
1506
|
-
}
|
|
1507
|
-
function encodeInjectionRule(P, pid, bound, mVars, mpVars) {
|
|
1508
|
-
const conditions = [`(Reachable ${mVars.join(" ")})`];
|
|
1509
|
-
conditions.push(...injectionConditions(P, pid, bound, mVars, mpVars));
|
|
1510
|
-
const body = `(and ${conditions.join("\n ")})`;
|
|
1511
|
-
return `(assert (forall (${quantified([...mVars, ...mpVars])})
|
|
1512
|
-
(=> ${body}
|
|
1513
|
-
(Reachable ${mpVars.join(" ")}))))`;
|
|
1514
|
-
}
|
|
1515
|
-
function conjoin(conditions) {
|
|
1516
|
-
if (conditions.length === 0) return "true";
|
|
1517
|
-
if (conditions.length === 1) return conditions[0];
|
|
1518
|
-
return `(and ${conditions.join(" ")})`;
|
|
1519
|
-
}
|
|
1520
|
-
function encodeStepRelationSmt2(flatNet) {
|
|
1521
|
-
const P = flatNet.places.length;
|
|
1522
|
-
const mVars = vars(P, "");
|
|
1523
|
-
const mpVars = vars(P, "p");
|
|
1524
|
-
const disjuncts = [];
|
|
1525
|
-
for (const ft of flatNet.transitions) {
|
|
1526
|
-
const conditions = firingConditions(flatNet, ft, mVars, mpVars);
|
|
1527
|
-
conditions.push(...envBoundConditions(flatNet, mpVars));
|
|
1528
|
-
disjuncts.push(conjoin(conditions));
|
|
1529
|
-
}
|
|
1530
|
-
for (const inj of resolveEnvInjection(flatNet)) {
|
|
1531
|
-
disjuncts.push(conjoin(injectionConditions(P, inj.pid, inj.bound, mVars, mpVars)));
|
|
1532
|
-
}
|
|
1533
|
-
if (disjuncts.length === 0) return "false";
|
|
1534
|
-
if (disjuncts.length === 1) return disjuncts[0];
|
|
1535
|
-
return `(or ${disjuncts.join("\n ")})`;
|
|
1536
|
-
}
|
|
1537
|
-
function encodeErrorRule(flatNet, property, mVars, sinkPlaces, envInject) {
|
|
1538
|
-
const violation = encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject);
|
|
1539
|
-
return `(assert (forall (${quantified(mVars)})
|
|
1540
|
-
(=> (and (Reachable ${mVars.join(" ")}) ${violation})
|
|
1541
|
-
Error)))`;
|
|
1542
|
-
}
|
|
1543
|
-
function indexOrdered(flatNet, places) {
|
|
1544
|
-
const idx = /* @__PURE__ */ new Set();
|
|
1545
|
-
for (const place of places) {
|
|
1546
|
-
const i = flatNet.placeIndex.get(place.name);
|
|
1547
|
-
if (i != null) idx.add(i);
|
|
1548
|
-
}
|
|
1549
|
-
return [...idx].sort((a, b) => a - b);
|
|
1550
|
-
}
|
|
1551
|
-
function encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject) {
|
|
1552
|
-
switch (property.type) {
|
|
1553
|
-
// DeadlockFree (VER-002): a quiescent marking that STRANDS a token — holds one
|
|
1554
|
-
// in a place that is not a declared sink. The empty marking strands nothing and
|
|
1555
|
-
// is therefore not a violation (AC4).
|
|
1556
|
-
case "deadlock-free": {
|
|
1557
|
-
const conditions = encodeQuiescent(flatNet, mVars, envInject);
|
|
1558
|
-
if (conditions == null) return "false";
|
|
1559
|
-
const sinks = new Set(indexOrdered(flatNet, sinkPlaces));
|
|
1560
|
-
const stranded = [];
|
|
1561
|
-
for (let pid = 0; pid < flatNet.places.length; pid++) {
|
|
1562
|
-
if (!sinks.has(pid)) stranded.push(`(>= ${mVars[pid]} 1)`);
|
|
1563
|
-
}
|
|
1564
|
-
if (stranded.length === 0) return "false";
|
|
1565
|
-
conditions.push(`(or ${stranded.join(" ")})`);
|
|
1566
|
-
return joinConditions(conditions);
|
|
1567
|
-
}
|
|
1568
|
-
// TerminatesAtSink (VER-002): a quiescent marking that reached NO declared sink.
|
|
1569
|
-
// This is the predicate DeadlockFree carried before the VER-002 split, unchanged.
|
|
1570
|
-
case "terminates-at-sink": {
|
|
1571
|
-
const conditions = encodeQuiescent(flatNet, mVars, envInject);
|
|
1572
|
-
if (conditions == null) return "false";
|
|
1573
|
-
for (const pid of indexOrdered(flatNet, sinkPlaces)) {
|
|
1574
|
-
conditions.push(`(= ${mVars[pid]} 0)`);
|
|
1575
|
-
}
|
|
1576
|
-
return joinConditions(conditions);
|
|
1577
|
-
}
|
|
1578
|
-
case "mutual-exclusion": {
|
|
1579
|
-
const conditions = indexOrdered(flatNet, [property.p1, property.p2]).map((i) => `(>= ${mVars[i]} 1)`);
|
|
1580
|
-
return conditions.length === 0 ? "false" : `(and ${conditions.join(" ")})`;
|
|
1581
|
-
}
|
|
1582
|
-
case "place-bound":
|
|
1583
|
-
case "branch-place-bound": {
|
|
1584
|
-
const pid = flatNet.placeIndex.get(property.place.name);
|
|
1585
|
-
return pid == null ? "false" : `(> ${mVars[pid]} ${property.bound})`;
|
|
1586
|
-
}
|
|
1587
|
-
case "unreachable": {
|
|
1588
|
-
const conditions = indexOrdered(flatNet, property.places).map((i) => `(>= ${mVars[i]} 1)`);
|
|
1589
|
-
return conditions.length === 0 ? "false" : `(and ${conditions.join(" ")})`;
|
|
1590
|
-
}
|
|
1591
|
-
// JoinedOrDeadLettered (NU-040 AC4): a quiescent state that still holds a
|
|
1592
|
-
// `pending` token is a stranded correlation group. Carries NO sink clause — a
|
|
1593
|
-
// declared sink must not excuse a stranded group.
|
|
1594
|
-
case "joined-or-dead-lettered": {
|
|
1595
|
-
const pid = flatNet.placeIndex.get(property.pending.name);
|
|
1596
|
-
if (pid == null) return "false";
|
|
1597
|
-
const conditions = encodeQuiescent(flatNet, mVars, envInject);
|
|
1598
|
-
if (conditions == null) return "false";
|
|
1599
|
-
conditions.push(`(>= ${mVars[pid]} 1)`);
|
|
1600
|
-
return joinConditions(conditions);
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
function joinConditions(conditions) {
|
|
1605
|
-
return conditions.length === 0 ? "true" : `(and ${conditions.join("\n ")})`;
|
|
1606
|
-
}
|
|
1607
|
-
function encodeQuiescent(flatNet, mVars, envInject) {
|
|
1608
|
-
const envBound = /* @__PURE__ */ new Map();
|
|
1609
|
-
for (const inj of envInject) envBound.set(inj.pid, inj.bound);
|
|
1610
|
-
const disabledConditions = [];
|
|
1611
|
-
for (const ft of flatNet.transitions) {
|
|
1612
|
-
const disableReasons = [];
|
|
1613
|
-
let permanentlyDisabled = false;
|
|
1614
|
-
for (let i = 0; i < flatNet.places.length; i++) {
|
|
1615
|
-
if (ft.preVector[i] > 0) {
|
|
1616
|
-
if (envBound.has(i)) {
|
|
1617
|
-
const k = envBound.get(i);
|
|
1618
|
-
if (k != null && ft.preVector[i] > k) permanentlyDisabled = true;
|
|
1619
|
-
continue;
|
|
1620
|
-
}
|
|
1621
|
-
disableReasons.push(`(< ${mVars[i]} ${ft.preVector[i]})`);
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
for (const inh of ft.inhibitorPlaces) disableReasons.push(`(> ${mVars[inh]} 0)`);
|
|
1625
|
-
for (const rd of ft.readPlaces) {
|
|
1626
|
-
if (envBound.has(rd)) {
|
|
1627
|
-
const k = envBound.get(rd);
|
|
1628
|
-
if (k != null && k < 1) permanentlyDisabled = true;
|
|
1629
|
-
continue;
|
|
1630
|
-
}
|
|
1631
|
-
disableReasons.push(`(< ${mVars[rd]} 1)`);
|
|
1632
|
-
}
|
|
1633
|
-
if (permanentlyDisabled) {
|
|
1634
|
-
disabledConditions.push("true");
|
|
1635
|
-
continue;
|
|
1636
|
-
}
|
|
1637
|
-
if (disableReasons.length === 0) return null;
|
|
1638
|
-
disabledConditions.push(`(or ${disableReasons.join(" ")})`);
|
|
1639
|
-
}
|
|
1640
|
-
return disabledConditions;
|
|
1641
|
-
}
|
|
1642
|
-
function injectionMap(flatNet) {
|
|
1643
|
-
const out = /* @__PURE__ */ new Map();
|
|
1644
|
-
for (const inj of resolveEnvInjection(flatNet)) out.set(inj.pid, inj.bound);
|
|
1645
|
-
return out;
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
// src/verification/z3/certificate-checker.ts
|
|
1649
|
-
var VC_LABELS = ["initiation (VC1)", "consecution (VC2)", "safety (VC3)"];
|
|
1650
|
-
async function checkCertificate(certificate, flatNet, initialMarking, property, invariants, sinkPlaces, solver, timeoutMs) {
|
|
1651
|
-
if (certificate == null) {
|
|
1652
|
-
return {
|
|
1653
|
-
type: "unavailable",
|
|
1654
|
-
reason: "no inductive invariant (define-fun block) could be extracted from the z3 model",
|
|
1655
|
-
invariant: null
|
|
1656
|
-
};
|
|
1657
|
-
}
|
|
1658
|
-
const shape = shapeFailure(flatNet, invariants);
|
|
1659
|
-
if (shape != null) return { type: "unavailable", reason: shape, invariant: certificate };
|
|
1660
|
-
if (!certificate.includes("(define-fun Reachable ") && !certificate.includes("(define-fun |Reachable| ")) {
|
|
1661
|
-
return { type: "unavailable", reason: "certificate does not define Reachable", invariant: certificate };
|
|
1662
|
-
}
|
|
1663
|
-
const vcs = buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants);
|
|
1664
|
-
let results;
|
|
1665
|
-
try {
|
|
1666
|
-
results = await runVcScript(script(vcs), timeoutMs, solver);
|
|
1667
|
-
} catch (e) {
|
|
1668
|
-
return { type: "unavailable", reason: String(e?.message ?? e), invariant: certificate };
|
|
1669
|
-
}
|
|
1670
|
-
for (let i = 0; i < results.length; i++) {
|
|
1671
|
-
if (results[i] !== "unsat") {
|
|
1672
|
-
const detail = await detailFor(vcs, i, results[i], flatNet, timeoutMs, solver);
|
|
1673
|
-
return { type: "failed", vc: VC_LABELS[i], detail, invariant: certificate };
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
return { type: "passed", invariant: certificate };
|
|
1677
|
-
}
|
|
1678
|
-
function vcScript(certificate, flatNet, initialMarking, property, sinkPlaces, invariants) {
|
|
1679
|
-
return script(buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants));
|
|
1680
|
-
}
|
|
1681
|
-
function shapeFailure(flatNet, invariants) {
|
|
1682
|
-
const P = flatNet.places.length;
|
|
1683
|
-
for (const inv of invariants) {
|
|
1684
|
-
if (inv.weights.length !== P) {
|
|
1685
|
-
return `P-invariant has ${inv.weights.length} weights for a ${P}-place net`;
|
|
1686
|
-
}
|
|
1687
|
-
for (const pid of inv.support) {
|
|
1688
|
-
if (pid >= P || pid < 0) return `P-invariant support names place index ${pid} in a ${P}-place net`;
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
return null;
|
|
1692
|
-
}
|
|
1693
|
-
var VcFailure = class extends Error {
|
|
1694
|
-
};
|
|
1695
|
-
async function runVcScript(text, timeoutMs, solver) {
|
|
1696
|
-
const reply = await runZ3Text(solver, text, "certificate", timeoutMs, []);
|
|
1697
|
-
const budget = timeoutBudget(timeoutMs);
|
|
1698
|
-
const err = errorLine(reply.stderr);
|
|
1699
|
-
if (err != null) throw new VcFailure(`z3 reported an error on stderr: ${err}`);
|
|
1700
|
-
if (timeoutLine(reply.stdout)) {
|
|
1701
|
-
throw new VcFailure(`z3 hard timeout after ${hardTimeoutSecs(budget)}s while checking the certificate`);
|
|
1702
|
-
}
|
|
1703
|
-
if (reply.exit.kind === "killed") {
|
|
1704
|
-
throw new VcFailure(`z3 did not exit within ${watchdogMs(budget)} ms while checking the certificate and was killed`);
|
|
1705
|
-
}
|
|
1706
|
-
const results = parseVcResults(reply.stdout);
|
|
1707
|
-
if (!replySucceeded(reply)) {
|
|
1708
|
-
const status = reply.exit.kind === "exited" ? `exit status: ${reply.exit.code}` : "the watchdog kill";
|
|
1709
|
-
throw new VcFailure(`z3 exited with ${status} after answering [${results.join(", ")}]`);
|
|
1710
|
-
}
|
|
1711
|
-
return results;
|
|
1712
|
-
}
|
|
1713
|
-
function parseVcResults(stdout) {
|
|
1714
|
-
const err = errorLine(stdout);
|
|
1715
|
-
if (err != null) throw new VcFailure(`z3 error while checking the certificate: ${err}`);
|
|
1716
|
-
if (timeoutLine(stdout)) throw new VcFailure("z3 hard timeout while checking the certificate");
|
|
1717
|
-
const results = stdout.split("\n").map((l) => l.trim()).filter((l) => l === "sat" || l === "unsat" || l === "unknown");
|
|
1718
|
-
if (results.length !== 3) {
|
|
1719
|
-
throw new VcFailure(`expected 3 VC answers from z3, got ${results.length}: [${results.join(", ")}]`);
|
|
1720
|
-
}
|
|
1721
|
-
return results;
|
|
1722
|
-
}
|
|
1723
|
-
function buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants) {
|
|
1724
|
-
const P = flatNet.places.length;
|
|
1725
|
-
const mVars = [];
|
|
1726
|
-
const mpVars = [];
|
|
1727
|
-
for (let i = 0; i < P; i++) {
|
|
1728
|
-
mVars.push(`m${i}`);
|
|
1729
|
-
mpVars.push(`m${i}p`);
|
|
1730
|
-
}
|
|
1731
|
-
const prelude = [
|
|
1732
|
-
"; IC3/PDR certificate check (plain SMT-LIB2, not HORN):",
|
|
1733
|
-
"; each VC below must be unsat for the certificate to stand.",
|
|
1734
|
-
certificate,
|
|
1735
|
-
""
|
|
1736
|
-
];
|
|
1737
|
-
for (const v of mVars) prelude.push(`(declare-const ${v} Int)`);
|
|
1738
|
-
for (const v of mpVars) prelude.push(`(declare-const ${v} Int)`);
|
|
1739
|
-
const m0 = [];
|
|
1740
|
-
for (let i = 0; i < P; i++) m0.push(String(initialMarking.tokens(flatNet.places[i])));
|
|
1741
|
-
const vc1 = [`(assert (not ${candidate(m0, invariants)}))`];
|
|
1742
|
-
const nonNegative = mVars.map((v) => `(assert (>= ${v} 0))`);
|
|
1743
|
-
const step = encodeStepRelationSmt2(flatNet);
|
|
1744
|
-
const vc2 = [
|
|
1745
|
-
...nonNegative,
|
|
1746
|
-
`(assert ${candidate(mVars, invariants)})`,
|
|
1747
|
-
`(assert ${step})`,
|
|
1748
|
-
`(assert (not ${candidate(mpVars, invariants)}))`
|
|
1749
|
-
];
|
|
1750
|
-
const bad = encodePropertyViolation(flatNet, property, mVars, sinkPlaces, resolveEnvInjection(flatNet));
|
|
1751
|
-
const vc3 = [...nonNegative, `(assert ${candidate(mVars, invariants)})`, `(assert ${bad})`];
|
|
1752
|
-
return { prelude, asserts: [vc1, vc2, vc3] };
|
|
1753
|
-
}
|
|
1754
|
-
function script(vcs) {
|
|
1755
|
-
const lines = [...vcs.prelude];
|
|
1756
|
-
for (let i = 0; i < vcs.asserts.length; i++) {
|
|
1757
|
-
lines.push("");
|
|
1758
|
-
lines.push(`; VC${i + 1} ${VC_LABELS[i]}`);
|
|
1759
|
-
lines.push("(push)");
|
|
1760
|
-
lines.push(...vcs.asserts[i]);
|
|
1761
|
-
lines.push("(check-sat)");
|
|
1762
|
-
lines.push("(pop)");
|
|
1763
|
-
}
|
|
1764
|
-
return lines.join("\n");
|
|
1765
|
-
}
|
|
1766
|
-
async function detailFor(vcs, i, answer, flatNet, timeoutMs, solver) {
|
|
1767
|
-
const lines = ["(set-option :produce-models true)", ...vcs.prelude, ...vcs.asserts[i], "(check-sat)"];
|
|
1768
|
-
lines.push(answer === "sat" ? "(get-model)" : "(get-info :reason-unknown)");
|
|
1769
|
-
let reply = "";
|
|
1770
|
-
try {
|
|
1771
|
-
reply = (await runZ3Text(solver, lines.join("\n"), "certificate-detail", timeoutMs, [])).stdout;
|
|
1772
|
-
} catch {
|
|
1773
|
-
reply = "";
|
|
1774
|
-
}
|
|
1775
|
-
if (answer === "sat") {
|
|
1776
|
-
const w = witness(reply, flatNet);
|
|
1777
|
-
return w == null ? "solver returned SATISFIABLE" : `solver returned SATISFIABLE (witness: ${w})`;
|
|
1778
|
-
}
|
|
1779
|
-
const r = reasonUnknown(reply);
|
|
1780
|
-
return r == null ? "solver returned UNKNOWN" : `solver returned UNKNOWN (${r})`;
|
|
1781
|
-
}
|
|
1782
|
-
function witness(model, flatNet) {
|
|
1783
|
-
const parts = [];
|
|
1784
|
-
for (let i = 0; i < flatNet.places.length; i++) {
|
|
1785
|
-
const needle = `(define-fun m${i} () Int`;
|
|
1786
|
-
const at = model.indexOf(needle);
|
|
1787
|
-
if (at < 0) continue;
|
|
1788
|
-
const rest = model.slice(at + needle.length).trimStart();
|
|
1789
|
-
let value;
|
|
1790
|
-
if (rest.startsWith("(")) {
|
|
1791
|
-
const end = sexprEnd(rest, 0);
|
|
1792
|
-
if (end < 0) continue;
|
|
1793
|
-
value = rest.slice(1, end - 1).trim().split(/\s+/).join("");
|
|
1794
|
-
} else {
|
|
1795
|
-
let end = 0;
|
|
1796
|
-
while (end < rest.length && !/\s/.test(rest[end]) && rest[end] !== ")") end++;
|
|
1797
|
-
if (end === 0) continue;
|
|
1798
|
-
value = rest.slice(0, end);
|
|
1799
|
-
}
|
|
1800
|
-
parts.push(`${flatNet.places[i].name}=${value}`);
|
|
1801
|
-
}
|
|
1802
|
-
return parts.length === 0 ? null : parts.join(", ");
|
|
1803
|
-
}
|
|
1804
|
-
function reasonUnknown(reply) {
|
|
1805
|
-
const at = reply.indexOf(":reason-unknown");
|
|
1806
|
-
if (at < 0) return null;
|
|
1807
|
-
const rest = reply.slice(at + ":reason-unknown".length).trimStart();
|
|
1808
|
-
const end = rest.indexOf(")");
|
|
1809
|
-
if (end < 0) return null;
|
|
1810
|
-
let reason = rest.slice(0, end).trim();
|
|
1811
|
-
if (reason.startsWith('"') && reason.endsWith('"') && reason.length >= 2) reason = reason.slice(1, -1);
|
|
1812
|
-
reason = reason.trim();
|
|
1813
|
-
return reason === "" ? null : reason;
|
|
1814
|
-
}
|
|
1815
|
-
function candidate(names, invariants) {
|
|
1816
|
-
return conjoin([`(Reachable ${names.join(" ")})`, ...invariantConditions(invariants, names)]);
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
// src/verification/analysis/dbm.ts
|
|
1820
|
-
var EPSILON = 1e-9;
|
|
1821
|
-
var DBM = class _DBM {
|
|
1822
|
-
bounds;
|
|
1823
|
-
dim;
|
|
1824
|
-
clockNames;
|
|
1825
|
-
_empty;
|
|
1826
|
-
constructor(bounds, dim, clockNames, empty) {
|
|
1827
|
-
this.bounds = bounds;
|
|
1828
|
-
this.dim = dim;
|
|
1829
|
-
this.clockNames = clockNames;
|
|
1830
|
-
this._empty = empty;
|
|
1831
|
-
}
|
|
1832
|
-
/** Creates an initial firing domain for enabled transitions. */
|
|
1833
|
-
static create(clockNames, lowerBounds, upperBounds) {
|
|
1834
|
-
const n = clockNames.length;
|
|
1835
|
-
const dim = n + 1;
|
|
1836
|
-
const bounds = makeMatrix(dim, Infinity);
|
|
1837
|
-
for (let i = 0; i < n; i++) {
|
|
1838
|
-
bounds[0 * dim + (i + 1)] = -lowerBounds[i];
|
|
1839
|
-
bounds[(i + 1) * dim + 0] = upperBounds[i];
|
|
1840
|
-
}
|
|
1841
|
-
return new _DBM(bounds, dim, clockNames, false).canonicalize();
|
|
1842
|
-
}
|
|
1843
|
-
/** Creates an empty (unsatisfiable) zone. */
|
|
1844
|
-
static empty(clockNames) {
|
|
1845
|
-
const b = new Float64Array(1);
|
|
1846
|
-
b[0] = 0;
|
|
1847
|
-
return new _DBM(b, 1, clockNames, true);
|
|
1848
|
-
}
|
|
1849
|
-
isEmpty() {
|
|
1850
|
-
return this._empty;
|
|
1851
|
-
}
|
|
1852
|
-
clockCount() {
|
|
1853
|
-
return this.clockNames.length;
|
|
1854
|
-
}
|
|
1855
|
-
get(i, j) {
|
|
1856
|
-
return this.bounds[i * this.dim + j];
|
|
1857
|
-
}
|
|
1858
|
-
/** Gets the lower bound (earliest firing time) for clock i. */
|
|
1859
|
-
getLowerBound(clockIndex) {
|
|
1860
|
-
if (this._empty || clockIndex < 0 || clockIndex >= this.clockNames.length) return 0;
|
|
1861
|
-
const val = -this.get(0, clockIndex + 1);
|
|
1862
|
-
return val === 0 ? 0 : val;
|
|
1863
|
-
}
|
|
1864
|
-
/** Gets the upper bound (latest firing time / deadline) for clock i. */
|
|
1865
|
-
getUpperBound(clockIndex) {
|
|
1866
|
-
if (this._empty || clockIndex < 0 || clockIndex >= this.clockNames.length) return Infinity;
|
|
1867
|
-
return this.get(clockIndex + 1, 0);
|
|
1868
|
-
}
|
|
1869
|
-
/** Checks if transition can fire (lower bound <= 0 after time passage). */
|
|
1870
|
-
canFire(clockIndex) {
|
|
1871
|
-
return !this._empty && this.getLowerBound(clockIndex) <= EPSILON;
|
|
1872
|
-
}
|
|
1873
|
-
/**
|
|
1874
|
-
* Computes the successor firing domain after firing transition t_f.
|
|
1875
|
-
* Implements the 5-step Berthomieu-Diaz successor formula.
|
|
1876
|
-
*/
|
|
1877
|
-
fireTransition(firedClock, newClockNames, newLowerBounds, newUpperBounds, persistentClocks) {
|
|
1878
|
-
if (this._empty) return this;
|
|
1879
|
-
const n = this.clockNames.length;
|
|
1880
|
-
if (firedClock < 0 || firedClock >= n) {
|
|
1881
|
-
throw new Error(`Invalid fired clock index: ${firedClock}`);
|
|
1882
|
-
}
|
|
1883
|
-
const constrained = new Float64Array(this.bounds);
|
|
1884
|
-
const dim = this.dim;
|
|
1885
|
-
const f = firedClock + 1;
|
|
1886
|
-
for (let i = 0; i < n; i++) {
|
|
1887
|
-
if (i !== firedClock) {
|
|
1888
|
-
const idx = i + 1;
|
|
1889
|
-
const pos = f * dim + idx;
|
|
1890
|
-
constrained[pos] = Math.min(constrained[pos], 0);
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
if (!canonicalizeInPlace(constrained, dim)) {
|
|
1894
|
-
return _DBM.empty([]);
|
|
1895
|
-
}
|
|
1896
|
-
const newN = persistentClocks.length + newClockNames.length;
|
|
1897
|
-
const newDim = newN + 1;
|
|
1898
|
-
const newBounds = makeMatrix(newDim, Infinity);
|
|
1899
|
-
for (let pi = 0; pi < persistentClocks.length; pi++) {
|
|
1900
|
-
const oldIdx = persistentClocks[pi] + 1;
|
|
1901
|
-
const newIdx = pi + 1;
|
|
1902
|
-
const upper = constrained[oldIdx * dim + f];
|
|
1903
|
-
const lower = Math.max(0, -constrained[f * dim + oldIdx]);
|
|
1904
|
-
newBounds[0 * newDim + newIdx] = -lower;
|
|
1905
|
-
newBounds[newIdx * newDim + 0] = upper;
|
|
1906
|
-
for (let pj = 0; pj < persistentClocks.length; pj++) {
|
|
1907
|
-
const oldJ = persistentClocks[pj] + 1;
|
|
1908
|
-
const newJ = pj + 1;
|
|
1909
|
-
newBounds[newIdx * newDim + newJ] = constrained[oldIdx * dim + oldJ];
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
const offset = persistentClocks.length;
|
|
1913
|
-
for (let k = 0; k < newClockNames.length; k++) {
|
|
1914
|
-
const idx = offset + k + 1;
|
|
1915
|
-
newBounds[0 * newDim + idx] = -newLowerBounds[k];
|
|
1916
|
-
newBounds[idx * newDim + 0] = newUpperBounds[k];
|
|
1917
|
-
}
|
|
1918
|
-
const allNames = [];
|
|
1919
|
-
for (const idx of persistentClocks) {
|
|
1920
|
-
allNames.push(this.clockNames[idx]);
|
|
1921
|
-
}
|
|
1922
|
-
allNames.push(...newClockNames);
|
|
1923
|
-
return new _DBM(newBounds, newDim, allNames, false).canonicalize();
|
|
1924
|
-
}
|
|
1925
|
-
/** Lets time pass: set all lower bounds to 0. */
|
|
1926
|
-
letTimePass() {
|
|
1927
|
-
if (this._empty) return this;
|
|
1928
|
-
const newBounds = new Float64Array(this.bounds);
|
|
1929
|
-
for (let i = 1; i < this.dim; i++) {
|
|
1930
|
-
newBounds[0 * this.dim + i] = 0;
|
|
1931
|
-
}
|
|
1932
|
-
return new _DBM(newBounds, this.dim, this.clockNames, false).canonicalize();
|
|
1933
|
-
}
|
|
1934
|
-
canonicalize() {
|
|
1935
|
-
if (this._empty) return this;
|
|
1936
|
-
const canon = new Float64Array(this.bounds);
|
|
1937
|
-
if (!canonicalizeInPlace(canon, this.dim)) {
|
|
1938
|
-
return _DBM.empty(this.clockNames);
|
|
1939
|
-
}
|
|
1940
|
-
return new _DBM(canon, this.dim, this.clockNames, false);
|
|
1941
|
-
}
|
|
1942
|
-
equals(other) {
|
|
1943
|
-
if (this === other) return true;
|
|
1944
|
-
if (this._empty && other._empty) return true;
|
|
1945
|
-
if (this._empty || other._empty) return false;
|
|
1946
|
-
if (this.clockNames.length !== other.clockNames.length) return false;
|
|
1947
|
-
for (let i = 0; i < this.clockNames.length; i++) {
|
|
1948
|
-
if (this.clockNames[i] !== other.clockNames[i]) return false;
|
|
1949
|
-
}
|
|
1950
|
-
if (this.bounds.length !== other.bounds.length) return false;
|
|
1951
|
-
for (let i = 0; i < this.bounds.length; i++) {
|
|
1952
|
-
if (Math.abs(this.bounds[i] - other.bounds[i]) > EPSILON) return false;
|
|
1953
|
-
}
|
|
1954
|
-
return true;
|
|
1955
|
-
}
|
|
1956
|
-
toString() {
|
|
1957
|
-
if (this._empty) return "DBM[empty]";
|
|
1958
|
-
const parts = [];
|
|
1959
|
-
for (let i = 0; i < this.clockNames.length; i++) {
|
|
1960
|
-
const lo = formatBound(this.getLowerBound(i));
|
|
1961
|
-
const hi = formatBound(this.getUpperBound(i));
|
|
1962
|
-
parts.push(`${this.clockNames[i]}:[${lo},${hi}]`);
|
|
1963
|
-
}
|
|
1964
|
-
return `DBM{${parts.join(", ")}}`;
|
|
1965
|
-
}
|
|
1966
|
-
};
|
|
1967
|
-
function makeMatrix(dim, fill) {
|
|
1968
|
-
const m = new Float64Array(dim * dim).fill(fill);
|
|
1969
|
-
for (let i = 0; i < dim; i++) {
|
|
1970
|
-
m[i * dim + i] = 0;
|
|
1971
|
-
}
|
|
1972
|
-
return m;
|
|
1973
|
-
}
|
|
1974
|
-
function canonicalizeInPlace(dbm, dim) {
|
|
1975
|
-
for (let k = 0; k < dim; k++) {
|
|
1976
|
-
for (let i = 0; i < dim; i++) {
|
|
1977
|
-
for (let j = 0; j < dim; j++) {
|
|
1978
|
-
const ik = dbm[i * dim + k];
|
|
1979
|
-
const kj = dbm[k * dim + j];
|
|
1980
|
-
if (ik < Infinity && kj < Infinity) {
|
|
1981
|
-
const via = ik + kj;
|
|
1982
|
-
if (via < dbm[i * dim + j]) {
|
|
1983
|
-
dbm[i * dim + j] = via;
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
for (let i = 0; i < dim; i++) {
|
|
1990
|
-
if (dbm[i * dim + i] < -EPSILON) return false;
|
|
1991
|
-
}
|
|
1992
|
-
return true;
|
|
1993
|
-
}
|
|
1994
|
-
function formatBound(b) {
|
|
1995
|
-
if (b >= Infinity / 2) return "\u221E";
|
|
1996
|
-
if (b === Math.trunc(b)) return String(b);
|
|
1997
|
-
return b.toFixed(3);
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
// src/verification/analysis/state-class.ts
|
|
2001
|
-
var StateClass = class {
|
|
2002
|
-
marking;
|
|
2003
|
-
firingDomain;
|
|
2004
|
-
enabledTransitions;
|
|
2005
|
-
/**
|
|
2006
|
-
* Class-relative earliest-ready time (seconds) of each enabled transition,
|
|
2007
|
-
* parallel to `enabledTransitions`. Captured from the firing-domain DBM
|
|
2008
|
-
* (`getLowerBound(k)`) *before* `letTimePass()` zeroes the lower bounds, i.e.
|
|
2009
|
-
* the minimum time from class entry at which clock `k` may fire.
|
|
2010
|
-
*
|
|
2011
|
-
* Purely additive: base timed-reachability (marking + DBM zone, `equals`,
|
|
2012
|
-
* `classKey`) ignores it. Read only by the ν conflict-priority prune (NU-052,
|
|
2013
|
-
* `priorityDominated`), where comparing `readyEarliest[H] <= readyEarliest[L]`
|
|
2014
|
-
* decides whether the strictly higher-priority `H` becomes ready no later than
|
|
2015
|
-
* `L` and so pre-empts it.
|
|
2016
|
-
*/
|
|
2017
|
-
readyEarliest;
|
|
2018
|
-
constructor(marking, firingDomain, enabledTransitions, readyEarliest) {
|
|
2019
|
-
this.marking = marking;
|
|
2020
|
-
this.firingDomain = firingDomain;
|
|
2021
|
-
this.enabledTransitions = [...enabledTransitions];
|
|
2022
|
-
this.readyEarliest = [...readyEarliest];
|
|
2023
|
-
}
|
|
2024
|
-
isEmpty() {
|
|
2025
|
-
return this.firingDomain.isEmpty();
|
|
2026
|
-
}
|
|
2027
|
-
canFire(transition) {
|
|
2028
|
-
const idx = this.enabledTransitions.indexOf(transition);
|
|
2029
|
-
if (idx < 0) return false;
|
|
2030
|
-
return this.firingDomain.getUpperBound(idx) >= 0;
|
|
2031
|
-
}
|
|
2032
|
-
transitionIndex(transition) {
|
|
2033
|
-
return this.enabledTransitions.indexOf(transition);
|
|
2034
|
-
}
|
|
2035
|
-
equals(other) {
|
|
2036
|
-
if (this === other) return true;
|
|
2037
|
-
return this.marking.toString() === other.marking.toString() && this.firingDomain.equals(other.firingDomain);
|
|
2038
|
-
}
|
|
2039
|
-
toString() {
|
|
2040
|
-
return `StateClass{${this.marking}, ${this.firingDomain}}`;
|
|
2041
|
-
}
|
|
2042
|
-
};
|
|
2043
|
-
|
|
2044
|
-
// src/core/internal/output-action-check.ts
|
|
2045
|
-
function requireOutputProducingActions(net) {
|
|
2046
|
-
for (const t of net.transitions) {
|
|
2047
|
-
if (t.outputSpec !== null && isPassthrough(t.action)) {
|
|
2048
|
-
throw new Error(
|
|
2049
|
-
`Transition '${t.name}' declares an output spec but carries passthrough(), which produces no tokens. Every firing would fail output validation (IO-015) and the declared output would never arrive. Bind an action that produces it \u2014 fork() moves the input token across \u2014 or drop the output spec if the transition is meant to be a sink.`
|
|
2050
|
-
);
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
// src/verification/analysis/state-class-graph.ts
|
|
2056
|
-
var StateClassGraph = class _StateClassGraph {
|
|
2057
|
-
net;
|
|
2058
|
-
initialClass;
|
|
2059
|
-
_stateClasses;
|
|
2060
|
-
_transitions;
|
|
2061
|
-
_successors;
|
|
2062
|
-
_predecessors;
|
|
2063
|
-
_complete;
|
|
2064
|
-
constructor(net, initialClass, stateClasses, transitions, complete) {
|
|
2065
|
-
this.net = net;
|
|
2066
|
-
this.initialClass = initialClass;
|
|
2067
|
-
this._stateClasses = stateClasses;
|
|
2068
|
-
this._transitions = transitions;
|
|
2069
|
-
this._complete = complete;
|
|
2070
|
-
this._successors = /* @__PURE__ */ new Map();
|
|
2071
|
-
this._predecessors = /* @__PURE__ */ new Map();
|
|
2072
|
-
for (const sc of stateClasses) {
|
|
2073
|
-
this._successors.set(sc, /* @__PURE__ */ new Set());
|
|
2074
|
-
this._predecessors.set(sc, /* @__PURE__ */ new Set());
|
|
2075
|
-
}
|
|
2076
|
-
for (const [from, tMap] of transitions) {
|
|
2077
|
-
for (const edges of tMap.values()) {
|
|
2078
|
-
for (const edge of edges) {
|
|
2079
|
-
this._successors.get(from).add(edge.target);
|
|
2080
|
-
this._predecessors.get(edge.target).add(from);
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
/**
|
|
2086
|
-
* Builds the state class graph for a Time Petri Net.
|
|
2087
|
-
*
|
|
2088
|
-
* @throws Error if the net violates CORE-043 — analysis rejects the same nets execution rejects.
|
|
2089
|
-
*/
|
|
2090
|
-
static build(net, initialMarking, maxClasses, environmentPlaces, environmentMode) {
|
|
2091
|
-
requireOutputProducingActions(net);
|
|
2092
|
-
const envMode = environmentMode ?? ignore();
|
|
2093
|
-
const envPlaces = /* @__PURE__ */ new Set();
|
|
2094
|
-
if (environmentPlaces) {
|
|
2095
|
-
for (const ep of environmentPlaces) {
|
|
2096
|
-
envPlaces.add(ep.place);
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
const initialClass = initialStateClass(net, initialMarking, envPlaces, envMode);
|
|
2100
|
-
const stateClasses = [initialClass];
|
|
2101
|
-
const stateClassSet = /* @__PURE__ */ new Set([classKey(initialClass)]);
|
|
2102
|
-
const classMap = /* @__PURE__ */ new Map([[classKey(initialClass), initialClass]]);
|
|
2103
|
-
const transitionMap = /* @__PURE__ */ new Map();
|
|
2104
|
-
transitionMap.set(initialClass, /* @__PURE__ */ new Map());
|
|
2105
|
-
const queue = [initialClass];
|
|
2106
|
-
let complete = true;
|
|
2107
|
-
while (queue.length > 0) {
|
|
2108
|
-
if (stateClasses.length >= maxClasses) {
|
|
2109
|
-
complete = false;
|
|
2110
|
-
break;
|
|
2111
|
-
}
|
|
2112
|
-
const current = queue.shift();
|
|
2113
|
-
for (const transition of current.enabledTransitions) {
|
|
2114
|
-
const virtualTransitions = expandTransition(transition);
|
|
2115
|
-
for (const vt of virtualTransitions) {
|
|
2116
|
-
const successor = computeSuccessor(net, current, vt, envPlaces, envMode);
|
|
2117
|
-
if (successor === null || successor.isEmpty()) continue;
|
|
2118
|
-
const tEdges = transitionMap.get(current);
|
|
2119
|
-
if (!tEdges.has(transition)) tEdges.set(transition, []);
|
|
2120
|
-
tEdges.get(transition).push({ branchIndex: vt.branchIndex, target: successor });
|
|
2121
|
-
const key = classKey(successor);
|
|
2122
|
-
if (!stateClassSet.has(key)) {
|
|
2123
|
-
stateClassSet.add(key);
|
|
2124
|
-
classMap.set(key, successor);
|
|
2125
|
-
stateClasses.push(successor);
|
|
2126
|
-
transitionMap.set(successor, /* @__PURE__ */ new Map());
|
|
2127
|
-
queue.push(successor);
|
|
2128
|
-
} else {
|
|
2129
|
-
const canonical = classMap.get(key);
|
|
2130
|
-
if (canonical !== successor) {
|
|
2131
|
-
const edges = tEdges.get(transition);
|
|
2132
|
-
edges[edges.length - 1] = { branchIndex: vt.branchIndex, target: canonical };
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
return new _StateClassGraph(net, initialClass, stateClasses, transitionMap, complete);
|
|
2139
|
-
}
|
|
2140
|
-
stateClasses() {
|
|
2141
|
-
return this._stateClasses;
|
|
2142
|
-
}
|
|
2143
|
-
size() {
|
|
2144
|
-
return this._stateClasses.length;
|
|
2145
|
-
}
|
|
2146
|
-
isComplete() {
|
|
2147
|
-
return this._complete;
|
|
2148
|
-
}
|
|
2149
|
-
successors(sc) {
|
|
2150
|
-
return this._successors.get(sc) ?? /* @__PURE__ */ new Set();
|
|
2151
|
-
}
|
|
2152
|
-
predecessors(sc) {
|
|
2153
|
-
return this._predecessors.get(sc) ?? /* @__PURE__ */ new Set();
|
|
2154
|
-
}
|
|
2155
|
-
/** Returns all outgoing transitions with their branch edges. */
|
|
2156
|
-
outgoingBranchEdges(sc) {
|
|
2157
|
-
return this._transitions.get(sc) ?? /* @__PURE__ */ new Map();
|
|
2158
|
-
}
|
|
2159
|
-
/** Returns the branch edges for a specific transition from a state class. */
|
|
2160
|
-
branchEdges(sc, transition) {
|
|
2161
|
-
const map = this._transitions.get(sc);
|
|
2162
|
-
if (!map) return [];
|
|
2163
|
-
return map.get(transition) ?? [];
|
|
2164
|
-
}
|
|
2165
|
-
/** Returns all transitions that are enabled from a state class. */
|
|
2166
|
-
enabledTransitions(sc) {
|
|
2167
|
-
const map = this._transitions.get(sc);
|
|
2168
|
-
if (!map) return /* @__PURE__ */ new Set();
|
|
2169
|
-
return new Set(map.keys());
|
|
2170
|
-
}
|
|
2171
|
-
/** Finds all state classes with a given marking. */
|
|
2172
|
-
classesWithMarking(marking) {
|
|
2173
|
-
const key = marking.toString();
|
|
2174
|
-
return this._stateClasses.filter((sc) => sc.marking.toString() === key);
|
|
2175
|
-
}
|
|
2176
|
-
/** Checks if a marking is reachable. */
|
|
2177
|
-
isReachable(marking) {
|
|
2178
|
-
const key = marking.toString();
|
|
2179
|
-
return this._stateClasses.some((sc) => sc.marking.toString() === key);
|
|
2180
|
-
}
|
|
2181
|
-
/** Gets all reachable markings. */
|
|
2182
|
-
reachableMarkings() {
|
|
2183
|
-
const markings = /* @__PURE__ */ new Set();
|
|
2184
|
-
for (const sc of this._stateClasses) {
|
|
2185
|
-
markings.add(sc.marking.toString());
|
|
2186
|
-
}
|
|
2187
|
-
return markings;
|
|
2188
|
-
}
|
|
2189
|
-
/** Counts edges in the graph (each branch edge counts separately). */
|
|
2190
|
-
edgeCount() {
|
|
2191
|
-
let count = 0;
|
|
2192
|
-
for (const map of this._transitions.values()) {
|
|
2193
|
-
for (const edges of map.values()) {
|
|
2194
|
-
count += edges.length;
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
return count;
|
|
2198
|
-
}
|
|
2199
|
-
toString() {
|
|
2200
|
-
return `StateClassGraph[classes=${this.size()}, edges=${this.edgeCount()}, complete=${this._complete}]`;
|
|
2201
|
-
}
|
|
2202
|
-
};
|
|
2203
|
-
function classKey(sc) {
|
|
2204
|
-
return `${sc.marking.toString()}|${sc.firingDomain.toString()}`;
|
|
2205
|
-
}
|
|
2206
|
-
function initialStateClass(net, initialMarking, envPlaces, envMode) {
|
|
2207
|
-
const enabledTransitions = findEnabledTransitions(net, initialMarking, envPlaces, envMode);
|
|
2208
|
-
const clockNames = enabledTransitions.map((t) => t.name);
|
|
2209
|
-
const lowerBounds = enabledTransitions.map((t) => earliest(t.timing) / 1e3);
|
|
2210
|
-
const upperBounds = enabledTransitions.map((t) => latest(t.timing) / 1e3);
|
|
2211
|
-
const baseDBM = DBM.create(clockNames, lowerBounds, upperBounds);
|
|
2212
|
-
const readyEarliest = enabledTransitions.map((_, k) => baseDBM.getLowerBound(k));
|
|
2213
|
-
const initialDBM = baseDBM.letTimePass();
|
|
2214
|
-
return new StateClass(initialMarking, initialDBM, enabledTransitions, readyEarliest);
|
|
2215
|
-
}
|
|
2216
|
-
function expandTransition(t) {
|
|
2217
|
-
let branches;
|
|
2218
|
-
if (t.outputSpec !== null) {
|
|
2219
|
-
branches = enumerateBranches(t.outputSpec);
|
|
2220
|
-
} else {
|
|
2221
|
-
branches = [/* @__PURE__ */ new Set()];
|
|
2222
|
-
}
|
|
2223
|
-
return branches.map((outputPlaces, i) => ({
|
|
2224
|
-
transition: t,
|
|
2225
|
-
branchIndex: i,
|
|
2226
|
-
outputPlaces
|
|
2227
|
-
}));
|
|
2228
|
-
}
|
|
2229
|
-
function computeSuccessor(net, current, fired, environmentPlaces, environmentMode) {
|
|
2230
|
-
const transition = fired.transition;
|
|
2231
|
-
const newMarking = fireTransition(current.marking, transition, fired.outputPlaces, environmentPlaces, environmentMode);
|
|
2232
|
-
const newEnabledAll = findEnabledTransitions(net, newMarking, environmentPlaces, environmentMode);
|
|
2233
|
-
const persistent = [];
|
|
2234
|
-
const persistentIndices = [];
|
|
2235
|
-
for (let i = 0; i < current.enabledTransitions.length; i++) {
|
|
2236
|
-
const t = current.enabledTransitions[i];
|
|
2237
|
-
if (t !== transition && newEnabledAll.includes(t)) {
|
|
2238
|
-
persistent.push(t);
|
|
2239
|
-
persistentIndices.push(i);
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
const newlyEnabled = [];
|
|
2243
|
-
for (const t of newEnabledAll) {
|
|
2244
|
-
if (!persistent.includes(t)) {
|
|
2245
|
-
newlyEnabled.push(t);
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
const firedIdx = current.transitionIndex(transition);
|
|
2249
|
-
const newClockNames = newlyEnabled.map((t) => t.name);
|
|
2250
|
-
const newLowerBounds = newlyEnabled.map((t) => earliest(t.timing) / 1e3);
|
|
2251
|
-
const newUpperBounds = newlyEnabled.map((t) => latest(t.timing) / 1e3);
|
|
2252
|
-
const firedDBM = current.firingDomain.fireTransition(
|
|
2253
|
-
firedIdx,
|
|
2254
|
-
newClockNames,
|
|
2255
|
-
newLowerBounds,
|
|
2256
|
-
newUpperBounds,
|
|
2257
|
-
persistentIndices
|
|
2258
|
-
);
|
|
2259
|
-
const allEnabled = [...persistent, ...newlyEnabled];
|
|
2260
|
-
const readyEarliest = allEnabled.map((_, k) => firedDBM.getLowerBound(k));
|
|
2261
|
-
const newDBM = firedDBM.letTimePass();
|
|
2262
|
-
return new StateClass(newMarking, newDBM, allEnabled, readyEarliest);
|
|
2263
|
-
}
|
|
2264
|
-
function findEnabledTransitions(net, marking, environmentPlaces, environmentMode) {
|
|
2265
|
-
const enabled = [];
|
|
2266
|
-
for (const transition of net.transitions) {
|
|
2267
|
-
if (isEnabled(transition, marking, environmentPlaces, environmentMode)) {
|
|
2268
|
-
enabled.push(transition);
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
return enabled;
|
|
2272
|
-
}
|
|
2273
|
-
function isEnabled(transition, marking, environmentPlaces, environmentMode) {
|
|
2274
|
-
for (const spec of transition.inputSpecs) {
|
|
2275
|
-
const required = inputRequiredCount(spec);
|
|
2276
|
-
if (!checkPlaceEnabled(spec.place, required, marking, environmentPlaces, environmentMode)) {
|
|
2277
|
-
return false;
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
for (const arc of transition.reads) {
|
|
2281
|
-
if (!checkPlaceEnabled(arc.place, 1, marking, environmentPlaces, environmentMode)) {
|
|
2282
|
-
return false;
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
for (const arc of transition.inhibitors) {
|
|
2286
|
-
if (marking.hasTokens(arc.place)) {
|
|
2287
|
-
return false;
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
return true;
|
|
2291
|
-
}
|
|
2292
|
-
function inputRequiredCount(spec) {
|
|
2293
|
-
switch (spec.type) {
|
|
2294
|
-
case "one":
|
|
2295
|
-
return 1;
|
|
2296
|
-
case "exactly":
|
|
2297
|
-
return spec.count;
|
|
2298
|
-
case "all":
|
|
2299
|
-
return 1;
|
|
2300
|
-
case "at-least":
|
|
2301
|
-
return spec.minimum;
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
function inputConsumeCount(spec, available) {
|
|
2305
|
-
return consumptionCount(spec, available);
|
|
2306
|
-
}
|
|
2307
|
-
function checkPlaceEnabled(place, required, marking, environmentPlaces, environmentMode) {
|
|
2308
|
-
if (!environmentPlaces.has(place)) {
|
|
2309
|
-
return marking.tokens(place) >= required;
|
|
2310
|
-
}
|
|
2311
|
-
switch (environmentMode.type) {
|
|
2312
|
-
case "always-available":
|
|
2313
|
-
return true;
|
|
2314
|
-
case "bounded":
|
|
2315
|
-
return required <= environmentMode.maxTokens;
|
|
2316
|
-
case "ignore":
|
|
2317
|
-
return marking.tokens(place) >= required;
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
function fireTransition(marking, transition, outputPlaces, environmentPlaces, environmentMode) {
|
|
2321
|
-
const builder = MarkingState.builder().copyFrom(marking);
|
|
2322
|
-
for (const spec of transition.inputSpecs) {
|
|
2323
|
-
const available = marking.tokens(spec.place);
|
|
2324
|
-
if (available < inputRequiredCount(spec)) {
|
|
2325
|
-
continue;
|
|
2326
|
-
}
|
|
2327
|
-
const toConsume = inputConsumeCount(spec, available);
|
|
2328
|
-
consumeFromPlace(builder, spec.place, toConsume, environmentPlaces, environmentMode);
|
|
2329
|
-
}
|
|
2330
|
-
for (const arc of transition.resets) {
|
|
2331
|
-
const current = marking.tokens(arc.place);
|
|
2332
|
-
if (current > 0) {
|
|
2333
|
-
builder.removeTokens(arc.place, current);
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
for (const place of outputPlaces) {
|
|
2337
|
-
builder.addTokens(place, 1);
|
|
2338
|
-
}
|
|
2339
|
-
return builder.build();
|
|
2340
|
-
}
|
|
2341
|
-
function consumeFromPlace(builder, place, count, environmentPlaces, environmentMode) {
|
|
2342
|
-
if (!environmentPlaces.has(place)) {
|
|
2343
|
-
builder.removeTokens(place, count);
|
|
2344
|
-
return;
|
|
2345
|
-
}
|
|
2346
|
-
if (environmentMode.type === "ignore") {
|
|
2347
|
-
builder.removeTokens(place, count);
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
|
-
// src/verification/z3/counterexample-decoder.ts
|
|
2352
|
-
function decode(answer, flatNet) {
|
|
2353
|
-
const states = decodeStateSet(answer, flatNet);
|
|
2354
|
-
return { states, note: states.size === 0 ? "no ground Reachable states in the z3 proof" : null };
|
|
2355
|
-
}
|
|
2356
|
-
function decodeStateSet(answer, flatNet) {
|
|
2357
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
2358
|
-
const P = flatNet.places.length;
|
|
2359
|
-
for (const head of ["(Reachable", "(|Reachable|"]) {
|
|
2360
|
-
let from = 0;
|
|
2361
|
-
for (; ; ) {
|
|
2362
|
-
const start = answer.indexOf(head, from);
|
|
2363
|
-
if (start < 0) break;
|
|
2364
|
-
from = start + head.length;
|
|
2365
|
-
if (head === "(Reachable") {
|
|
2366
|
-
const next = answer[from];
|
|
2367
|
-
if (next == null || !(/\s/.test(next) || next === ")")) continue;
|
|
2368
|
-
}
|
|
2369
|
-
const end = sexprEnd(answer, start);
|
|
2370
|
-
if (end < 0) break;
|
|
2371
|
-
const inner = answer.slice(start + head.length, end - 1);
|
|
2372
|
-
const args = parseGroundIntArgs(inner);
|
|
2373
|
-
if (args != null && args.length === P) {
|
|
2374
|
-
const marking = toMarking(args, flatNet);
|
|
2375
|
-
const key = marking.toString();
|
|
2376
|
-
if (!byKey.has(key)) byKey.set(key, marking);
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
return new Set(byKey.values());
|
|
2381
|
-
}
|
|
2382
|
-
function toMarking(args, flatNet) {
|
|
2383
|
-
const builder = MarkingState.builder();
|
|
2384
|
-
for (let i = 0; i < args.length; i++) {
|
|
2385
|
-
if (args[i] > 0) builder.tokens(flatNet.places[i], args[i]);
|
|
2386
|
-
}
|
|
2387
|
-
return builder.build();
|
|
2388
|
-
}
|
|
2389
|
-
function parseGroundIntArgs(inner) {
|
|
2390
|
-
const args = [];
|
|
2391
|
-
let rest = inner.trimStart();
|
|
2392
|
-
while (rest !== "") {
|
|
2393
|
-
if (rest.startsWith("(")) {
|
|
2394
|
-
const stripped = rest.slice(1);
|
|
2395
|
-
const close = stripped.indexOf(")");
|
|
2396
|
-
if (close < 0) return null;
|
|
2397
|
-
const body = stripped.slice(0, close);
|
|
2398
|
-
if (body.includes("(")) return null;
|
|
2399
|
-
const trimmed = body.trim();
|
|
2400
|
-
if (!trimmed.startsWith("-")) return null;
|
|
2401
|
-
const n = parseInt64(trimmed.slice(1).trim());
|
|
2402
|
-
if (n == null) return null;
|
|
2403
|
-
args.push(-n);
|
|
2404
|
-
rest = stripped.slice(close + 1).trimStart();
|
|
2405
|
-
} else {
|
|
2406
|
-
let tokenEnd = rest.length;
|
|
2407
|
-
for (let i = 0; i < rest.length; i++) {
|
|
2408
|
-
const c = rest[i];
|
|
2409
|
-
if (/\s/.test(c) || c === "(" || c === ")") {
|
|
2410
|
-
tokenEnd = i;
|
|
2411
|
-
break;
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
const n = parseInt64(rest.slice(0, tokenEnd));
|
|
2415
|
-
if (n == null) return null;
|
|
2416
|
-
args.push(n);
|
|
2417
|
-
rest = rest.slice(tokenEnd).trimStart();
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
return args;
|
|
2421
|
-
}
|
|
2422
|
-
function parseInt64(token) {
|
|
2423
|
-
return /^-?\d+$/.test(token) ? Number(token) : null;
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2426
|
-
// src/verification/encoding/flat-net.ts
|
|
2427
|
-
function flatNetPlaceCount(net) {
|
|
2428
|
-
return net.places.length;
|
|
2429
|
-
}
|
|
2430
|
-
function flatNetTransitionCount(net) {
|
|
2431
|
-
return net.transitions.length;
|
|
2432
|
-
}
|
|
2433
|
-
function flatNetIndexOf(net, place) {
|
|
2434
|
-
return net.placeIndex.get(place.name) ?? -1;
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
|
-
// src/verification/z3/abstract-replayer.ts
|
|
2438
|
-
function stepName(step) {
|
|
2439
|
-
return step.kind === "fire" ? step.transition : `inject(${step.place})`;
|
|
2440
|
-
}
|
|
2441
|
-
function stateKey(state) {
|
|
2442
|
-
return state.join(",");
|
|
2443
|
-
}
|
|
2444
|
-
function vectorize(marking, flatNet) {
|
|
2445
|
-
return flatNet.places.map((p) => marking.tokens(p));
|
|
2446
|
-
}
|
|
2447
|
-
function toMarkingState(state, flatNet) {
|
|
2448
|
-
const builder = MarkingState.builder();
|
|
2449
|
-
for (let i = 0; i < flatNet.places.length; i++) {
|
|
2450
|
-
if (state[i] > 0) builder.tokens(flatNet.places[i], state[i]);
|
|
2451
|
-
}
|
|
2452
|
-
return builder.build();
|
|
2453
|
-
}
|
|
2454
|
-
function enabledA(state, ft) {
|
|
2455
|
-
const P = state.length;
|
|
2456
|
-
for (let p = 0; p < P; p++) {
|
|
2457
|
-
if (ft.preVector[p] > 0 && state[p] < ft.preVector[p]) return false;
|
|
2458
|
-
}
|
|
2459
|
-
for (const p of ft.readPlaces) {
|
|
2460
|
-
if (state[p] < 1) return false;
|
|
2461
|
-
}
|
|
2462
|
-
for (const p of ft.inhibitorPlaces) {
|
|
2463
|
-
if (state[p] !== 0) return false;
|
|
2464
|
-
}
|
|
2465
|
-
return true;
|
|
2466
|
-
}
|
|
2467
|
-
function fireIndexed(state, ft, resets) {
|
|
2468
|
-
const P = state.length;
|
|
2469
|
-
const next = new Array(P);
|
|
2470
|
-
for (let p = 0; p < P; p++) {
|
|
2471
|
-
if (resets.has(p) || ft.consumeAll[p]) {
|
|
2472
|
-
next[p] = ft.postVector[p];
|
|
2473
|
-
} else {
|
|
2474
|
-
next[p] = state[p] - ft.preVector[p] + ft.postVector[p];
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
return next;
|
|
2478
|
-
}
|
|
2479
|
-
function injectA(state, idx) {
|
|
2480
|
-
const next = [...state];
|
|
2481
|
-
next[idx] = next[idx] + 1;
|
|
2482
|
-
return next;
|
|
2483
|
-
}
|
|
2484
|
-
function buildIndex(flatNet) {
|
|
2485
|
-
const resetSets = flatNet.transitions.map((ft) => new Set(ft.resetPlaces));
|
|
2486
|
-
const envInj = /* @__PURE__ */ new Map();
|
|
2487
|
-
for (const [name, bound] of flatNet.environmentInjection) {
|
|
2488
|
-
const idx = flatNet.placeIndex.get(name);
|
|
2489
|
-
if (idx != null) envInj.set(idx, bound);
|
|
2490
|
-
}
|
|
2491
|
-
const envCaps = [];
|
|
2492
|
-
for (const [name, cap] of flatNet.environmentBounds) {
|
|
2493
|
-
const idx = flatNet.placeIndex.get(name);
|
|
2494
|
-
if (idx != null) envCaps.push([idx, cap]);
|
|
2495
|
-
}
|
|
2496
|
-
return { flatNet, resetSets, envInj, envCaps };
|
|
2497
|
-
}
|
|
2498
|
-
function withinEnvBounds(index, state) {
|
|
2499
|
-
for (const [idx, cap] of index.envCaps) {
|
|
2500
|
-
if (state[idx] > cap) return false;
|
|
2501
|
-
}
|
|
2502
|
-
return true;
|
|
2503
|
-
}
|
|
2504
|
-
function successorsIndexed(index, state) {
|
|
2505
|
-
const out = [];
|
|
2506
|
-
const transitions = index.flatNet.transitions;
|
|
2507
|
-
for (let t = 0; t < transitions.length; t++) {
|
|
2508
|
-
const ft = transitions[t];
|
|
2509
|
-
if (!enabledA(state, ft)) continue;
|
|
2510
|
-
const next = fireIndexed(state, ft, index.resetSets[t]);
|
|
2511
|
-
if (!withinEnvBounds(index, next)) continue;
|
|
2512
|
-
out.push({ state: next, step: { kind: "fire", transition: ft.name } });
|
|
2513
|
-
}
|
|
2514
|
-
for (const [name, bound] of index.flatNet.environmentInjection) {
|
|
2515
|
-
const idx = index.flatNet.placeIndex.get(name);
|
|
2516
|
-
if (idx == null) continue;
|
|
2517
|
-
if (bound === null || state[idx] < bound) {
|
|
2518
|
-
out.push({ state: injectA(state, idx), step: { kind: "inject", place: name } });
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
return out;
|
|
2522
|
-
}
|
|
2523
|
-
function enabledRelaxEnv(state, ft, envInj) {
|
|
2524
|
-
const P = state.length;
|
|
2525
|
-
for (let p = 0; p < P; p++) {
|
|
2526
|
-
const pre = ft.preVector[p];
|
|
2527
|
-
if (pre <= 0) continue;
|
|
2528
|
-
if (envInj.has(p)) {
|
|
2529
|
-
const bound = envInj.get(p);
|
|
2530
|
-
if (bound !== null && pre > bound) return false;
|
|
2531
|
-
continue;
|
|
2532
|
-
}
|
|
2533
|
-
if (state[p] < pre) return false;
|
|
2534
|
-
}
|
|
2535
|
-
for (const p of ft.readPlaces) {
|
|
2536
|
-
if (envInj.has(p)) {
|
|
2537
|
-
const bound = envInj.get(p);
|
|
2538
|
-
if (bound !== null && bound < 1) return false;
|
|
2539
|
-
continue;
|
|
2540
|
-
}
|
|
2541
|
-
if (state[p] < 1) return false;
|
|
2542
|
-
}
|
|
2543
|
-
for (const p of ft.inhibitorPlaces) {
|
|
2544
|
-
if (state[p] !== 0) return false;
|
|
2545
|
-
}
|
|
2546
|
-
return true;
|
|
2547
|
-
}
|
|
2548
|
-
function isQuiescent(index, state) {
|
|
2549
|
-
for (const ft of index.flatNet.transitions) {
|
|
2550
|
-
if (enabledRelaxEnv(state, ft, index.envInj)) return false;
|
|
2551
|
-
}
|
|
2552
|
-
return true;
|
|
2553
|
-
}
|
|
2554
|
-
function sinkIndices(flatNet, sinkPlaces) {
|
|
2555
|
-
const idx = /* @__PURE__ */ new Set();
|
|
2556
|
-
for (const sink of sinkPlaces) {
|
|
2557
|
-
const i = flatNetIndexOf(flatNet, sink);
|
|
2558
|
-
if (i >= 0) idx.add(i);
|
|
2559
|
-
}
|
|
2560
|
-
return idx;
|
|
2561
|
-
}
|
|
2562
|
-
function satisfiesBadIndexed(index, state, property, sinkPlaces) {
|
|
2563
|
-
const flatNet = index.flatNet;
|
|
2564
|
-
switch (property.type) {
|
|
2565
|
-
// DeadlockFree (VER-002): quiescent AND some marked place is not a declared
|
|
2566
|
-
// sink. Mirrors the encoder's `stranded` disjunction.
|
|
2567
|
-
case "deadlock-free": {
|
|
2568
|
-
if (!isQuiescent(index, state)) return false;
|
|
2569
|
-
const sinks = sinkIndices(flatNet, sinkPlaces);
|
|
2570
|
-
for (let pid = 0; pid < flatNet.places.length; pid++) {
|
|
2571
|
-
if (!sinks.has(pid) && state[pid] >= 1) return true;
|
|
2572
|
-
}
|
|
2573
|
-
return false;
|
|
2574
|
-
}
|
|
2575
|
-
// TerminatesAtSink (VER-002): quiescent AND no declared sink marked.
|
|
2576
|
-
case "terminates-at-sink": {
|
|
2577
|
-
if (!isQuiescent(index, state)) return false;
|
|
2578
|
-
for (const pid of sinkIndices(flatNet, sinkPlaces)) {
|
|
2579
|
-
if (state[pid] !== 0) return false;
|
|
2580
|
-
}
|
|
2581
|
-
return true;
|
|
2582
|
-
}
|
|
2583
|
-
case "mutual-exclusion": {
|
|
2584
|
-
const idx1 = flatNetIndexOf(flatNet, property.p1);
|
|
2585
|
-
const idx2 = flatNetIndexOf(flatNet, property.p2);
|
|
2586
|
-
if (idx1 < 0 || idx2 < 0) return false;
|
|
2587
|
-
return state[idx1] >= 1 && state[idx2] >= 1;
|
|
2588
|
-
}
|
|
2589
|
-
case "place-bound":
|
|
2590
|
-
case "branch-place-bound": {
|
|
2591
|
-
const idx = flatNetIndexOf(flatNet, property.place);
|
|
2592
|
-
if (idx < 0) return false;
|
|
2593
|
-
return state[idx] > property.bound;
|
|
2594
|
-
}
|
|
2595
|
-
// JoinedOrDeadLettered (NU-040 AC4): quiescent AND `pending` marked. No sink
|
|
2596
|
-
// clause — a marked sink must not excuse a stranded group.
|
|
2597
|
-
case "joined-or-dead-lettered": {
|
|
2598
|
-
const idx = flatNetIndexOf(flatNet, property.pending);
|
|
2599
|
-
if (idx < 0) return false;
|
|
2600
|
-
return isQuiescent(index, state) && state[idx] >= 1;
|
|
2601
|
-
}
|
|
2602
|
-
case "unreachable": {
|
|
2603
|
-
let resolved = 0;
|
|
2604
|
-
for (const p of property.places) {
|
|
2605
|
-
const idx = flatNetIndexOf(flatNet, p);
|
|
2606
|
-
if (idx < 0) continue;
|
|
2607
|
-
resolved++;
|
|
2608
|
-
if (state[idx] < 1) return false;
|
|
2609
|
-
}
|
|
2610
|
-
return resolved > 0;
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
function replayCounterexample(flatNet, initial, decodedStates, property, sinkPlaces, options = {}) {
|
|
2615
|
-
const segmentBudget = options.segmentBudget ?? 3;
|
|
2616
|
-
const nodeBudget = options.nodeBudget ?? 1e4;
|
|
2617
|
-
const anchors = /* @__PURE__ */ new Set();
|
|
2618
|
-
for (const s of decodedStates) anchors.add(stateKey(s));
|
|
2619
|
-
if (anchors.size === 0) {
|
|
2620
|
-
return { kind: "exhausted", reason: "no decoded states to replay", nodesExplored: 0 };
|
|
2621
|
-
}
|
|
2622
|
-
const initKey = stateKey(initial);
|
|
2623
|
-
if (!anchors.has(initKey)) {
|
|
2624
|
-
return {
|
|
2625
|
-
kind: "exhausted",
|
|
2626
|
-
reason: "the initial marking is not among the decoded states",
|
|
2627
|
-
nodesExplored: 0
|
|
2628
|
-
};
|
|
2629
|
-
}
|
|
2630
|
-
const index = buildIndex(flatNet);
|
|
2631
|
-
if (satisfiesBadIndexed(index, initial, property, sinkPlaces)) {
|
|
2632
|
-
return { kind: "confirmed", states: [initial], steps: [], nodesExplored: 1 };
|
|
2633
|
-
}
|
|
2634
|
-
const nodes = [{ state: initial, step: null, parent: -1, segment: 0 }];
|
|
2635
|
-
const bestSegment = /* @__PURE__ */ new Map([[initKey, 0]]);
|
|
2636
|
-
const queue = [0];
|
|
2637
|
-
let truncated = false;
|
|
2638
|
-
for (let head = 0; head < queue.length; head++) {
|
|
2639
|
-
const idx = queue[head];
|
|
2640
|
-
const node = nodes[idx];
|
|
2641
|
-
if (node.segment >= segmentBudget) {
|
|
2642
|
-
truncated = true;
|
|
2643
|
-
continue;
|
|
2644
|
-
}
|
|
2645
|
-
for (const succ of successorsIndexed(index, node.state)) {
|
|
2646
|
-
const key = stateKey(succ.state);
|
|
2647
|
-
const segment = anchors.has(key) ? 0 : node.segment + 1;
|
|
2648
|
-
const prior = bestSegment.get(key);
|
|
2649
|
-
if (prior !== void 0 && prior <= segment) continue;
|
|
2650
|
-
bestSegment.set(key, segment);
|
|
2651
|
-
if (nodes.length >= nodeBudget) {
|
|
2652
|
-
return {
|
|
2653
|
-
kind: "exhausted",
|
|
2654
|
-
reason: `search budget exhausted (${nodeBudget} nodes) before reaching a violating state`,
|
|
2655
|
-
nodesExplored: nodes.length
|
|
2656
|
-
};
|
|
2657
|
-
}
|
|
2658
|
-
nodes.push({ state: succ.state, step: succ.step, parent: idx, segment });
|
|
2659
|
-
const childIdx = nodes.length - 1;
|
|
2660
|
-
if (satisfiesBadIndexed(index, succ.state, property, sinkPlaces)) {
|
|
2661
|
-
const chain = reconstruct(nodes, childIdx);
|
|
2662
|
-
return { kind: "confirmed", ...chain, nodesExplored: nodes.length };
|
|
2663
|
-
}
|
|
2664
|
-
queue.push(childIdx);
|
|
2665
|
-
}
|
|
2666
|
-
}
|
|
2667
|
-
if (truncated) {
|
|
2668
|
-
return {
|
|
2669
|
-
kind: "exhausted",
|
|
2670
|
-
reason: `no violating state within ${segmentBudget} abstract step(s) of a decoded state (${bestSegment.size} state(s) explored)`,
|
|
2671
|
-
nodesExplored: nodes.length
|
|
2672
|
-
};
|
|
2673
|
-
}
|
|
2674
|
-
return { kind: "no-chain", nodesExplored: nodes.length };
|
|
2675
|
-
}
|
|
2676
|
-
function reconstruct(nodes, last) {
|
|
2677
|
-
const states = [];
|
|
2678
|
-
const steps = [];
|
|
2679
|
-
for (let i = last; i >= 0; i = nodes[i].parent) {
|
|
2680
|
-
const node = nodes[i];
|
|
2681
|
-
states.push(node.state);
|
|
2682
|
-
if (node.step != null) steps.push(node.step);
|
|
2683
|
-
}
|
|
2684
|
-
states.reverse();
|
|
2685
|
-
steps.reverse();
|
|
2686
|
-
return { states, steps };
|
|
2687
|
-
}
|
|
2688
|
-
|
|
2689
|
-
// src/verification/z3/name-coloured-encoder.ts
|
|
2690
|
-
function colourSlotBound(coloured, semiflows) {
|
|
2691
|
-
const w = (inv, pid) => inv.weights[pid] ?? 0;
|
|
2692
|
-
const isSemiflow = (inv) => inv.weights.every((x) => x >= 0);
|
|
2693
|
-
let single = null;
|
|
2694
|
-
for (const inv of semiflows) {
|
|
2695
|
-
if (isSemiflow(inv) && coloured.every((pid) => w(inv, pid) >= 1)) {
|
|
2696
|
-
if (single === null || inv.constant < single) single = inv.constant;
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
if (single !== null) return single;
|
|
2700
|
-
const covered = new Array(coloured.length).fill(false);
|
|
2701
|
-
for (const inv of semiflows) {
|
|
2702
|
-
if (!isSemiflow(inv) || inv.constant !== 0) continue;
|
|
2703
|
-
for (let i = 0; i < coloured.length; i++) {
|
|
2704
|
-
if (w(inv, coloured[i]) >= 1) covered[i] = true;
|
|
2705
|
-
}
|
|
2706
|
-
}
|
|
2707
|
-
const free = [...covered];
|
|
2708
|
-
let sumConst = 0;
|
|
2709
|
-
for (const inv of semiflows) {
|
|
2710
|
-
if (!isSemiflow(inv) || inv.constant === 0) continue;
|
|
2711
|
-
if (!coloured.some((pid, i) => !free[i] && w(inv, pid) >= 1)) continue;
|
|
2712
|
-
for (let i = 0; i < coloured.length; i++) {
|
|
2713
|
-
if (w(inv, coloured[i]) >= 1) covered[i] = true;
|
|
2714
|
-
}
|
|
2715
|
-
sumConst += inv.constant;
|
|
2716
|
-
}
|
|
2717
|
-
if (covered.every((c) => c)) return sumConst;
|
|
2718
|
-
return null;
|
|
2719
|
-
}
|
|
2720
|
-
function buildColouredPlan(net, flat, initial, budgetNames, fragmentMode, carrierPlaces, semiflows) {
|
|
2721
|
-
const P = flat.places.length;
|
|
2722
|
-
const isColoured = new Array(P).fill(false);
|
|
2723
|
-
for (const t of net.transitions) {
|
|
2724
|
-
const ms = t.matchSpec;
|
|
2725
|
-
if (ms) {
|
|
2726
|
-
for (const key of ms.keys) {
|
|
2727
|
-
const pid = flat.placeIndex.get(key.place.name);
|
|
2728
|
-
if (pid == null) return null;
|
|
2729
|
-
isColoured[pid] = true;
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
}
|
|
2733
|
-
if (fragmentMode === "extended") {
|
|
2734
|
-
for (const c of carrierPlaces) {
|
|
2735
|
-
const pid = flat.placeIndex.get(c);
|
|
2736
|
-
if (pid != null) isColoured[pid] = true;
|
|
2737
|
-
}
|
|
2738
|
-
}
|
|
2739
|
-
const coloured = [];
|
|
2740
|
-
for (let i = 0; i < P; i++) if (isColoured[i]) coloured.push(i);
|
|
2741
|
-
if (coloured.length === 0) return null;
|
|
2742
|
-
for (const pid of coloured) {
|
|
2743
|
-
if (initial.tokens(flat.places[pid]) !== 0) return null;
|
|
2744
|
-
}
|
|
2745
|
-
const k = colourSlotBound(coloured, semiflows);
|
|
2746
|
-
if (k === null) return null;
|
|
2747
|
-
if (k === 0 && coloured.length === P) return null;
|
|
2748
|
-
const budgetIdx = /* @__PURE__ */ new Set();
|
|
2749
|
-
for (const n of budgetNames) {
|
|
2750
|
-
const i = flat.placeIndex.get(n);
|
|
2751
|
-
if (i != null) budgetIdx.add(i);
|
|
2752
|
-
}
|
|
2753
|
-
for (const ft of flat.transitions) {
|
|
2754
|
-
const touches = ft.inhibitorPlaces.some((i) => isColoured[i]) || ft.readPlaces.some((i) => isColoured[i]) || ft.resetPlaces.some((i) => isColoured[i]) || ft.consumeAll.some((ca, i) => ca && isColoured[i]);
|
|
2755
|
-
if (touches) return null;
|
|
2756
|
-
}
|
|
2757
|
-
const classes = [];
|
|
2758
|
-
for (const ft of flat.transitions) {
|
|
2759
|
-
const colouredIn = coloured.filter((pid) => ft.preVector[pid] > 0);
|
|
2760
|
-
const colouredOut = coloured.filter((pid) => ft.postVector[pid] > 0);
|
|
2761
|
-
const ms = ft.source.matchSpec;
|
|
2762
|
-
if (ms) {
|
|
2763
|
-
if (colouredOut.length !== 0 || colouredIn.length === 0) return null;
|
|
2764
|
-
if (colouredIn.some((pid) => ft.preVector[pid] !== 1)) return null;
|
|
2765
|
-
classes.push({ kind: "join", colouredIn });
|
|
2766
|
-
} else if (colouredIn.length !== 0) {
|
|
2767
|
-
if (fragmentMode !== "extended") return null;
|
|
2768
|
-
if (colouredIn.length !== 1 || ft.preVector[colouredIn[0]] !== 1) return null;
|
|
2769
|
-
if (colouredOut.some((o) => ft.postVector[o] !== 1)) return null;
|
|
2770
|
-
classes.push({ kind: "consume", inputCol: colouredIn[0], colouredOut });
|
|
2771
|
-
} else if (colouredOut.length !== 0) {
|
|
2772
|
-
if (colouredOut.some((o) => ft.postVector[o] !== 1)) return null;
|
|
2773
|
-
let budgetConsumed = 0;
|
|
2774
|
-
for (const b of budgetIdx) budgetConsumed += ft.preVector[b];
|
|
2775
|
-
if (budgetConsumed < 1) return null;
|
|
2776
|
-
classes.push({ kind: "mint", colouredOut });
|
|
2777
|
-
} else {
|
|
2778
|
-
classes.push({ kind: "untouched" });
|
|
2779
|
-
}
|
|
2780
|
-
}
|
|
2781
|
-
return { coloured, isColoured, k, classes };
|
|
2782
|
-
}
|
|
2783
|
-
function buildLayout(plan, P) {
|
|
2784
|
-
const colUnc = new Array(P).fill(-1);
|
|
2785
|
-
const colCol = Array.from({ length: P }, () => []);
|
|
2786
|
-
const cur = [];
|
|
2787
|
-
const nxt = [];
|
|
2788
|
-
for (let i = 0; i < P; i++) {
|
|
2789
|
-
if (plan.isColoured[i]) {
|
|
2790
|
-
const idxs = [];
|
|
2791
|
-
for (let c = 0; c < plan.k; c++) {
|
|
2792
|
-
idxs.push(cur.length);
|
|
2793
|
-
cur.push(`m${i}_${c}`);
|
|
2794
|
-
nxt.push(`m${i}_${c}p`);
|
|
2795
|
-
}
|
|
2796
|
-
colCol[i] = idxs;
|
|
2797
|
-
} else {
|
|
2798
|
-
colUnc[i] = cur.length;
|
|
2799
|
-
cur.push(`m${i}`);
|
|
2800
|
-
nxt.push(`m${i}p`);
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2803
|
-
return { colUnc, colCol, cur, nxt };
|
|
2804
|
-
}
|
|
2805
|
-
function quantified2(names) {
|
|
2806
|
-
return names.map((v) => `(${v} Int)`).join(" ");
|
|
2807
|
-
}
|
|
2808
|
-
function encodeColoured(plan, flat, initial, property, invariants, sinkPlaces) {
|
|
2809
|
-
const P = flat.places.length;
|
|
2810
|
-
const k = plan.k;
|
|
2811
|
-
const lay = buildLayout(plan, P);
|
|
2812
|
-
const nCols = lay.cur.length;
|
|
2813
|
-
const lines = [];
|
|
2814
|
-
lines.push("(set-logic HORN)");
|
|
2815
|
-
lines.push("");
|
|
2816
|
-
lines.push(`(declare-fun Reachable (${new Array(nCols).fill("Int").join(" ")}) Bool)`);
|
|
2817
|
-
lines.push("(declare-fun Error () Bool)");
|
|
2818
|
-
lines.push("");
|
|
2819
|
-
const init = [];
|
|
2820
|
-
for (let i = 0; i < P; i++) {
|
|
2821
|
-
if (plan.isColoured[i]) {
|
|
2822
|
-
for (let c = 0; c < k; c++) init.push("0");
|
|
2823
|
-
} else {
|
|
2824
|
-
init.push(String(initial.tokens(flat.places[i])));
|
|
2825
|
-
}
|
|
2826
|
-
}
|
|
2827
|
-
lines.push(`(assert (Reachable ${init.join(" ")}))`);
|
|
2828
|
-
lines.push("");
|
|
2829
|
-
for (let ti = 0; ti < plan.classes.length; ti++) {
|
|
2830
|
-
const cls = plan.classes[ti];
|
|
2831
|
-
const ft = flat.transitions[ti];
|
|
2832
|
-
switch (cls.kind) {
|
|
2833
|
-
case "untouched":
|
|
2834
|
-
lines.push(encodeRule(plan, lay, invariants, (enab, upd) => uncolouredIncidence(lay, plan, ft, enab, upd)));
|
|
2835
|
-
break;
|
|
2836
|
-
case "mint":
|
|
2837
|
-
for (let c = 0; c < k; c++) {
|
|
2838
|
-
lines.push(encodeRule(plan, lay, invariants, (enab, upd) => {
|
|
2839
|
-
uncolouredIncidence(lay, plan, ft, enab, upd);
|
|
2840
|
-
for (const q of plan.coloured) enab.push(`(= ${lay.cur[lay.colCol[q][c]]} 0)`);
|
|
2841
|
-
for (const o of cls.colouredOut) {
|
|
2842
|
-
const col = lay.colCol[o][c];
|
|
2843
|
-
upd.push({ col, expr: `(+ ${lay.cur[col]} 1)` });
|
|
2844
|
-
}
|
|
2845
|
-
}));
|
|
2846
|
-
}
|
|
2847
|
-
break;
|
|
2848
|
-
case "join":
|
|
2849
|
-
for (let c = 0; c < k; c++) {
|
|
2850
|
-
lines.push(encodeRule(plan, lay, invariants, (enab, upd) => {
|
|
2851
|
-
uncolouredIncidence(lay, plan, ft, enab, upd);
|
|
2852
|
-
for (const ip of cls.colouredIn) {
|
|
2853
|
-
const col = lay.colCol[ip][c];
|
|
2854
|
-
enab.push(`(>= ${lay.cur[col]} 1)`);
|
|
2855
|
-
upd.push({ col, expr: `(- ${lay.cur[col]} 1)` });
|
|
2856
|
-
}
|
|
2857
|
-
}));
|
|
2858
|
-
}
|
|
2859
|
-
break;
|
|
2860
|
-
case "consume":
|
|
2861
|
-
for (let c = 0; c < k; c++) {
|
|
2862
|
-
lines.push(encodeRule(plan, lay, invariants, (enab, upd) => {
|
|
2863
|
-
uncolouredIncidence(lay, plan, ft, enab, upd);
|
|
2864
|
-
const icol = lay.colCol[cls.inputCol][c];
|
|
2865
|
-
enab.push(`(>= ${lay.cur[icol]} 1)`);
|
|
2866
|
-
upd.push({ col: icol, expr: `(- ${lay.cur[icol]} 1)` });
|
|
2867
|
-
for (const o of cls.colouredOut) {
|
|
2868
|
-
const ocol = lay.colCol[o][c];
|
|
2869
|
-
upd.push({ col: ocol, expr: `(+ ${lay.cur[ocol]} 1)` });
|
|
2870
|
-
}
|
|
2871
|
-
}));
|
|
2872
|
-
}
|
|
2873
|
-
break;
|
|
2874
|
-
}
|
|
2875
|
-
}
|
|
2876
|
-
lines.push("");
|
|
2877
|
-
const error = encodeError(plan, lay, flat, property, sinkPlaces, injectionMap(flat));
|
|
2878
|
-
if (error == null) return null;
|
|
2879
|
-
lines.push(error);
|
|
2880
|
-
lines.push("");
|
|
2881
|
-
lines.push("(assert (not Error))");
|
|
2882
|
-
lines.push("(check-sat)");
|
|
2883
|
-
return { smt2: lines.join("\n"), placeCount: P };
|
|
2884
|
-
}
|
|
2885
|
-
function encodeRule(plan, lay, invariants, fill) {
|
|
2886
|
-
const enab = [];
|
|
2887
|
-
const upd = [];
|
|
2888
|
-
fill(enab, upd);
|
|
2889
|
-
const conditions = [`(Reachable ${lay.cur.join(" ")})`, ...enab];
|
|
2890
|
-
const changed = new Array(lay.cur.length).fill(null);
|
|
2891
|
-
for (const u of upd) changed[u.col] = u.expr;
|
|
2892
|
-
for (let col = 0; col < lay.cur.length; col++) {
|
|
2893
|
-
const expr = changed[col];
|
|
2894
|
-
if (expr != null) {
|
|
2895
|
-
conditions.push(`(= ${lay.nxt[col]} ${expr})`);
|
|
2896
|
-
conditions.push(`(>= ${lay.nxt[col]} 0)`);
|
|
2897
|
-
} else {
|
|
2898
|
-
conditions.push(`(= ${lay.nxt[col]} ${lay.cur[col]})`);
|
|
2899
|
-
}
|
|
2900
|
-
}
|
|
2901
|
-
for (const inv of invariants) {
|
|
2902
|
-
const eq = liftedInvariant(inv, plan, lay, lay.nxt);
|
|
2903
|
-
if (eq != null) conditions.push(eq);
|
|
2904
|
-
}
|
|
2905
|
-
const body = `(and ${conditions.join("\n ")})`;
|
|
2906
|
-
return `(assert (forall (${quantified2([...lay.cur, ...lay.nxt])})
|
|
2907
|
-
(=> ${body}
|
|
2908
|
-
(Reachable ${lay.nxt.join(" ")}))))`;
|
|
2909
|
-
}
|
|
2910
|
-
function uncolouredIncidence(lay, plan, ft, enab, upd) {
|
|
2911
|
-
const P = ft.preVector.length;
|
|
2912
|
-
for (let i = 0; i < P; i++) {
|
|
2913
|
-
if (plan.isColoured[i]) continue;
|
|
2914
|
-
const col = lay.colUnc[i];
|
|
2915
|
-
const pre = ft.preVector[i];
|
|
2916
|
-
if (pre > 0) enab.push(`(>= ${lay.cur[col]} ${pre})`);
|
|
2917
|
-
if (ft.resetPlaces.includes(i) || ft.consumeAll[i]) {
|
|
2918
|
-
upd.push({ col, expr: String(ft.postVector[i]) });
|
|
2919
|
-
} else {
|
|
2920
|
-
const delta = ft.postVector[i] - ft.preVector[i];
|
|
2921
|
-
if (delta > 0) upd.push({ col, expr: `(+ ${lay.cur[col]} ${delta})` });
|
|
2922
|
-
else if (delta < 0) upd.push({ col, expr: `(- ${lay.cur[col]} ${-delta})` });
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
for (const pid of ft.inhibitorPlaces) enab.push(`(= ${lay.cur[lay.colUnc[pid]]} 0)`);
|
|
2926
|
-
for (const pid of ft.readPlaces) enab.push(`(>= ${lay.cur[lay.colUnc[pid]]} 1)`);
|
|
2927
|
-
}
|
|
2928
|
-
function aggregate(plan, lay, place, names) {
|
|
2929
|
-
if (plan.isColoured[place]) {
|
|
2930
|
-
const cols = lay.colCol[place];
|
|
2931
|
-
if (cols.length === 0) return "0";
|
|
2932
|
-
if (cols.length === 1) return names[cols[0]];
|
|
2933
|
-
return `(+ ${cols.map((c) => names[c]).join(" ")})`;
|
|
2934
|
-
}
|
|
2935
|
-
return names[lay.colUnc[place]];
|
|
2936
|
-
}
|
|
2937
|
-
function liftedInvariant(inv, plan, lay, names) {
|
|
2938
|
-
const terms = [];
|
|
2939
|
-
for (const i of [...inv.support].sort((a, b) => a - b)) {
|
|
2940
|
-
const agg = aggregate(plan, lay, i, names);
|
|
2941
|
-
const w = inv.weights[i];
|
|
2942
|
-
terms.push(w === 1 ? agg : `(* ${w} ${agg})`);
|
|
2943
|
-
}
|
|
2944
|
-
if (terms.length === 0) return null;
|
|
2945
|
-
const sum = terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
|
|
2946
|
-
return `(= ${sum} ${inv.constant})`;
|
|
2947
|
-
}
|
|
2948
|
-
function encodeError(plan, lay, flat, property, sinkPlaces, envInj) {
|
|
2949
|
-
const violation = encodeViolation(plan, lay, flat, property, sinkPlaces, envInj);
|
|
2950
|
-
if (violation == null) return null;
|
|
2951
|
-
return `(assert (forall (${quantified2(lay.cur)})
|
|
2952
|
-
(=> (and (Reachable ${lay.cur.join(" ")}) ${violation})
|
|
2953
|
-
Error)))`;
|
|
2954
|
-
}
|
|
2955
|
-
function encodeViolation(plan, lay, flat, property, sinkPlaces, envInj) {
|
|
2956
|
-
const anyPlacePresent = (places) => {
|
|
2957
|
-
const conds = indexOrdered(flat, places).map((pid) => `(>= ${aggregate(plan, lay, pid, lay.cur)} 1)`);
|
|
2958
|
-
return conds.length === 0 ? "false" : `(and ${conds.join(" ")})`;
|
|
2959
|
-
};
|
|
2960
|
-
switch (property.type) {
|
|
2961
|
-
case "place-bound":
|
|
2962
|
-
case "branch-place-bound": {
|
|
2963
|
-
const pid = flat.placeIndex.get(property.place.name);
|
|
2964
|
-
if (pid == null) return null;
|
|
2965
|
-
return `(> ${aggregate(plan, lay, pid, lay.cur)} ${property.bound})`;
|
|
2966
|
-
}
|
|
2967
|
-
case "mutual-exclusion":
|
|
2968
|
-
return anyPlacePresent([property.p1, property.p2]);
|
|
2969
|
-
case "unreachable":
|
|
2970
|
-
return anyPlacePresent(property.places);
|
|
2971
|
-
// DeadlockFree (VER-002): quiescent AND some marked place is not a declared
|
|
2972
|
-
// sink. Mirrors the flat encoder's `stranded` disjunction.
|
|
2973
|
-
case "deadlock-free": {
|
|
2974
|
-
const conds = encodeColouredQuiescent(plan, lay, flat, envInj);
|
|
2975
|
-
if (conds == null) return "false";
|
|
2976
|
-
const sinks = new Set(indexOrdered(flat, sinkPlaces));
|
|
2977
|
-
const stranded = [];
|
|
2978
|
-
for (let pid = 0; pid < flat.places.length; pid++) {
|
|
2979
|
-
if (!sinks.has(pid)) stranded.push(`(>= ${aggregate(plan, lay, pid, lay.cur)} 1)`);
|
|
2980
|
-
}
|
|
2981
|
-
if (stranded.length === 0) return "false";
|
|
2982
|
-
conds.push(`(or ${stranded.join(" ")})`);
|
|
2983
|
-
return joinColoured(conds);
|
|
2984
|
-
}
|
|
2985
|
-
// TerminatesAtSink (VER-002): quiescent AND no declared sink marked.
|
|
2986
|
-
case "terminates-at-sink": {
|
|
2987
|
-
const conds = encodeColouredQuiescent(plan, lay, flat, envInj);
|
|
2988
|
-
if (conds == null) return "false";
|
|
2989
|
-
for (const pid of indexOrdered(flat, sinkPlaces)) {
|
|
2990
|
-
conds.push(`(= ${aggregate(plan, lay, pid, lay.cur)} 0)`);
|
|
2991
|
-
}
|
|
2992
|
-
return joinColoured(conds);
|
|
2993
|
-
}
|
|
2994
|
-
// JoinedOrDeadLettered (NU-040 AC4): quiescent AND `pending` marked, with NO
|
|
2995
|
-
// sink clause.
|
|
2996
|
-
case "joined-or-dead-lettered": {
|
|
2997
|
-
const pid = flat.placeIndex.get(property.pending.name);
|
|
2998
|
-
if (pid == null) return null;
|
|
2999
|
-
const conds = encodeColouredQuiescent(plan, lay, flat, envInj);
|
|
3000
|
-
if (conds == null) return "false";
|
|
3001
|
-
conds.push(`(>= ${aggregate(plan, lay, pid, lay.cur)} 1)`);
|
|
3002
|
-
return joinColoured(conds);
|
|
3003
|
-
}
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
|
-
function uncolouredDisable(ft, lay, plan, envInj, reasons) {
|
|
3007
|
-
let permanentlyDisabled = false;
|
|
3008
|
-
const P = ft.preVector.length;
|
|
3009
|
-
for (let i = 0; i < P; i++) {
|
|
3010
|
-
if (plan.isColoured[i] || ft.preVector[i] === 0) continue;
|
|
3011
|
-
if (envInj.has(i)) {
|
|
3012
|
-
const bound = envInj.get(i);
|
|
3013
|
-
if (bound != null && ft.preVector[i] > bound) permanentlyDisabled = true;
|
|
3014
|
-
continue;
|
|
3015
|
-
}
|
|
3016
|
-
reasons.push(`(< ${lay.cur[lay.colUnc[i]]} ${ft.preVector[i]})`);
|
|
3017
|
-
}
|
|
3018
|
-
for (const inh of ft.inhibitorPlaces) reasons.push(`(> ${lay.cur[lay.colUnc[inh]]} 0)`);
|
|
3019
|
-
for (const rd of ft.readPlaces) {
|
|
3020
|
-
if (envInj.has(rd)) {
|
|
3021
|
-
const bound = envInj.get(rd);
|
|
3022
|
-
if (bound != null && bound < 1) permanentlyDisabled = true;
|
|
3023
|
-
continue;
|
|
3024
|
-
}
|
|
3025
|
-
reasons.push(`(< ${lay.cur[lay.colUnc[rd]]} 1)`);
|
|
3026
|
-
}
|
|
3027
|
-
return permanentlyDisabled;
|
|
3028
|
-
}
|
|
3029
|
-
function colouredDisabledTerm(cls, plan, lay) {
|
|
3030
|
-
const k = plan.k;
|
|
3031
|
-
if (k === 0) {
|
|
3032
|
-
return cls.kind === "untouched" ? null : "true";
|
|
3033
|
-
}
|
|
3034
|
-
switch (cls.kind) {
|
|
3035
|
-
case "untouched":
|
|
3036
|
-
return null;
|
|
3037
|
-
case "mint": {
|
|
3038
|
-
const perColour = [];
|
|
3039
|
-
for (let c = 0; c < k; c++) {
|
|
3040
|
-
const present = plan.coloured.map((q) => `(>= ${lay.cur[lay.colCol[q][c]]} 1)`);
|
|
3041
|
-
perColour.push(`(or ${present.join(" ")})`);
|
|
3042
|
-
}
|
|
3043
|
-
return `(and ${perColour.join(" ")})`;
|
|
3044
|
-
}
|
|
3045
|
-
case "join": {
|
|
3046
|
-
const perColour = [];
|
|
3047
|
-
for (let c = 0; c < k; c++) {
|
|
3048
|
-
const missing = cls.colouredIn.map((i) => `(= ${lay.cur[lay.colCol[i][c]]} 0)`);
|
|
3049
|
-
perColour.push(`(or ${missing.join(" ")})`);
|
|
3050
|
-
}
|
|
3051
|
-
return `(and ${perColour.join(" ")})`;
|
|
3052
|
-
}
|
|
3053
|
-
case "consume": {
|
|
3054
|
-
const perColour = [];
|
|
3055
|
-
for (let c = 0; c < k; c++) perColour.push(`(= ${lay.cur[lay.colCol[cls.inputCol][c]]} 0)`);
|
|
3056
|
-
return `(and ${perColour.join(" ")})`;
|
|
3057
|
-
}
|
|
3058
|
-
}
|
|
3059
|
-
}
|
|
3060
|
-
function joinColoured(conds) {
|
|
3061
|
-
return conds.length === 0 ? "true" : `(and ${conds.join(" ")})`;
|
|
3062
|
-
}
|
|
3063
|
-
function encodeColouredQuiescent(plan, lay, flat, envInj) {
|
|
3064
|
-
const disabledConditions = [];
|
|
3065
|
-
for (let ti = 0; ti < plan.classes.length; ti++) {
|
|
3066
|
-
const cls = plan.classes[ti];
|
|
3067
|
-
const ft = flat.transitions[ti];
|
|
3068
|
-
const reasons = [];
|
|
3069
|
-
const permanentlyDisabled = uncolouredDisable(ft, lay, plan, envInj, reasons);
|
|
3070
|
-
if (permanentlyDisabled) {
|
|
3071
|
-
disabledConditions.push("true");
|
|
3072
|
-
continue;
|
|
3073
|
-
}
|
|
3074
|
-
const term = colouredDisabledTerm(cls, plan, lay);
|
|
3075
|
-
if (term != null) reasons.push(term);
|
|
3076
|
-
if (reasons.length === 0) return null;
|
|
3077
|
-
disabledConditions.push(reasons.length === 1 ? reasons[0] : `(or ${reasons.join(" ")})`);
|
|
3078
|
-
}
|
|
3079
|
-
return disabledConditions;
|
|
3080
|
-
}
|
|
3081
|
-
|
|
3082
|
-
// src/verification/analysis/name-fragment.ts
|
|
3083
|
-
function classify(net, mode, carrierPlaces) {
|
|
3084
|
-
const coloured = /* @__PURE__ */ new Set();
|
|
3085
|
-
let anyMatch = false;
|
|
3086
|
-
for (const t of net.transitions) {
|
|
3087
|
-
if (t.matchSpec !== null) {
|
|
3088
|
-
anyMatch = true;
|
|
3089
|
-
for (const key of t.matchSpec.keys) coloured.add(key.place.name);
|
|
3090
|
-
}
|
|
3091
|
-
}
|
|
3092
|
-
if (!anyMatch || coloured.size === 0) return null;
|
|
3093
|
-
if (mode === "extended") {
|
|
3094
|
-
for (const c of carrierPlaces) coloured.add(c);
|
|
3095
|
-
}
|
|
3096
|
-
for (const t of net.transitions) {
|
|
3097
|
-
if (t.resets.some((r) => coloured.has(r.place.name)) || t.reads.some((r) => coloured.has(r.place.name)) || t.inhibitors.some((i) => coloured.has(i.place.name))) {
|
|
3098
|
-
return null;
|
|
3099
|
-
}
|
|
3100
|
-
}
|
|
3101
|
-
const roles = /* @__PURE__ */ new Map();
|
|
3102
|
-
for (const t of net.transitions) {
|
|
3103
|
-
const colouredInputs = t.inputSpecs.filter((s) => coloured.has(s.place.name));
|
|
3104
|
-
const consumesColoured = colouredInputs.length > 0;
|
|
3105
|
-
let producesColoured = false;
|
|
3106
|
-
if (t.outputSpec !== null) {
|
|
3107
|
-
for (const branch of enumerateBranches(t.outputSpec)) {
|
|
3108
|
-
for (const p of branch) {
|
|
3109
|
-
if (coloured.has(p.name)) producesColoured = true;
|
|
3110
|
-
}
|
|
3111
|
-
}
|
|
3112
|
-
}
|
|
3113
|
-
let role;
|
|
3114
|
-
if (t.matchSpec !== null) {
|
|
3115
|
-
if (producesColoured) return null;
|
|
3116
|
-
const colouredIn = [];
|
|
3117
|
-
for (const key of t.matchSpec.keys) {
|
|
3118
|
-
const place = key.place.name;
|
|
3119
|
-
const required = fixedRequiredCount(t, place);
|
|
3120
|
-
if (required === null) return null;
|
|
3121
|
-
colouredIn.push([place, required]);
|
|
3122
|
-
}
|
|
3123
|
-
colouredIn.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
3124
|
-
role = { type: "join", colouredIn };
|
|
3125
|
-
} else if (consumesColoured) {
|
|
3126
|
-
if (mode === "base") return null;
|
|
3127
|
-
if (colouredInputs.length !== 1) return null;
|
|
3128
|
-
const spec = colouredInputs[0];
|
|
3129
|
-
const countOne = spec.type === "one" || spec.type === "exactly" && spec.count === 1;
|
|
3130
|
-
if (!countOne) return null;
|
|
3131
|
-
role = { type: "consume", colouredInput: spec.place.name };
|
|
3132
|
-
} else if (producesColoured) {
|
|
3133
|
-
role = { type: "mint" };
|
|
3134
|
-
} else {
|
|
3135
|
-
role = { type: "ordinary" };
|
|
3136
|
-
}
|
|
3137
|
-
roles.set(t.name, role);
|
|
3138
|
-
}
|
|
3139
|
-
const colouredOrder = [...coloured].sort();
|
|
3140
|
-
return {
|
|
3141
|
-
colouredOrder,
|
|
3142
|
-
isColoured: (p) => coloured.has(p),
|
|
3143
|
-
role: (tn) => roles.get(tn) ?? { type: "ordinary" }
|
|
3144
|
-
};
|
|
3145
|
-
}
|
|
3146
|
-
function fixedRequiredCount(t, placeName2) {
|
|
3147
|
-
for (const spec of t.inputSpecs) {
|
|
3148
|
-
if (spec.place.name === placeName2) {
|
|
3149
|
-
switch (spec.type) {
|
|
3150
|
-
case "one":
|
|
3151
|
-
return 1;
|
|
3152
|
-
case "exactly":
|
|
3153
|
-
return spec.count;
|
|
3154
|
-
case "all":
|
|
3155
|
-
return null;
|
|
3156
|
-
case "at-least":
|
|
3157
|
-
return null;
|
|
3158
|
-
}
|
|
3159
|
-
}
|
|
3160
|
-
}
|
|
3161
|
-
return null;
|
|
3162
|
-
}
|
|
3163
|
-
|
|
3164
|
-
// src/verification/analysis/name-marking.ts
|
|
3165
|
-
var NameMarking = class _NameMarking {
|
|
3166
|
-
// place name -> (symbol -> count). Only coloured places appear; a place's
|
|
3167
|
-
// total here equals its count in the base MarkingState.
|
|
3168
|
-
perPlace;
|
|
3169
|
-
constructor(perPlace) {
|
|
3170
|
-
this.perPlace = perPlace ?? /* @__PURE__ */ new Map();
|
|
3171
|
-
}
|
|
3172
|
-
copy() {
|
|
3173
|
-
const p = /* @__PURE__ */ new Map();
|
|
3174
|
-
for (const [place, syms] of this.perPlace) {
|
|
3175
|
-
p.set(place, new Map(syms));
|
|
3176
|
-
}
|
|
3177
|
-
return new _NameMarking(p);
|
|
3178
|
-
}
|
|
3179
|
-
add(place, sym, count) {
|
|
3180
|
-
if (count === 0) return;
|
|
3181
|
-
let syms = this.perPlace.get(place);
|
|
3182
|
-
if (!syms) {
|
|
3183
|
-
syms = /* @__PURE__ */ new Map();
|
|
3184
|
-
this.perPlace.set(place, syms);
|
|
3185
|
-
}
|
|
3186
|
-
syms.set(sym, (syms.get(sym) ?? 0) + count);
|
|
3187
|
-
}
|
|
3188
|
-
/** Removes `count` of `sym` from `place`; returns false (unchanged) if fewer present. */
|
|
3189
|
-
remove(place, sym, count) {
|
|
3190
|
-
const syms = this.perPlace.get(place);
|
|
3191
|
-
if (!syms) return false;
|
|
3192
|
-
const have = syms.get(sym);
|
|
3193
|
-
if (have === void 0 || have < count) return false;
|
|
3194
|
-
const left = have - count;
|
|
3195
|
-
if (left === 0) {
|
|
3196
|
-
syms.delete(sym);
|
|
3197
|
-
if (syms.size === 0) this.perPlace.delete(place);
|
|
3198
|
-
} else {
|
|
3199
|
-
syms.set(sym, left);
|
|
3200
|
-
}
|
|
3201
|
-
return true;
|
|
3202
|
-
}
|
|
3203
|
-
countOf(place, sym) {
|
|
3204
|
-
return this.perPlace.get(place)?.get(sym) ?? 0;
|
|
3205
|
-
}
|
|
3206
|
-
symbolsIn(place) {
|
|
3207
|
-
const syms = this.perPlace.get(place);
|
|
3208
|
-
return syms ? [...syms.keys()] : [];
|
|
3209
|
-
}
|
|
3210
|
-
liveSymbols() {
|
|
3211
|
-
const all2 = /* @__PURE__ */ new Set();
|
|
3212
|
-
for (const syms of this.perPlace.values()) {
|
|
3213
|
-
for (const s of syms.keys()) all2.add(s);
|
|
3214
|
-
}
|
|
3215
|
-
return [...all2];
|
|
3216
|
-
}
|
|
3217
|
-
/**
|
|
3218
|
-
* Symmetry-canonical key over `colouredOrder` (the finiteness mechanism). Two
|
|
3219
|
-
* markings differing only by a permutation of symbols produce an identical key
|
|
3220
|
-
* (NU-001). Each symbol's signature is its count vector over `colouredOrder`;
|
|
3221
|
-
* symbols are ranked by (signature, raw id) and emitted per place as a
|
|
3222
|
-
* rank-multiset — a complete invariant of the symbol-permutation orbit.
|
|
3223
|
-
*/
|
|
3224
|
-
canonicalKey(colouredOrder) {
|
|
3225
|
-
const signature = (s) => colouredOrder.map((p) => this.countOf(p, s));
|
|
3226
|
-
const ranked = this.liveSymbols().map((s) => ({ sig: signature(s), sym: s }));
|
|
3227
|
-
ranked.sort((a, b) => {
|
|
3228
|
-
const c = compareNumberArrays(a.sig, b.sig);
|
|
3229
|
-
return c !== 0 ? c : a.sym - b.sym;
|
|
3230
|
-
});
|
|
3231
|
-
const rankOf = /* @__PURE__ */ new Map();
|
|
3232
|
-
ranked.forEach((r, i) => rankOf.set(r.sym, i));
|
|
3233
|
-
const parts = colouredOrder.map((p) => {
|
|
3234
|
-
const syms = this.perPlace.get(p);
|
|
3235
|
-
const entries = [];
|
|
3236
|
-
if (syms) {
|
|
3237
|
-
for (const [s, c] of syms) entries.push([rankOf.get(s), c]);
|
|
3238
|
-
}
|
|
3239
|
-
entries.sort((a, b) => a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1]);
|
|
3240
|
-
const inner = entries.map(([r, c]) => `${r}x${c}`).join(",");
|
|
3241
|
-
return `${p}:{${inner}}`;
|
|
3242
|
-
});
|
|
3243
|
-
return parts.join("#");
|
|
3244
|
-
}
|
|
3245
|
-
};
|
|
3246
|
-
function compareNumberArrays(a, b) {
|
|
3247
|
-
const n = Math.min(a.length, b.length);
|
|
3248
|
-
for (let i = 0; i < n; i++) {
|
|
3249
|
-
if (a[i] !== b[i]) return a[i] - b[i];
|
|
3250
|
-
}
|
|
3251
|
-
return a.length - b.length;
|
|
3252
|
-
}
|
|
3253
|
-
|
|
3254
|
-
// src/verification/analysis/name-state-class.ts
|
|
3255
|
-
var NameStateClass = class {
|
|
3256
|
-
base;
|
|
3257
|
-
names;
|
|
3258
|
-
/** The symmetry-canonical name-partition key (the name layer's intern key). */
|
|
3259
|
-
nameKey;
|
|
3260
|
-
constructor(base, names, colouredOrder, nameKey) {
|
|
3261
|
-
this.base = base;
|
|
3262
|
-
this.names = names;
|
|
3263
|
-
this.nameKey = nameKey ?? names.canonicalKey(colouredOrder);
|
|
3264
|
-
}
|
|
3265
|
-
/** Full dedup key: the base key (marking + DBM zone) joined with the name key. */
|
|
3266
|
-
get key() {
|
|
3267
|
-
return `${baseKeyOf(this.base)}||${this.nameKey}`;
|
|
3268
|
-
}
|
|
3269
|
-
};
|
|
3270
|
-
function baseKeyOf(base) {
|
|
3271
|
-
return `${base.marking.toString()}|${base.firingDomain.toString()}`;
|
|
3272
|
-
}
|
|
3273
|
-
|
|
3274
|
-
// src/verification/analysis/name-state-class-graph.ts
|
|
3275
|
-
var NameStateClassGraph = class _NameStateClassGraph {
|
|
3276
|
-
classes = [];
|
|
3277
|
-
edges = [];
|
|
3278
|
-
_successors = [];
|
|
3279
|
-
_complete = true;
|
|
3280
|
-
isComplete() {
|
|
3281
|
-
return this._complete;
|
|
3282
|
-
}
|
|
3283
|
-
classCount() {
|
|
3284
|
-
return this.classes.length;
|
|
3285
|
-
}
|
|
3286
|
-
successorsOf(idx) {
|
|
3287
|
-
return this._successors[idx];
|
|
3288
|
-
}
|
|
3289
|
-
/** The base count-marking of class `idx` (for property queries). */
|
|
3290
|
-
markingOf(idx) {
|
|
3291
|
-
return this.classes[idx].base.marking;
|
|
3292
|
-
}
|
|
3293
|
-
static build(net, initialMarking, fragment, maxClasses, environmentPlaces, environmentMode, prioritySemantics = "none") {
|
|
3294
|
-
const envMode = environmentMode ?? ignore();
|
|
3295
|
-
const envPlaces = /* @__PURE__ */ new Set();
|
|
3296
|
-
if (environmentPlaces) {
|
|
3297
|
-
for (const ep of environmentPlaces) envPlaces.add(ep.place);
|
|
3298
|
-
}
|
|
3299
|
-
const graph = new _NameStateClassGraph();
|
|
3300
|
-
const base0 = initialStateClass(net, initialMarking, envPlaces, envMode);
|
|
3301
|
-
const baseIntern = /* @__PURE__ */ new Map();
|
|
3302
|
-
const nameIntern = /* @__PURE__ */ new Map();
|
|
3303
|
-
const indexOf = /* @__PURE__ */ new Map();
|
|
3304
|
-
const b0 = internBase(baseIntern, base0);
|
|
3305
|
-
const n0 = internNames(nameIntern, new NameMarking(), fragment.colouredOrder);
|
|
3306
|
-
graph.pushClass(
|
|
3307
|
-
new NameStateClass(b0.base, n0.names, fragment.colouredOrder, n0.nameKey),
|
|
3308
|
-
classId(b0.id, n0.id),
|
|
3309
|
-
indexOf
|
|
3310
|
-
);
|
|
3311
|
-
const sym = { next: 0 };
|
|
3312
|
-
const queue = [0];
|
|
3313
|
-
while (queue.length > 0) {
|
|
3314
|
-
if (graph.classes.length >= maxClasses) {
|
|
3315
|
-
graph._complete = false;
|
|
3316
|
-
break;
|
|
3317
|
-
}
|
|
3318
|
-
const curIdx = queue.shift();
|
|
3319
|
-
const current = graph.classes[curIdx];
|
|
3320
|
-
const enabled = current.base.enabledTransitions;
|
|
3321
|
-
for (let idxL = 0; idxL < enabled.length; idxL++) {
|
|
3322
|
-
const transition = enabled[idxL];
|
|
3323
|
-
if (prioritySemantics === "conflict" && priorityDominated(
|
|
3324
|
-
transition,
|
|
3325
|
-
idxL,
|
|
3326
|
-
enabled,
|
|
3327
|
-
current.base.readyEarliest,
|
|
3328
|
-
current.base.marking,
|
|
3329
|
-
current.names,
|
|
3330
|
-
fragment
|
|
3331
|
-
)) {
|
|
3332
|
-
continue;
|
|
3333
|
-
}
|
|
3334
|
-
const role = fragment.role(transition.name);
|
|
3335
|
-
for (const vt of expandTransition(transition)) {
|
|
3336
|
-
const baseSucc = computeSuccessor(net, current.base, vt, envPlaces, envMode);
|
|
3337
|
-
if (baseSucc === null || baseSucc.isEmpty()) continue;
|
|
3338
|
-
const nameSuccs = nameSuccessors(role, current.names, vt.outputPlaces, fragment, sym);
|
|
3339
|
-
const shared = internBase(baseIntern, baseSucc);
|
|
3340
|
-
for (const nm of nameSuccs) {
|
|
3341
|
-
const sharedNames = internNames(nameIntern, nm, fragment.colouredOrder);
|
|
3342
|
-
const id = classId(shared.id, sharedNames.id);
|
|
3343
|
-
let toIdx = indexOf.get(id);
|
|
3344
|
-
if (toIdx === void 0) {
|
|
3345
|
-
toIdx = graph.classes.length;
|
|
3346
|
-
graph.pushClass(
|
|
3347
|
-
new NameStateClass(shared.base, sharedNames.names, fragment.colouredOrder, sharedNames.nameKey),
|
|
3348
|
-
id,
|
|
3349
|
-
indexOf
|
|
3350
|
-
);
|
|
3351
|
-
queue.push(toIdx);
|
|
3352
|
-
}
|
|
3353
|
-
graph.addEdge(curIdx, toIdx, transition.name);
|
|
3354
|
-
}
|
|
3355
|
-
}
|
|
3356
|
-
}
|
|
3357
|
-
}
|
|
3358
|
-
return graph;
|
|
3359
|
-
}
|
|
3360
|
-
pushClass(c, id, indexOf) {
|
|
3361
|
-
const idx = this.classes.length;
|
|
3362
|
-
this.classes.push(c);
|
|
3363
|
-
this._successors.push([]);
|
|
3364
|
-
indexOf.set(id, idx);
|
|
3365
|
-
}
|
|
3366
|
-
addEdge(from, to, name) {
|
|
3367
|
-
this.edges.push({ from, to, transitionName: name });
|
|
3368
|
-
this._successors[from].push(to);
|
|
3369
|
-
}
|
|
3370
|
-
};
|
|
3371
|
-
function classId(baseId, nameId) {
|
|
3372
|
-
return `${baseId}:${nameId}`;
|
|
3373
|
-
}
|
|
3374
|
-
function internBase(intern, base) {
|
|
3375
|
-
const key = `${baseKeyOf(base)}#${base.readyEarliest.join(",")}`;
|
|
3376
|
-
let entry = intern.get(key);
|
|
3377
|
-
if (entry === void 0) {
|
|
3378
|
-
entry = { id: intern.size, base };
|
|
3379
|
-
intern.set(key, entry);
|
|
3380
|
-
}
|
|
3381
|
-
return entry;
|
|
3382
|
-
}
|
|
3383
|
-
function internNames(intern, names, colouredOrder) {
|
|
3384
|
-
const nameKey = names.canonicalKey(colouredOrder);
|
|
3385
|
-
let entry = intern.get(nameKey);
|
|
3386
|
-
if (entry === void 0) {
|
|
3387
|
-
entry = { id: intern.size, names, nameKey };
|
|
3388
|
-
intern.set(nameKey, entry);
|
|
3389
|
-
}
|
|
3390
|
-
return entry;
|
|
3391
|
-
}
|
|
3392
|
-
var READY_EPS = 1e-9;
|
|
3393
|
-
function priorityDominated(l, idxL, enabled, readyEarliest, marking, names, fragment) {
|
|
3394
|
-
return enabled.some(
|
|
3395
|
-
(h, idxH) => h !== l && h.priority > l.priority && readyEarliest[idxH] <= readyEarliest[idxL] + READY_EPS && willFire(h, names, fragment) && sharesConsumedInput(h, l, marking)
|
|
3396
|
-
);
|
|
3397
|
-
}
|
|
3398
|
-
function willFire(h, names, fragment) {
|
|
3399
|
-
const role = fragment.role(h.name);
|
|
3400
|
-
switch (role.type) {
|
|
3401
|
-
case "join":
|
|
3402
|
-
return enablingSymbols(names, role.colouredIn).length > 0;
|
|
3403
|
-
case "consume":
|
|
3404
|
-
return names.symbolsIn(role.colouredInput).length > 0;
|
|
3405
|
-
case "ordinary":
|
|
3406
|
-
case "mint":
|
|
3407
|
-
return true;
|
|
3408
|
-
default: {
|
|
3409
|
-
const _exhaustive = role;
|
|
3410
|
-
return _exhaustive;
|
|
3411
|
-
}
|
|
3412
|
-
}
|
|
3413
|
-
}
|
|
3414
|
-
function sharesConsumedInput(h, l, marking) {
|
|
3415
|
-
const lIns = /* @__PURE__ */ new Set();
|
|
3416
|
-
for (const p of l.inputPlaces()) lIns.add(p.name);
|
|
3417
|
-
for (const p of h.inputPlaces()) {
|
|
3418
|
-
if (lIns.has(p.name) && marking.tokens(p) < consumedDemand(h, p.name) + consumedDemand(l, p.name)) {
|
|
3419
|
-
return true;
|
|
3420
|
-
}
|
|
3421
|
-
}
|
|
3422
|
-
return false;
|
|
3423
|
-
}
|
|
3424
|
-
function consumedDemand(t, placeName2) {
|
|
3425
|
-
let demand = 0;
|
|
3426
|
-
for (const spec of t.inputSpecs) {
|
|
3427
|
-
if (spec.place.name === placeName2) demand += inputRequiredCount2(spec);
|
|
3428
|
-
}
|
|
3429
|
-
return demand;
|
|
3430
|
-
}
|
|
3431
|
-
function inputRequiredCount2(spec) {
|
|
3432
|
-
switch (spec.type) {
|
|
3433
|
-
case "one":
|
|
3434
|
-
return 1;
|
|
3435
|
-
case "exactly":
|
|
3436
|
-
return spec.count;
|
|
3437
|
-
case "all":
|
|
3438
|
-
return 1;
|
|
3439
|
-
case "at-least":
|
|
3440
|
-
return spec.minimum;
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3443
|
-
function colouredOutputs(outputPlaces, fragment) {
|
|
3444
|
-
return [...outputPlaces].filter((p) => fragment.isColoured(p.name)).map((p) => p.name);
|
|
3445
|
-
}
|
|
3446
|
-
function nameSuccessors(role, names, outputPlaces, fragment, sym) {
|
|
3447
|
-
switch (role.type) {
|
|
3448
|
-
case "ordinary":
|
|
3449
|
-
return [names.copy()];
|
|
3450
|
-
case "mint": {
|
|
3451
|
-
const colouredOut = colouredOutputs(outputPlaces, fragment);
|
|
3452
|
-
const nm = names.copy();
|
|
3453
|
-
if (colouredOut.length > 0) {
|
|
3454
|
-
const fresh = sym.next++;
|
|
3455
|
-
for (const p of colouredOut) nm.add(p, fresh, 1);
|
|
3456
|
-
}
|
|
3457
|
-
return [nm];
|
|
3458
|
-
}
|
|
3459
|
-
case "join": {
|
|
3460
|
-
const result = [];
|
|
3461
|
-
for (const s of enablingSymbols(names, role.colouredIn)) {
|
|
3462
|
-
const nm = names.copy();
|
|
3463
|
-
for (const [p, req] of role.colouredIn) nm.remove(p, s, req);
|
|
3464
|
-
result.push(nm);
|
|
3465
|
-
}
|
|
3466
|
-
return result;
|
|
3467
|
-
}
|
|
3468
|
-
case "consume": {
|
|
3469
|
-
const colouredOut = colouredOutputs(outputPlaces, fragment);
|
|
3470
|
-
const result = [];
|
|
3471
|
-
for (const s of names.symbolsIn(role.colouredInput)) {
|
|
3472
|
-
const nm = names.copy();
|
|
3473
|
-
nm.remove(role.colouredInput, s, 1);
|
|
3474
|
-
for (const p of colouredOut) nm.add(p, s, 1);
|
|
3475
|
-
result.push(nm);
|
|
3476
|
-
}
|
|
3477
|
-
return result;
|
|
3478
|
-
}
|
|
3479
|
-
}
|
|
3480
|
-
}
|
|
3481
|
-
function enablingSymbols(names, colouredIn) {
|
|
3482
|
-
if (colouredIn.length === 0) return [];
|
|
3483
|
-
const [firstPlace, firstReq] = colouredIn[0];
|
|
3484
|
-
const result = [];
|
|
3485
|
-
for (const s of names.symbolsIn(firstPlace)) {
|
|
3486
|
-
if (names.countOf(firstPlace, s) < firstReq) continue;
|
|
3487
|
-
let ok = true;
|
|
3488
|
-
for (let i = 1; i < colouredIn.length; i++) {
|
|
3489
|
-
const [p, req] = colouredIn[i];
|
|
3490
|
-
if (names.countOf(p, s) < req) {
|
|
3491
|
-
ok = false;
|
|
3492
|
-
break;
|
|
3493
|
-
}
|
|
3494
|
-
}
|
|
3495
|
-
if (ok) result.push(s);
|
|
3496
|
-
}
|
|
3497
|
-
return result;
|
|
3498
|
-
}
|
|
3499
|
-
|
|
3500
|
-
// src/verification/nu-scg-verifier.ts
|
|
3501
|
-
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";
|
|
3502
|
-
function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces, environmentMode, maxClasses, fragmentMode, carrierPlaces, prioritySemantics) {
|
|
3503
|
-
const fragment = classify(net, fragmentMode, carrierPlaces);
|
|
3504
|
-
if (fragment === null) return null;
|
|
3505
|
-
for (const p of initial.placesWithTokens()) {
|
|
3506
|
-
if (fragment.isColoured(p.name)) return null;
|
|
3507
|
-
}
|
|
3508
|
-
const scg = NameStateClassGraph.build(
|
|
3509
|
-
net,
|
|
3510
|
-
initial,
|
|
3511
|
-
fragment,
|
|
3512
|
-
maxClasses,
|
|
3513
|
-
environmentPlaces,
|
|
3514
|
-
environmentMode,
|
|
3515
|
-
prioritySemantics
|
|
3516
|
-
);
|
|
3517
|
-
if (!scg.isComplete()) {
|
|
3518
|
-
return {
|
|
3519
|
-
verdict: {
|
|
3520
|
-
type: "unknown",
|
|
3521
|
-
reason: `\u03BD name-aware state-class graph truncated at ${maxClasses} classes \u2014 the live correlation pool is not structurally bounded; reachability over unbounded fresh names is undecidable (NU-050, Route B). Declare a budget place to bound the live pool, or raise nuMaxClasses.`
|
|
3522
|
-
},
|
|
3523
|
-
trace: [],
|
|
3524
|
-
transitions: [],
|
|
3525
|
-
note: "",
|
|
3526
|
-
classCount: scg.classCount()
|
|
3527
|
-
};
|
|
3528
|
-
}
|
|
3529
|
-
const violating = decide(scg, property, sinkPlaces);
|
|
3530
|
-
if (violating >= 0) {
|
|
3531
|
-
const [trace, transitions] = counterexamplePath(scg, violating);
|
|
3532
|
-
return { verdict: { type: "violated" }, trace, transitions, note: NOTE_EXACT, classCount: scg.classCount() };
|
|
3533
|
-
}
|
|
3534
|
-
return {
|
|
3535
|
-
verdict: { type: "proven", method: "\u03BD name-partition SCG (NU-050, Route B)", inductiveInvariant: null },
|
|
3536
|
-
trace: [],
|
|
3537
|
-
transitions: [],
|
|
3538
|
-
note: NOTE_EXACT,
|
|
3539
|
-
classCount: scg.classCount()
|
|
3540
|
-
};
|
|
3541
|
-
}
|
|
3542
|
-
function decide(scg, property, sinkPlaces) {
|
|
3543
|
-
const firstWhere = (pred) => {
|
|
3544
|
-
for (let i = 0; i < scg.classCount(); i++) {
|
|
3545
|
-
if (pred(i)) return i;
|
|
3546
|
-
}
|
|
3547
|
-
return -1;
|
|
3548
|
-
};
|
|
3549
|
-
switch (property.type) {
|
|
3550
|
-
case "place-bound":
|
|
3551
|
-
case "branch-place-bound":
|
|
3552
|
-
return firstWhere((i) => scg.markingOf(i).tokens(property.place) > property.bound);
|
|
3553
|
-
case "unreachable":
|
|
3554
|
-
return firstWhere((i) => {
|
|
3555
|
-
const m = scg.markingOf(i);
|
|
3556
|
-
for (const p of property.places) {
|
|
3557
|
-
if (!m.hasTokens(p)) return false;
|
|
3558
|
-
}
|
|
3559
|
-
return true;
|
|
3560
|
-
});
|
|
3561
|
-
case "mutual-exclusion":
|
|
3562
|
-
return firstWhere((i) => {
|
|
3563
|
-
const m = scg.markingOf(i);
|
|
3564
|
-
return m.hasTokens(property.p1) && m.hasTokens(property.p2);
|
|
3565
|
-
});
|
|
3566
|
-
// DeadlockFree (VER-002): a quiescent class that strands a token — some marked
|
|
3567
|
-
// place is not a declared sink. The empty marking strands nothing (AC4).
|
|
3568
|
-
case "deadlock-free":
|
|
3569
|
-
return firstWhere((i) => scg.successorsOf(i).length === 0 && !allTokensInSinks(scg.markingOf(i), sinkPlaces));
|
|
3570
|
-
// TerminatesAtSink (VER-002): a quiescent class that marks NO declared sink.
|
|
3571
|
-
// Inverts with DeadlockFree on the empty marking, by design.
|
|
3572
|
-
case "terminates-at-sink":
|
|
3573
|
-
return firstWhere((i) => scg.successorsOf(i).length === 0 && !anySinkMarked(scg.markingOf(i), sinkPlaces));
|
|
3574
|
-
// JoinedOrDeadLettered (NU-040 AC4): a quiescent class still holding a pending
|
|
3575
|
-
// token. No sink clause.
|
|
3576
|
-
case "joined-or-dead-lettered":
|
|
3577
|
-
return firstWhere((i) => scg.successorsOf(i).length === 0 && scg.markingOf(i).hasTokens(property.pending));
|
|
3578
|
-
}
|
|
3579
|
-
}
|
|
3580
|
-
function allTokensInSinks(m, sinks) {
|
|
3581
|
-
const sinkNames = /* @__PURE__ */ new Set();
|
|
3582
|
-
for (const s of sinks) sinkNames.add(s.name);
|
|
3583
|
-
for (const p of m.placesWithTokens()) {
|
|
3584
|
-
if (!sinkNames.has(p.name)) return false;
|
|
3585
|
-
}
|
|
3586
|
-
return true;
|
|
3587
|
-
}
|
|
3588
|
-
function anySinkMarked(m, sinks) {
|
|
3589
|
-
const sinkNames = /* @__PURE__ */ new Set();
|
|
3590
|
-
for (const s of sinks) sinkNames.add(s.name);
|
|
3591
|
-
for (const p of m.placesWithTokens()) {
|
|
3592
|
-
if (sinkNames.has(p.name)) return true;
|
|
3593
|
-
}
|
|
3594
|
-
return false;
|
|
3595
|
-
}
|
|
3596
|
-
function counterexamplePath(scg, target) {
|
|
3597
|
-
const n = scg.classCount();
|
|
3598
|
-
const parent = new Array(n).fill(-1);
|
|
3599
|
-
const via = new Array(n).fill("");
|
|
3600
|
-
const visited = new Array(n).fill(false);
|
|
3601
|
-
visited[0] = true;
|
|
3602
|
-
const queue = [0];
|
|
3603
|
-
while (queue.length > 0) {
|
|
3604
|
-
const u = queue.shift();
|
|
3605
|
-
if (u === target) break;
|
|
3606
|
-
for (const e of scg.edges) {
|
|
3607
|
-
if (e.from === u && !visited[e.to]) {
|
|
3608
|
-
visited[e.to] = true;
|
|
3609
|
-
parent[e.to] = u;
|
|
3610
|
-
via[e.to] = e.transitionName;
|
|
3611
|
-
queue.push(e.to);
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
|
-
const chain = [];
|
|
3616
|
-
for (let cur = target; cur !== -1; cur = parent[cur]) {
|
|
3617
|
-
chain.push(cur);
|
|
3618
|
-
}
|
|
3619
|
-
chain.reverse();
|
|
3620
|
-
const markings = chain.map((i) => scg.markingOf(i));
|
|
3621
|
-
const transitions = chain.slice(1).map((i) => via[i]);
|
|
3622
|
-
return [markings, transitions];
|
|
3623
|
-
}
|
|
3624
|
-
|
|
3625
|
-
// src/verification/smt-verifier.ts
|
|
3626
|
-
var IGNORE_MODE_VACUITY_REASON = "environment places present but not modeled (mode=ignore); a proof would be vacuous \u2014 use alwaysAvailable() or bounded(k) to model external injection";
|
|
3627
|
-
var SmtVerifier = class _SmtVerifier {
|
|
3628
|
-
constructor(net) {
|
|
3629
|
-
this.net = net;
|
|
3630
|
-
}
|
|
3631
|
-
net;
|
|
3632
|
-
_initialMarking = MarkingState.empty();
|
|
3633
|
-
_property = deadlockFree();
|
|
3634
|
-
_environmentPlaces = /* @__PURE__ */ new Set();
|
|
3635
|
-
_sinkPlaces = /* @__PURE__ */ new Set();
|
|
3636
|
-
_budgetPlaces = /* @__PURE__ */ new Set();
|
|
3637
|
-
_environmentMode = alwaysAvailable();
|
|
3638
|
-
_timeoutMs = 6e4;
|
|
3639
|
-
_certificateCheck = true;
|
|
3640
|
-
_counterexampleReplay = true;
|
|
3641
|
-
_semiflowInvariants = false;
|
|
3642
|
-
_nuMaxClasses = 1e5;
|
|
3643
|
-
_fragmentMode = "base";
|
|
3644
|
-
_carrierPlaces = /* @__PURE__ */ new Set();
|
|
3645
|
-
_prioritySemantics = "none";
|
|
3646
|
-
static forNet(net) {
|
|
3647
|
-
return new _SmtVerifier(net);
|
|
3648
|
-
}
|
|
3649
|
-
initialMarking(arg) {
|
|
3650
|
-
if (arg instanceof MarkingState) {
|
|
3651
|
-
this._initialMarking = arg;
|
|
3652
|
-
} else {
|
|
3653
|
-
const builder = MarkingState.builder();
|
|
3654
|
-
arg(builder);
|
|
3655
|
-
this._initialMarking = builder.build();
|
|
3656
|
-
}
|
|
3657
|
-
return this;
|
|
3658
|
-
}
|
|
3659
|
-
property(property) {
|
|
3660
|
-
this._property = property;
|
|
3661
|
-
return this;
|
|
3662
|
-
}
|
|
3663
|
-
environmentPlaces(...places) {
|
|
3664
|
-
for (const p of places) this._environmentPlaces.add(p);
|
|
3665
|
-
return this;
|
|
3666
|
-
}
|
|
3667
|
-
environmentMode(mode) {
|
|
3668
|
-
this._environmentMode = mode;
|
|
3669
|
-
return this;
|
|
3670
|
-
}
|
|
3671
|
-
/**
|
|
3672
|
-
* Declares expected sink (terminal) places for deadlock-freedom analysis.
|
|
3673
|
-
* Markings where any sink place has a token are not considered deadlocks.
|
|
3674
|
-
*/
|
|
3675
|
-
sinkPlaces(...places) {
|
|
3676
|
-
for (const p of places) this._sinkPlaces.add(p);
|
|
3677
|
-
return this;
|
|
3678
|
-
}
|
|
3679
|
-
/**
|
|
3680
|
-
* Declares ν-net budget places (NU-040): places whose token count bounds the
|
|
3681
|
-
* live correlation pool (they gate fresh-name minting). Declaring at least one
|
|
3682
|
-
* places the net in the decidable bounded fragment, so reachability-safety
|
|
3683
|
-
* properties over its ν-joins are verified (the matched transitions are
|
|
3684
|
-
* over-approximated). Without any budget place, a net that mints fresh names
|
|
3685
|
-
* is treated as unbounded and the verifier returns `unknown` (NU-050).
|
|
3686
|
-
*/
|
|
3687
|
-
budgetPlaces(...places) {
|
|
3688
|
-
for (const p of places) this._budgetPlaces.add(p.name);
|
|
3689
|
-
return this;
|
|
3690
|
-
}
|
|
3691
|
-
timeout(ms) {
|
|
3692
|
-
this._timeoutMs = ms;
|
|
3693
|
-
return this;
|
|
3694
|
-
}
|
|
3695
|
-
/**
|
|
3696
|
-
* Enables/disables the independent IC3 certificate check (default: enabled).
|
|
3697
|
-
*
|
|
3698
|
-
* When a proven verdict comes from the IC3/Spacer path on the flat count
|
|
3699
|
-
* encoding, the synthesized inductive invariant is re-validated with a plain
|
|
3700
|
-
* solver against the UNSTRENGTHENED step relation — VC1 (init), VC2
|
|
3701
|
-
* (consecution), VC3 (safety) — so a Spacer or encoder defect cannot certify
|
|
3702
|
-
* a false PROVEN. A certificate that fails validation downgrades the verdict
|
|
3703
|
-
* to unknown. Structural proofs and the coloured ν-encoding are unaffected.
|
|
3704
|
-
*/
|
|
3705
|
-
certificateCheck(enabled) {
|
|
3706
|
-
this._certificateCheck = enabled;
|
|
3707
|
-
return this;
|
|
3708
|
-
}
|
|
3709
|
-
/**
|
|
3710
|
-
* Enables/disables abstract counterexample replay (default: enabled).
|
|
3711
|
-
*
|
|
3712
|
-
* When a violated verdict comes from the flat count encoding, the decoded
|
|
3713
|
-
* counterexample states (an order-free set — the derivation tree is walked in
|
|
3714
|
-
* traversal order, not firing order) are re-executed TS-side against the
|
|
3715
|
-
* abstract semantics the encoder emits (Lean's `fireA`, Basic.lean), searching
|
|
3716
|
-
* for a firing order from M₀ to a property-violating marking. See
|
|
3717
|
-
* `SmtVerificationResult.counterexampleConfirmed` for how each outcome lands.
|
|
3718
|
-
*/
|
|
3719
|
-
counterexampleReplay(enabled) {
|
|
3720
|
-
this._counterexampleReplay = enabled;
|
|
3721
|
-
return this;
|
|
3722
|
-
}
|
|
3723
|
-
/**
|
|
3724
|
-
* Also hands the validated **P-semiflows** to the encoders as invariants
|
|
3725
|
-
* (VER-007; default: disabled — the encoders then see only the null-space basis).
|
|
3726
|
-
*
|
|
3727
|
-
* Every validated semiflow is a conservation law in its own right (`y >= 0`,
|
|
3728
|
-
* `y·C = 0`, `y·M0` exact, zero weight on every reset / consume-all place), and
|
|
3729
|
-
* the Farkas enumeration returns the *minimal* laws of the net. The null-space
|
|
3730
|
-
* basis the encoders get by default is one basis of many: elimination hands back
|
|
3731
|
-
* mixed-sign rows (discarded as not semi-positive) or rows that fold a reset place
|
|
3732
|
-
* into a chain whose other combinations avoid it (dropped by the H1 guard). On a
|
|
3733
|
-
* net with a few reset arcs that can lose every law of the chains those arcs
|
|
3734
|
-
* touch, and without them IC3 has to rediscover the conservation of each chain —
|
|
3735
|
-
* on a ~100-place net it does not within any practical budget.
|
|
3736
|
-
*
|
|
3737
|
-
* **Turn this on if the net has any `all()` / `atLeast(n)` or reset arc on a busy
|
|
3738
|
-
* place** — draining an input queue is the everyday case. Every basis row whose
|
|
3739
|
-
* support touches such a place fails the H1 guard and is dropped, so the encoders
|
|
3740
|
-
* run on a deficient invariant set and nothing in the report says a law is missing
|
|
3741
|
-
* beyond the `Dropped` lines.
|
|
3742
|
-
*
|
|
3743
|
-
* This reaches the **name-coloured** encoder (NU-050) as well as the flat one, and
|
|
3744
|
-
* it matters most there. On a 113-place ν-net, whole-net deadlock-freedom went from
|
|
3745
|
-
* `unknown` after 50 minutes to `proven` in about 15 seconds with this option as the
|
|
3746
|
-
* only change; on the flat path, reachability-safety queries that timed out at 120 s
|
|
3747
|
-
* close in about a second.
|
|
3748
|
-
*
|
|
3749
|
-
* Soundness is unchanged: the semiflows pass the same exact re-validation as the
|
|
3750
|
-
* basis rows, the union is pure strengthening (`Semiflow.lean`,
|
|
3751
|
-
* `semiflow_union_sound`), and the certificate check re-proves the strengthened
|
|
3752
|
-
* invariant — that check is flat-path only, so a coloured `proven` reports
|
|
3753
|
-
* `Certificate check: not applicable (name-coloured encoding)`. Off by default so
|
|
3754
|
-
* reports stay byte-equal.
|
|
3755
|
-
*/
|
|
3756
|
-
semiflowInvariants(enabled) {
|
|
3757
|
-
this._semiflowInvariants = enabled;
|
|
3758
|
-
return this;
|
|
3759
|
-
}
|
|
3760
|
-
/**
|
|
3761
|
-
* Sets the class-count cap for the ν-aware state-class-graph analysis (NU-050,
|
|
3762
|
-
* Route B). When the symbolic name-aware graph would exceed this, the analysis
|
|
3763
|
-
* truncates and the verdict is `unknown` (the live correlation pool is not
|
|
3764
|
-
* structurally bounded). Default 100_000.
|
|
3765
|
-
*/
|
|
3766
|
-
nuMaxClasses(max) {
|
|
3767
|
-
this._nuMaxClasses = max;
|
|
3768
|
-
return this;
|
|
3769
|
-
}
|
|
3770
|
-
/**
|
|
3771
|
-
* Selects the ν-net coloured-place fragment for Route B (NU-051). `base`
|
|
3772
|
-
* (default) admits the shipped mint → matched-join fragment only; `extended`
|
|
3773
|
-
* additionally admits the opt-in coloured-consumer (drain/relay) role and the
|
|
3774
|
-
* declared {@link carrierPlaces}. When `extended` is requested but the net
|
|
3775
|
-
* falls outside the coloured-consumer fragment, Route B declines and a short
|
|
3776
|
-
* note is appended to the report before falling back to the sound
|
|
3777
|
-
* over-approximation.
|
|
3778
|
-
*/
|
|
3779
|
-
fragmentMode(mode) {
|
|
3780
|
-
this._fragmentMode = mode;
|
|
3781
|
-
return this;
|
|
3782
|
-
}
|
|
3783
|
-
/**
|
|
3784
|
-
* Declares ν-net *carrier* places (NU-051, EXTENDED only): intermediate places
|
|
3785
|
-
* that carry a fresh name from the minting fork onward to a ν-join input. Under
|
|
3786
|
-
* {@link fragmentMode} `extended` they are unioned into the coloured set so the
|
|
3787
|
-
* existing mint co-mints one fresh name into all of them; under `base` they are
|
|
3788
|
-
* ignored. Accumulating. Throws if a declared place is not in the net — a
|
|
3789
|
-
* mistyped carrier name would let two fork branches mint independent names, so
|
|
3790
|
-
* the join never becomes name-enabled and the verifier would otherwise report a
|
|
3791
|
-
* confident false deadlock; it must surface, never silently proceed.
|
|
3792
|
-
*/
|
|
3793
|
-
carrierPlaces(...places) {
|
|
3794
|
-
for (const p of places) {
|
|
3795
|
-
if (![...this.net.places].some((np) => np.name === p.name)) {
|
|
3796
|
-
throw new Error(`declared carrier place '${p.name}' not in the net`);
|
|
3797
|
-
}
|
|
3798
|
-
this._carrierPlaces.add(p.name);
|
|
3799
|
-
}
|
|
3800
|
-
return this;
|
|
3801
|
-
}
|
|
3802
|
-
/**
|
|
3803
|
-
* Selects how the Route-B name-aware analyzer treats transition priority
|
|
3804
|
-
* (NU-052). Defaults to `'none'` (the priority-blind over-approximation).
|
|
3805
|
-
* `'conflict'` models the executor's conflict-only priority resolution, so a
|
|
3806
|
-
* lower-priority transition pre-empted by a conflicting, no-later-ready,
|
|
3807
|
-
* strictly-higher-priority one is not explored — removing spurious
|
|
3808
|
-
* dead-letter-drain stalls the eager, priority-ordered executor never produces.
|
|
3809
|
-
*/
|
|
3810
|
-
prioritySemantics(semantics) {
|
|
3811
|
-
this._prioritySemantics = semantics;
|
|
3812
|
-
return this;
|
|
3813
|
-
}
|
|
3814
|
-
/**
|
|
3815
|
-
* The name-coloured plan and its encoding, or a null plan when the net is outside
|
|
3816
|
-
* the fragment (NU-050) and a null encoding when the property names a place the net
|
|
3817
|
-
* does not resolve.
|
|
3818
|
-
*
|
|
3819
|
-
* {@link verify} and {@link encodeScripts} share this deliberately. They used to
|
|
3820
|
-
* invoke `buildColouredPlan` and `encodeColoured` separately, so handing the encoder
|
|
3821
|
-
* the wrong one of the two lists changed only one of them — and the script-parity
|
|
3822
|
-
* goldens are generated from `encodeScripts`. Unifying the invocation closes that. It
|
|
3823
|
-
* does not make the two paths identical: each still computes its own invariant and
|
|
3824
|
-
* semiflow lists, so they can still drift through the arguments rather than the call.
|
|
3825
|
-
*
|
|
3826
|
-
* `invariants` is what the encoder conjoins into every rule body (the null-space
|
|
3827
|
-
* basis, unioned with the semiflows when VER-007 is enabled); `semiflows` sets the
|
|
3828
|
-
* colour-slot bound k (NU-053). They are not the same list.
|
|
3829
|
-
*/
|
|
3830
|
-
colouredAttempt(flatNet, invariants, semiflows) {
|
|
3831
|
-
const hasMatch = [...this.net.transitions].some((t) => t.matchSpec !== null);
|
|
3832
|
-
const nuBounded = this._budgetPlaces.size > 0;
|
|
3833
|
-
if (!hasMatch || !nuBounded) return { plan: null, encoding: null };
|
|
3834
|
-
const plan = buildColouredPlan(
|
|
3835
|
-
this.net,
|
|
3836
|
-
flatNet,
|
|
3837
|
-
this._initialMarking,
|
|
3838
|
-
this._budgetPlaces,
|
|
3839
|
-
this._fragmentMode,
|
|
3840
|
-
this._carrierPlaces,
|
|
3841
|
-
semiflows
|
|
3842
|
-
);
|
|
3843
|
-
if (plan == null) return { plan: null, encoding: null };
|
|
3844
|
-
return {
|
|
3845
|
-
plan,
|
|
3846
|
-
encoding: encodeColoured(
|
|
3847
|
-
plan,
|
|
3848
|
-
flatNet,
|
|
3849
|
-
this._initialMarking,
|
|
3850
|
-
this._property,
|
|
3851
|
-
invariants,
|
|
3852
|
-
this._sinkPlaces
|
|
3853
|
-
)
|
|
3854
|
-
};
|
|
3855
|
-
}
|
|
3856
|
-
/**
|
|
3857
|
-
* The SMT-LIB2 scripts {@link verify} would send to z3 for this configuration,
|
|
3858
|
-
* without running a solver (VER-013 AC1): the HORN query (flat, or name-coloured
|
|
3859
|
-
* when a declared budget puts the net on Route A's exact encoding) and, for the
|
|
3860
|
-
* flat encoding, the certificate-check script built around
|
|
3861
|
-
* {@link placeholderCertificate}. This is what the cross-language golden tests diff
|
|
3862
|
-
* byte for byte. Route B, the structural pre-check and the unresolved-place
|
|
3863
|
-
* refusal are bypassed: it is what Route A encodes.
|
|
3864
|
-
*/
|
|
3865
|
-
encodeScripts() {
|
|
3866
|
-
requireOutputProducingActions(this.net);
|
|
3867
|
-
const flatNet = flatten(this.net, this._environmentPlaces, this._environmentMode);
|
|
3868
|
-
const matrix = IncidenceMatrix.from(flatNet);
|
|
3869
|
-
const { valid: basis } = validateInvariantsExact(
|
|
3870
|
-
matrix,
|
|
3871
|
-
computePInvariants(matrix, flatNet, this._initialMarking),
|
|
3872
|
-
flatNet,
|
|
3873
|
-
this._initialMarking
|
|
3874
|
-
);
|
|
3875
|
-
const { valid: semiflows } = validateInvariantsExact(
|
|
3876
|
-
matrix,
|
|
3877
|
-
computePSemiflows(matrix, flatNet, this._initialMarking),
|
|
3878
|
-
flatNet,
|
|
3879
|
-
this._initialMarking
|
|
3880
|
-
);
|
|
3881
|
-
let invariants = basis;
|
|
3882
|
-
if (this._semiflowInvariants) invariants = strengthenWithSemiflows(basis, semiflows).invariants;
|
|
3883
|
-
invariants = canonicalInvariantOrder(invariants);
|
|
3884
|
-
const attempt = this.colouredAttempt(flatNet, invariants, semiflows);
|
|
3885
|
-
if (attempt.encoding != null) {
|
|
3886
|
-
return { horn: attempt.encoding.smt2, certificate: null, coloured: true };
|
|
3887
|
-
}
|
|
3888
|
-
const horn = encode(flatNet, this._initialMarking, this._property, invariants, this._sinkPlaces, this._counterexampleReplay).smt2;
|
|
3889
|
-
const certificate = vcScript(
|
|
3890
|
-
placeholderCertificate(flatNet.places.length),
|
|
3891
|
-
flatNet,
|
|
3892
|
-
this._initialMarking,
|
|
3893
|
-
this._property,
|
|
3894
|
-
this._sinkPlaces,
|
|
3895
|
-
invariants
|
|
3896
|
-
);
|
|
3897
|
-
return { horn, certificate, coloured: false };
|
|
3898
|
-
}
|
|
3899
|
-
/**
|
|
3900
|
-
* Runs the verification pipeline.
|
|
3901
|
-
*
|
|
3902
|
-
* @throws Error if the net violates CORE-043 — verification rejects the same nets execution rejects.
|
|
3903
|
-
*/
|
|
3904
|
-
async verify() {
|
|
3905
|
-
requireOutputProducingActions(this.net);
|
|
3906
|
-
const start = performance.now();
|
|
3907
|
-
const report = [];
|
|
3908
|
-
report.push("=== IC3/PDR SAFETY VERIFICATION ===\n");
|
|
3909
|
-
report.push(`Net: ${this.net.name}`);
|
|
3910
|
-
const propDesc = this._sinkPlaces.size === 0 ? propertyDescription(this._property) : `${propertyDescription(this._property)} (sinks: ${[...this._sinkPlaces].map((p) => p.name).join(", ")})`;
|
|
3911
|
-
report.push(`Property: ${propDesc}`);
|
|
3912
|
-
report.push(`Timeout: ${(this._timeoutMs / 1e3).toFixed(0)}s
|
|
3913
|
-
`);
|
|
3914
|
-
const hasMatch = [...this.net.transitions].some((t) => t.matchSpec !== null);
|
|
3915
|
-
const nuBounded = this._budgetPlaces.size > 0;
|
|
3916
|
-
if (hasMatch && (!isReachabilitySafety(this._property) || !nuBounded)) {
|
|
3917
|
-
const outcome = verifyViaNameScg(
|
|
3918
|
-
this.net,
|
|
3919
|
-
this._initialMarking,
|
|
3920
|
-
this._property,
|
|
3921
|
-
this._sinkPlaces,
|
|
3922
|
-
this._environmentPlaces,
|
|
3923
|
-
this._environmentMode,
|
|
3924
|
-
this._nuMaxClasses,
|
|
3925
|
-
this._fragmentMode,
|
|
3926
|
-
this._carrierPlaces,
|
|
3927
|
-
this._prioritySemantics
|
|
3928
|
-
);
|
|
3929
|
-
const deferToRouteA = outcome !== null && outcome.verdict.type === "unknown" && !isReachabilitySafety(this._property) && nuBounded;
|
|
3930
|
-
if (outcome !== null && !deferToRouteA) {
|
|
3931
|
-
report.push("=== \u03BD-net Route B: name-aware state-class graph (NU-050) ===");
|
|
3932
|
-
report.push(` Name-partition state classes: ${outcome.classCount}`);
|
|
3933
|
-
report.push(outcome.note);
|
|
3934
|
-
if (outcome.transitions.length > 0) {
|
|
3935
|
-
report.push(` Counterexample trace: ${outcome.trace.length} states, ${outcome.transitions.length} transitions`);
|
|
3936
|
-
}
|
|
3937
|
-
let routeBVerdict = outcome.verdict;
|
|
3938
|
-
if (routeBVerdict.type === "proven" && this._environmentPlaces.size > 0 && this._environmentMode.type === "ignore") {
|
|
3939
|
-
report.push(` Downgraded to UNKNOWN: ${IGNORE_MODE_VACUITY_REASON}`);
|
|
3940
|
-
routeBVerdict = { type: "unknown", reason: IGNORE_MODE_VACUITY_REASON };
|
|
3941
|
-
}
|
|
3942
|
-
return buildResult(
|
|
3943
|
-
routeBVerdict,
|
|
3944
|
-
report.join("\n"),
|
|
3945
|
-
[],
|
|
3946
|
-
[],
|
|
3947
|
-
outcome.trace,
|
|
3948
|
-
outcome.transitions,
|
|
3949
|
-
performance.now() - start,
|
|
3950
|
-
{
|
|
3951
|
-
places: [...this.net.places].length,
|
|
3952
|
-
transitions: [...this.net.transitions].length,
|
|
3953
|
-
invariantsFound: 0,
|
|
3954
|
-
structuralResult: "n/a (\u03BD name-partition SCG)"
|
|
3955
|
-
}
|
|
3956
|
-
);
|
|
3957
|
-
} else if (deferToRouteA) {
|
|
3958
|
-
report.push(
|
|
3959
|
-
"\u03BD-net Route B inconclusive (name-partition truncated); deferring to Route A coloured IC3/PDR (NU-053)."
|
|
3960
|
-
);
|
|
3961
|
-
}
|
|
3962
|
-
if (this._fragmentMode === "extended" && !deferToRouteA) {
|
|
3963
|
-
report.push(
|
|
3964
|
-
"\u03BD-net Route B (EXTENDED) declined: net outside coloured-consumer fragment (a coloured place consumed count != 1 or by multiple inputs, carries a reset/read/inhibitor arc, or a join re-mints a coloured place); verified via sound over-approximation instead."
|
|
3965
|
-
);
|
|
3966
|
-
}
|
|
3967
|
-
}
|
|
3968
|
-
report.push("Phase 1: Flattening net...");
|
|
3969
|
-
const flatNet = flatten(this.net, this._environmentPlaces, this._environmentMode);
|
|
3970
|
-
report.push(` Places: ${flatNet.places.length}`);
|
|
3971
|
-
report.push(` Transitions (expanded): ${flatNet.transitions.length}`);
|
|
3972
|
-
if (flatNet.environmentBounds.size > 0) {
|
|
3973
|
-
report.push(` Environment bounds: ${flatNet.environmentBounds.size} places`);
|
|
3974
|
-
}
|
|
3975
|
-
report.push("");
|
|
3976
|
-
report.push("Phase 2: Structural pre-check (siphon/trap)...");
|
|
3977
|
-
const structResult = structuralCheck(flatNet, this._initialMarking);
|
|
3978
|
-
let structResultStr;
|
|
3979
|
-
switch (structResult.type) {
|
|
3980
|
-
case "no-potential-deadlock":
|
|
3981
|
-
structResultStr = "no potential deadlock";
|
|
3982
|
-
break;
|
|
3983
|
-
case "potential-deadlock":
|
|
3984
|
-
structResultStr = `potential deadlock (siphon: {${[...structResult.siphon].join(",")}})`;
|
|
3985
|
-
break;
|
|
3986
|
-
case "inconclusive":
|
|
3987
|
-
structResultStr = `inconclusive (${structResult.reason})`;
|
|
3988
|
-
break;
|
|
3989
|
-
}
|
|
3990
|
-
report.push(` Result: ${structResultStr}
|
|
3991
|
-
`);
|
|
3992
|
-
if (this._property.type === "deadlock-free" && !hasMatch && this._sinkPlaces.size === 0 && structResult.type === "no-potential-deadlock" && this._environmentPlaces.size === 0) {
|
|
3993
|
-
report.push("=== RESULT ===\n");
|
|
3994
|
-
report.push("PROVEN (structural): Deadlock-freedom verified by Commoner's theorem.");
|
|
3995
|
-
report.push(" All siphons contain initially marked traps.");
|
|
3996
|
-
report.push(" Certificate check: not applicable (structural proof)");
|
|
3997
|
-
return buildResult(
|
|
3998
|
-
{ type: "proven", method: "structural", inductiveInvariant: null },
|
|
3999
|
-
report.join("\n"),
|
|
4000
|
-
[],
|
|
4001
|
-
[],
|
|
4002
|
-
[],
|
|
4003
|
-
[],
|
|
4004
|
-
performance.now() - start,
|
|
4005
|
-
{ places: flatNet.places.length, transitions: flatNet.transitions.length, invariantsFound: 0, structuralResult: structResultStr }
|
|
4006
|
-
);
|
|
4007
|
-
}
|
|
4008
|
-
report.push("Phase 3: Computing P-invariants...");
|
|
4009
|
-
const matrix = IncidenceMatrix.from(flatNet);
|
|
4010
|
-
const { valid: basisInvariants, dropped: droppedInvariants } = validateInvariantsExact(
|
|
4011
|
-
matrix,
|
|
4012
|
-
computePInvariants(matrix, flatNet, this._initialMarking),
|
|
4013
|
-
flatNet,
|
|
4014
|
-
this._initialMarking
|
|
4015
|
-
);
|
|
4016
|
-
const { valid: semiflows, dropped: droppedSemiflows } = validateInvariantsExact(
|
|
4017
|
-
matrix,
|
|
4018
|
-
computePSemiflows(matrix, flatNet, this._initialMarking),
|
|
4019
|
-
flatNet,
|
|
4020
|
-
this._initialMarking
|
|
4021
|
-
);
|
|
4022
|
-
report.push(` Found: ${basisInvariants.length} P-invariant(s)`);
|
|
4023
|
-
let invariants = basisInvariants;
|
|
4024
|
-
if (this._semiflowInvariants) {
|
|
4025
|
-
const { invariants: strengthened, added } = strengthenWithSemiflows(basisInvariants, semiflows);
|
|
4026
|
-
invariants = strengthened;
|
|
4027
|
-
report.push(` Semiflows encoded as invariants: ${added}`);
|
|
4028
|
-
}
|
|
4029
|
-
invariants = canonicalInvariantOrder(invariants);
|
|
4030
|
-
const structurallyBounded = isCoveredByInvariants(invariants, flatNet.places.length);
|
|
4031
|
-
report.push(` Structurally bounded: ${structurallyBounded ? "YES" : "NO"}`);
|
|
4032
|
-
for (const inv of invariants) {
|
|
4033
|
-
report.push(` ${formatInvariant(inv, flatNet)}`);
|
|
4034
|
-
}
|
|
4035
|
-
for (const { invariant, reason } of droppedInvariants) {
|
|
4036
|
-
report.push(` Dropped invariant: ${formatInvariant(invariant, flatNet)} - ${reason}`);
|
|
4037
|
-
}
|
|
4038
|
-
if (droppedInvariants.length > 0) {
|
|
4039
|
-
report.push(` Dropped: ${droppedInvariants.length} invariant(s) failed the exact re-check`);
|
|
4040
|
-
}
|
|
4041
|
-
for (const { invariant, reason } of droppedSemiflows) {
|
|
4042
|
-
report.push(` Dropped semiflow: ${formatInvariant(invariant, flatNet)} - ${reason}`);
|
|
4043
|
-
}
|
|
4044
|
-
if (droppedSemiflows.length > 0) {
|
|
4045
|
-
report.push(` Dropped: ${droppedSemiflows.length} semiflow(s) failed the exact re-check`);
|
|
4046
|
-
}
|
|
4047
|
-
report.push("");
|
|
4048
|
-
report.push("Phase 4: IC3/PDR verification via Z3 Spacer...");
|
|
4049
|
-
const stats = {
|
|
4050
|
-
places: flatNet.places.length,
|
|
4051
|
-
transitions: flatNet.transitions.length,
|
|
4052
|
-
invariantsFound: invariants.length,
|
|
4053
|
-
structuralResult: structResultStr
|
|
4054
|
-
};
|
|
4055
|
-
let solver;
|
|
4056
|
-
try {
|
|
4057
|
-
solver = resolveZ3();
|
|
4058
|
-
} catch (e) {
|
|
4059
|
-
const reason = e instanceof Z3Unavailable ? e.message : String(e?.message ?? e);
|
|
4060
|
-
report.push(` Solver: z3 unavailable (${reason})`);
|
|
4061
|
-
report.push(` Status: UNKNOWN (${reason})
|
|
4062
|
-
`);
|
|
4063
|
-
report.push("=== RESULT ===\n");
|
|
4064
|
-
report.push(`UNKNOWN: Could not determine ${propDesc}`);
|
|
4065
|
-
report.push(` Reason: ${reason}`);
|
|
4066
|
-
return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats);
|
|
4067
|
-
}
|
|
4068
|
-
report.push(` Solver: z3 ${formatZ3Version(solver.version)}`);
|
|
4069
|
-
const colouredAttempt = this.colouredAttempt(flatNet, invariants, semiflows);
|
|
4070
|
-
const colouredPlan = colouredAttempt.plan;
|
|
4071
|
-
let encoding;
|
|
4072
|
-
if (colouredPlan != null) {
|
|
4073
|
-
report.push(
|
|
4074
|
-
` \u03BD-encoding: name-coloured (exact within budget k=${colouredPlan.k}; ${colouredPlan.coloured.length} coloured place(s))`
|
|
4075
|
-
);
|
|
4076
|
-
const coloured = colouredAttempt.encoding;
|
|
4077
|
-
if (coloured == null) {
|
|
4078
|
-
const reason = "property names a place that does not resolve in the net; refusing to certify (the encoding would be vacuously proven)";
|
|
4079
|
-
report.push(" Status: UNKNOWN (unresolved property place)\n");
|
|
4080
|
-
report.push("=== RESULT ===\n");
|
|
4081
|
-
report.push(`UNKNOWN: ${reason}`);
|
|
4082
|
-
return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats);
|
|
4083
|
-
}
|
|
4084
|
-
encoding = coloured;
|
|
4085
|
-
} else {
|
|
4086
|
-
const unresolved = unresolvedPropertyPlace(flatNet, this._property);
|
|
4087
|
-
if (unresolved != null) {
|
|
4088
|
-
const reason = `property names a place that does not resolve in the net ('${unresolved}'); refusing to certify (the encoding would be vacuously proven)`;
|
|
4089
|
-
report.push(" Status: UNKNOWN (unresolved property place)\n");
|
|
4090
|
-
report.push("=== RESULT ===\n");
|
|
4091
|
-
report.push(`UNKNOWN: ${reason}`);
|
|
4092
|
-
return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats);
|
|
4093
|
-
}
|
|
4094
|
-
encoding = encode(flatNet, this._initialMarking, this._property, invariants, this._sinkPlaces, this._counterexampleReplay);
|
|
4095
|
-
}
|
|
4096
|
-
const queryResult = await runZ3Spacer(
|
|
4097
|
-
solver,
|
|
4098
|
-
this._timeoutMs,
|
|
4099
|
-
encoding.smt2,
|
|
4100
|
-
colouredPlan != null ? "horn-coloured" : "horn"
|
|
4101
|
-
);
|
|
4102
|
-
switch (queryResult.type) {
|
|
4103
|
-
case "proven": {
|
|
4104
|
-
if (this._environmentPlaces.size > 0 && this._environmentMode.type === "ignore") {
|
|
4105
|
-
const reason = IGNORE_MODE_VACUITY_REASON;
|
|
4106
|
-
report.push(` Status: UNSAT, but vacuous under ignore mode
|
|
4107
|
-
`);
|
|
4108
|
-
report.push("=== RESULT ===\n");
|
|
4109
|
-
report.push(`UNKNOWN: ${reason}`);
|
|
4110
|
-
return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats);
|
|
4111
|
-
}
|
|
4112
|
-
report.push(" Status: UNSAT (property holds)");
|
|
4113
|
-
if (colouredPlan != null) {
|
|
4114
|
-
report.push(" Certificate check: not applicable (name-coloured encoding)");
|
|
4115
|
-
} else if (!this._certificateCheck) {
|
|
4116
|
-
report.push(" Certificate check: not applicable (disabled)");
|
|
4117
|
-
} else {
|
|
4118
|
-
const certificate = await checkCertificate(
|
|
4119
|
-
queryResult.invariantFormula,
|
|
4120
|
-
flatNet,
|
|
4121
|
-
this._initialMarking,
|
|
4122
|
-
this._property,
|
|
4123
|
-
invariants,
|
|
4124
|
-
this._sinkPlaces,
|
|
4125
|
-
solver,
|
|
4126
|
-
this._timeoutMs
|
|
4127
|
-
);
|
|
4128
|
-
const reason = certificateDowngradeReason(certificate);
|
|
4129
|
-
if (reason != null) {
|
|
4130
|
-
report.push(" Certificate check: FAILED");
|
|
4131
|
-
if (certificate.type !== "passed" && certificate.invariant != null) {
|
|
4132
|
-
report.push(" Uncertified invariant:");
|
|
4133
|
-
for (const line of certificate.invariant.split("\n")) report.push(` ${line}`);
|
|
4134
|
-
}
|
|
4135
|
-
report.push("");
|
|
4136
|
-
report.push("=== RESULT ===\n");
|
|
4137
|
-
report.push(`UNKNOWN: ${reason}`);
|
|
4138
|
-
return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats);
|
|
4139
|
-
}
|
|
4140
|
-
report.push(" Certificate check: PASSED (init, consecution, safety)");
|
|
4141
|
-
}
|
|
4142
|
-
report.push("");
|
|
4143
|
-
const formula = queryResult.invariantFormula;
|
|
4144
|
-
const discoveredInvariants = formula != null ? [formula] : [];
|
|
4145
|
-
if (formula != null) {
|
|
4146
|
-
report.push("Phase 5: Inductive invariant (discovered by IC3)");
|
|
4147
|
-
report.push(" Spacer synthesized:");
|
|
4148
|
-
for (const line of formula.split("\n")) report.push(` ${line}`);
|
|
4149
|
-
report.push(" This formula is INDUCTIVE: preserved by all transitions.");
|
|
4150
|
-
report.push("");
|
|
4151
|
-
}
|
|
4152
|
-
report.push("=== RESULT ===\n");
|
|
4153
|
-
report.push(`PROVEN (IC3/PDR): ${propDesc}`);
|
|
4154
|
-
report.push(" Z3 Spacer proved no reachable state violates the property.");
|
|
4155
|
-
report.push(" NOTE: Verification ignores timing constraints.");
|
|
4156
|
-
report.push(" An untimed proof is STRONGER than a timed one (timing only restricts behavior).");
|
|
4157
|
-
return this.applyNuGuard(buildResult(
|
|
4158
|
-
{ type: "proven", method: "IC3/PDR", inductiveInvariant: formula },
|
|
4159
|
-
report.join("\n"),
|
|
4160
|
-
invariants,
|
|
4161
|
-
discoveredInvariants,
|
|
4162
|
-
[],
|
|
4163
|
-
[],
|
|
4164
|
-
performance.now() - start,
|
|
4165
|
-
stats
|
|
4166
|
-
), hasMatch, nuBounded, colouredPlan != null);
|
|
4167
|
-
}
|
|
4168
|
-
case "violated": {
|
|
4169
|
-
report.push(" Status: SAT (counterexample found)\n");
|
|
4170
|
-
const decoded = decode(queryResult.answer, flatNet);
|
|
4171
|
-
if (decoded.note != null) report.push(` Counterexample decoding: ${decoded.note}`);
|
|
4172
|
-
let confirmed = null;
|
|
4173
|
-
let trace = [...decoded.states];
|
|
4174
|
-
let transitions = [];
|
|
4175
|
-
let replayed = false;
|
|
4176
|
-
if (colouredPlan == null && this._counterexampleReplay) {
|
|
4177
|
-
const assessment = assessCounterexample(
|
|
4178
|
-
flatNet,
|
|
4179
|
-
this._initialMarking,
|
|
4180
|
-
decoded.states,
|
|
4181
|
-
this._property,
|
|
4182
|
-
this._sinkPlaces
|
|
4183
|
-
);
|
|
4184
|
-
if (assessment.kind === "confirmed") {
|
|
4185
|
-
confirmed = true;
|
|
4186
|
-
replayed = true;
|
|
4187
|
-
trace = assessment.trace;
|
|
4188
|
-
transitions = assessment.firings;
|
|
4189
|
-
report.push(" Counterexample replay: CONFIRMED (abstract chain M0 -> bad re-executed)");
|
|
4190
|
-
} else if (assessment.kind === "unconfirmed") {
|
|
4191
|
-
confirmed = false;
|
|
4192
|
-
report.push(` Counterexample replay: UNCONFIRMED (${assessment.note})`);
|
|
4193
|
-
report.push(" The verdict rests on Spacer's answer.");
|
|
4194
|
-
} else {
|
|
4195
|
-
report.push(" Counterexample replay: FAILED");
|
|
4196
|
-
report.push(` Decoded states (order-free set, ${decoded.states.size}):`);
|
|
4197
|
-
for (const m of decoded.states) report.push(` ${m}`);
|
|
4198
|
-
report.push(` Raw Z3 answer: ${truncate(queryResult.answer, 2e3)}`);
|
|
4199
|
-
report.push("");
|
|
4200
|
-
report.push("=== RESULT ===\n");
|
|
4201
|
-
report.push(`UNKNOWN: ${assessment.reason}`);
|
|
4202
|
-
return buildResult(
|
|
4203
|
-
{ type: "unknown", reason: assessment.reason },
|
|
4204
|
-
report.join("\n"),
|
|
4205
|
-
invariants,
|
|
4206
|
-
[],
|
|
4207
|
-
[],
|
|
4208
|
-
[],
|
|
4209
|
-
performance.now() - start,
|
|
4210
|
-
stats,
|
|
4211
|
-
false
|
|
4212
|
-
);
|
|
4213
|
-
}
|
|
4214
|
-
}
|
|
4215
|
-
report.push("=== RESULT ===\n");
|
|
4216
|
-
report.push(`VIOLATED: ${propDesc}`);
|
|
4217
|
-
if (trace.length > 0) {
|
|
4218
|
-
report.push(` Counterexample trace (${replayed ? "replay order, " : "proof order, "}${trace.length} states):`);
|
|
4219
|
-
for (let i = 0; i < trace.length; i++) report.push(` ${i}: ${trace[i]}`);
|
|
4220
|
-
}
|
|
4221
|
-
if (transitions.length > 0) report.push(` Firing sequence: ${transitions.join(" -> ")}`);
|
|
4222
|
-
report.push("\n WARNING: This counterexample is in UNTIMED semantics.");
|
|
4223
|
-
report.push(" It may be spurious if timing constraints prevent this sequence.");
|
|
4224
|
-
return this.applyNuGuard(buildResult(
|
|
4225
|
-
{ type: "violated" },
|
|
4226
|
-
report.join("\n"),
|
|
4227
|
-
invariants,
|
|
4228
|
-
[],
|
|
4229
|
-
trace,
|
|
4230
|
-
transitions,
|
|
4231
|
-
performance.now() - start,
|
|
4232
|
-
stats,
|
|
4233
|
-
confirmed
|
|
4234
|
-
), hasMatch, nuBounded, colouredPlan != null);
|
|
4235
|
-
}
|
|
4236
|
-
case "unknown": {
|
|
4237
|
-
report.push(` Status: UNKNOWN (${queryResult.reason})
|
|
4238
|
-
`);
|
|
4239
|
-
report.push("=== RESULT ===\n");
|
|
4240
|
-
report.push(`UNKNOWN: Could not determine ${propDesc}`);
|
|
4241
|
-
report.push(` Reason: ${queryResult.reason}`);
|
|
4242
|
-
return buildResult(
|
|
4243
|
-
{ type: "unknown", reason: queryResult.reason },
|
|
4244
|
-
report.join("\n"),
|
|
4245
|
-
invariants,
|
|
4246
|
-
[],
|
|
4247
|
-
[],
|
|
4248
|
-
[],
|
|
4249
|
-
performance.now() - start,
|
|
4250
|
-
stats
|
|
4251
|
-
);
|
|
4252
|
-
}
|
|
4253
|
-
}
|
|
4254
|
-
}
|
|
4255
|
-
/**
|
|
4256
|
-
* ν-net soundness guard (NU-040, NU-050). Applied only when the net contains
|
|
4257
|
-
* match (ν-join) transitions, and only to a proven/violated verdict (an
|
|
4258
|
-
* existing unknown is left as-is).
|
|
4259
|
-
*
|
|
4260
|
-
* - Quiescence-based properties (deadlock / joined-or-dead-lettered): the
|
|
4261
|
-
* name-blind over-approximation over-fires joins, so it sees fewer quiescent
|
|
4262
|
-
* states and may miss a real stranded marking — downgraded to unknown
|
|
4263
|
-
* (exact quiescence reasoning is deferred to the SCG name-partition quotient).
|
|
4264
|
-
* - Reachability-safety with unbounded fresh names (no budget declared):
|
|
4265
|
-
* reachability over unbounded fresh names is undecidable — unknown.
|
|
4266
|
-
* - Bounded reachability-safety in the name-coloured fragment (`exact`): name
|
|
4267
|
-
* equality is encoded exactly via bounded name-colouring, so the verdict is
|
|
4268
|
-
* sound *and* complete within the budget — no spurious different-name
|
|
4269
|
-
* counterexample. The verdict is kept and the exact-path note is appended.
|
|
4270
|
-
* - Bounded reachability-safety outside that fragment: `proven` is sound; a
|
|
4271
|
-
* `violated` may be spurious — the verdict is kept and the over-approximation
|
|
4272
|
-
* caveat is appended to the report.
|
|
4273
|
-
*/
|
|
4274
|
-
applyNuGuard(result, hasMatch, nuBounded, exact) {
|
|
4275
|
-
if (!hasMatch || result.verdict.type === "unknown") return result;
|
|
4276
|
-
if (exact) {
|
|
4277
|
-
const note2 = "\nNote: \u03BD-join name equality is encoded exactly via bounded name-colouring (k = budget); the verdict is sound and complete within the budget bound \u2014 no spurious different-name counterexample (NU-050 #1 / NU-053).\n";
|
|
4278
|
-
return { ...result, report: result.report + note2 };
|
|
4279
|
-
}
|
|
4280
|
-
if (!isReachabilitySafety(this._property)) {
|
|
4281
|
-
return downgradeToUnknown(
|
|
4282
|
-
result,
|
|
4283
|
-
"\u03BD-matching transitions present and the property depends on quiescence (deadlock / joined-or-dead-lettered); the name-blind over-approximation cannot decide it soundly \u2014 deferred to the exact \u03BD-analysis (NU-050)"
|
|
4284
|
-
);
|
|
4285
|
-
}
|
|
4286
|
-
if (!nuBounded) {
|
|
4287
|
-
return downgradeToUnknown(
|
|
4288
|
-
result,
|
|
4289
|
-
"\u03BD-matching transitions present with unbounded fresh names (no budget place declared via budgetPlaces(...)); reachability over unbounded fresh names is undecidable (NU-040) \u2014 declare the budget place(s) that gate minting to verify within the bounded fragment"
|
|
4290
|
-
);
|
|
4291
|
-
}
|
|
4292
|
-
const note = "\nNote: matched (\u03BD-join) transitions are over-approximated (name equality assumed satisfiable). 'proven' is sound; a 'violated' counterexample may be spurious pending the exact \u03BD-analysis (NU-050).\n";
|
|
4293
|
-
return { ...result, report: result.report + note };
|
|
4294
|
-
}
|
|
4295
|
-
};
|
|
4296
|
-
function isReachabilitySafety(property) {
|
|
4297
|
-
switch (property.type) {
|
|
4298
|
-
case "place-bound":
|
|
4299
|
-
case "branch-place-bound":
|
|
4300
|
-
case "mutual-exclusion":
|
|
4301
|
-
case "unreachable":
|
|
4302
|
-
return true;
|
|
4303
|
-
case "deadlock-free":
|
|
4304
|
-
case "terminates-at-sink":
|
|
4305
|
-
case "joined-or-dead-lettered":
|
|
4306
|
-
return false;
|
|
4307
|
-
}
|
|
4308
|
-
}
|
|
4309
|
-
function assessCounterexample(flatNet, initialMarking, decodedStates, property, sinkPlaces) {
|
|
4310
|
-
if (decodedStates.size === 0) {
|
|
4311
|
-
return {
|
|
4312
|
-
kind: "unconfirmed",
|
|
4313
|
-
note: "no counterexample states could be decoded from the Spacer answer, so the abstract replay could not run"
|
|
4314
|
-
};
|
|
4315
|
-
}
|
|
4316
|
-
let outcome;
|
|
4317
|
-
try {
|
|
4318
|
-
outcome = replayCounterexample(
|
|
4319
|
-
flatNet,
|
|
4320
|
-
vectorize(initialMarking, flatNet),
|
|
4321
|
-
[...decodedStates].map((m) => vectorize(m, flatNet)),
|
|
4322
|
-
property,
|
|
4323
|
-
sinkPlaces
|
|
4324
|
-
);
|
|
4325
|
-
} catch (e) {
|
|
4326
|
-
outcome = { kind: "exhausted", reason: `replay threw: ${e?.message ?? e}`, nodesExplored: 0 };
|
|
4327
|
-
}
|
|
4328
|
-
switch (outcome.kind) {
|
|
4329
|
-
case "confirmed":
|
|
4330
|
-
return {
|
|
4331
|
-
kind: "confirmed",
|
|
4332
|
-
trace: outcome.states.map((s) => toMarkingState(s, flatNet)),
|
|
4333
|
-
firings: outcome.steps.map(stepName)
|
|
4334
|
-
};
|
|
4335
|
-
case "exhausted":
|
|
4336
|
-
return { kind: "unconfirmed", note: `abstract replay did not complete: ${outcome.reason}` };
|
|
4337
|
-
case "no-chain":
|
|
4338
|
-
return {
|
|
4339
|
-
kind: "downgraded",
|
|
4340
|
-
reason: "counterexample replay found no firing chain to the violation under the abstract semantics, so VIOLATED is withheld"
|
|
4341
|
-
};
|
|
4342
|
-
}
|
|
4343
|
-
}
|
|
4344
|
-
function certificateDowngradeReason(outcome) {
|
|
4345
|
-
switch (outcome.type) {
|
|
4346
|
-
case "passed":
|
|
4347
|
-
return null;
|
|
4348
|
-
case "failed":
|
|
4349
|
-
return `certificate check failed: ${outcome.vc} was not UNSAT - ${outcome.detail}; the IC3 certificate could not be independently re-validated against the unstrengthened step relation, so PROVEN is withheld`;
|
|
4350
|
-
case "unavailable":
|
|
4351
|
-
return `certificate check could not run: ${outcome.reason}; PROVEN is withheld without an independently validated certificate`;
|
|
4352
|
-
}
|
|
4353
|
-
}
|
|
4354
|
-
function placeholderCertificate(placeCount) {
|
|
4355
|
-
const params = [];
|
|
4356
|
-
for (let i = 0; i < placeCount; i++) params.push(`(x!${i} Int)`);
|
|
4357
|
-
return `(define-fun Reachable (${params.join(" ")}) Bool
|
|
4358
|
-
true)`;
|
|
4359
|
-
}
|
|
4360
|
-
function downgradeToUnknown(result, reason) {
|
|
4361
|
-
return {
|
|
4362
|
-
...result,
|
|
4363
|
-
verdict: { type: "unknown", reason },
|
|
4364
|
-
report: result.report + `
|
|
4365
|
-
Downgraded to UNKNOWN: ${reason}
|
|
4366
|
-
`,
|
|
4367
|
-
discoveredInvariants: [],
|
|
4368
|
-
counterexampleTrace: [],
|
|
4369
|
-
counterexampleTransitions: [],
|
|
4370
|
-
counterexampleConfirmed: null
|
|
4371
|
-
};
|
|
4372
|
-
}
|
|
4373
|
-
function truncate(s, max) {
|
|
4374
|
-
return s.length <= max ? s : `${s.slice(0, max)}\u2026 (${s.length - max} chars truncated)`;
|
|
4375
|
-
}
|
|
4376
|
-
function unresolvedPropertyPlace(flatNet, property) {
|
|
4377
|
-
const named = (() => {
|
|
4378
|
-
switch (property.type) {
|
|
4379
|
-
case "deadlock-free":
|
|
4380
|
-
return [];
|
|
4381
|
-
case "terminates-at-sink":
|
|
4382
|
-
return [];
|
|
4383
|
-
case "mutual-exclusion":
|
|
4384
|
-
return [property.p1, property.p2];
|
|
4385
|
-
case "place-bound":
|
|
4386
|
-
return [property.place];
|
|
4387
|
-
case "branch-place-bound":
|
|
4388
|
-
return [property.place];
|
|
4389
|
-
case "unreachable":
|
|
4390
|
-
return [...property.places];
|
|
4391
|
-
case "joined-or-dead-lettered":
|
|
4392
|
-
return [property.pending];
|
|
4393
|
-
}
|
|
4394
|
-
})();
|
|
4395
|
-
for (const place of named) {
|
|
4396
|
-
if (!flatNet.placeIndex.has(place.name)) return place.name;
|
|
4397
|
-
}
|
|
4398
|
-
return null;
|
|
4399
|
-
}
|
|
4400
|
-
function formatInvariant(inv, flatNet) {
|
|
4401
|
-
const parts = [];
|
|
4402
|
-
for (const idx of inv.support) {
|
|
4403
|
-
if (inv.weights[idx] !== 1) {
|
|
4404
|
-
parts.push(`${inv.weights[idx]}*${flatNet.places[idx].name}`);
|
|
4405
|
-
} else {
|
|
4406
|
-
parts.push(flatNet.places[idx].name);
|
|
4407
|
-
}
|
|
4408
|
-
}
|
|
4409
|
-
return `${parts.length === 0 ? "0" : parts.join(" + ")} = ${inv.constant}`;
|
|
4410
|
-
}
|
|
4411
|
-
function buildResult(verdict, report, invariants, discoveredInvariants, trace, transitions, elapsedMs, statistics, counterexampleConfirmed = null) {
|
|
4412
|
-
return { verdict, report, invariants, discoveredInvariants, counterexampleTrace: trace, counterexampleTransitions: transitions, counterexampleConfirmed, elapsedMs, statistics };
|
|
4413
|
-
}
|
|
4414
|
-
|
|
4415
|
-
// src/verification/smt-verification-result.ts
|
|
4416
|
-
function isProven(result) {
|
|
4417
|
-
return result.verdict.type === "proven";
|
|
4418
|
-
}
|
|
4419
|
-
function isViolated(result) {
|
|
4420
|
-
return result.verdict.type === "violated";
|
|
4421
|
-
}
|
|
4422
|
-
|
|
4423
|
-
export {
|
|
4424
|
-
one,
|
|
4425
|
-
exactly,
|
|
4426
|
-
all,
|
|
4427
|
-
atLeast,
|
|
4428
|
-
requiredCount,
|
|
4429
|
-
consumptionCount,
|
|
4430
|
-
and,
|
|
4431
|
-
andPlaces,
|
|
4432
|
-
xor,
|
|
4433
|
-
xorPlaces,
|
|
4434
|
-
outPlace,
|
|
4435
|
-
timeout,
|
|
4436
|
-
timeoutPlace,
|
|
4437
|
-
forwardInput,
|
|
4438
|
-
allPlaces,
|
|
4439
|
-
enumerateBranches,
|
|
4440
|
-
passthrough,
|
|
4441
|
-
isPassthrough,
|
|
4442
|
-
transform,
|
|
4443
|
-
fork,
|
|
4444
|
-
transformFrom,
|
|
4445
|
-
transformAsync,
|
|
4446
|
-
produce,
|
|
4447
|
-
withTimeout,
|
|
4448
|
-
MarkingState,
|
|
4449
|
-
MarkingStateBuilder,
|
|
4450
|
-
deadlockFree,
|
|
4451
|
-
terminatesAtSink,
|
|
4452
|
-
mutualExclusion,
|
|
4453
|
-
placeBound,
|
|
4454
|
-
unreachable,
|
|
4455
|
-
branchPlaceBound,
|
|
4456
|
-
joinedOrDeadLettered,
|
|
4457
|
-
propertyDescription,
|
|
4458
|
-
flatTransition,
|
|
4459
|
-
alwaysAvailable,
|
|
4460
|
-
bounded,
|
|
4461
|
-
ignore,
|
|
4462
|
-
flatten,
|
|
4463
|
-
IncidenceMatrix,
|
|
4464
|
-
pInvariant,
|
|
4465
|
-
pInvariantToString,
|
|
4466
|
-
computePInvariants,
|
|
4467
|
-
strengthenWithSemiflows,
|
|
4468
|
-
computePSemiflows,
|
|
4469
|
-
isCoveredByInvariants,
|
|
4470
|
-
canonicalInvariantOrder,
|
|
4471
|
-
structuralCheck,
|
|
4472
|
-
findMinimalSiphons,
|
|
4473
|
-
findMaximalTrapIn,
|
|
4474
|
-
Z3_ENV,
|
|
4475
|
-
DUMP_ENV,
|
|
4476
|
-
MIN_Z3_VERSION,
|
|
4477
|
-
parseZ3Version,
|
|
4478
|
-
formatZ3Version,
|
|
4479
|
-
Z3Unavailable,
|
|
4480
|
-
Z3ProcessError,
|
|
4481
|
-
z3SolverAt,
|
|
4482
|
-
resolveZ3,
|
|
4483
|
-
z3Available,
|
|
4484
|
-
runZ3Text,
|
|
4485
|
-
runZ3Spacer,
|
|
4486
|
-
encode,
|
|
4487
|
-
encodeStepRelationSmt2,
|
|
4488
|
-
checkCertificate,
|
|
4489
|
-
vcScript,
|
|
4490
|
-
DBM,
|
|
4491
|
-
StateClass,
|
|
4492
|
-
requireOutputProducingActions,
|
|
4493
|
-
StateClassGraph,
|
|
4494
|
-
decode,
|
|
4495
|
-
decodeStateSet,
|
|
4496
|
-
flatNetPlaceCount,
|
|
4497
|
-
flatNetTransitionCount,
|
|
4498
|
-
flatNetIndexOf,
|
|
4499
|
-
replayCounterexample,
|
|
4500
|
-
SmtVerifier,
|
|
4501
|
-
placeholderCertificate,
|
|
4502
|
-
isProven,
|
|
4503
|
-
isViolated
|
|
4504
|
-
};
|
|
4505
|
-
//# sourceMappingURL=chunk-75KEJQGC.js.map
|