@stevenvo780/st-lang 4.13.0 → 4.14.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/reasoning/hoare-logic/index.d.ts +130 -0
- package/dist/reasoning/hoare-logic/index.d.ts.map +1 -0
- package/dist/reasoning/hoare-logic/index.js +535 -0
- package/dist/reasoning/hoare-logic/index.js.map +1 -0
- package/dist/reasoning/model-checking/index.d.ts +113 -0
- package/dist/reasoning/model-checking/index.d.ts.map +1 -0
- package/dist/reasoning/model-checking/index.js +786 -0
- package/dist/reasoning/model-checking/index.js.map +1 -0
- package/dist/reasoning/separation-logic/index.d.ts +190 -0
- package/dist/reasoning/separation-logic/index.d.ts.map +1 -0
- package/dist/reasoning/separation-logic/index.js +758 -0
- package/dist/reasoning/separation-logic/index.js.map +1 -0
- package/dist/reasoning/universal-algebra/index.d.ts +196 -0
- package/dist/reasoning/universal-algebra/index.d.ts.map +1 -0
- package/dist/reasoning/universal-algebra/index.js +865 -0
- package/dist/reasoning/universal-algebra/index.js.map +1 -0
- package/dist/tests/reasoning/hoare-logic/hoare-logic.test.d.ts +2 -0
- package/dist/tests/reasoning/hoare-logic/hoare-logic.test.d.ts.map +1 -0
- package/dist/tests/reasoning/hoare-logic/hoare-logic.test.js +340 -0
- package/dist/tests/reasoning/hoare-logic/hoare-logic.test.js.map +1 -0
- package/dist/tests/reasoning/model-checking/model-checking.test.d.ts +2 -0
- package/dist/tests/reasoning/model-checking/model-checking.test.d.ts.map +1 -0
- package/dist/tests/reasoning/model-checking/model-checking.test.js +222 -0
- package/dist/tests/reasoning/model-checking/model-checking.test.js.map +1 -0
- package/dist/tests/reasoning/separation-logic/separation-logic.test.d.ts +2 -0
- package/dist/tests/reasoning/separation-logic/separation-logic.test.d.ts.map +1 -0
- package/dist/tests/reasoning/separation-logic/separation-logic.test.js +311 -0
- package/dist/tests/reasoning/separation-logic/separation-logic.test.js.map +1 -0
- package/dist/tests/reasoning/universal-algebra/universal-algebra.test.d.ts +2 -0
- package/dist/tests/reasoning/universal-algebra/universal-algebra.test.d.ts.map +1 -0
- package/dist/tests/reasoning/universal-algebra/universal-algebra.test.js +289 -0
- package/dist/tests/reasoning/universal-algebra/universal-algebra.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// ST Model Checking — Explicit-state model checker
|
|
4
|
+
// ============================================================
|
|
5
|
+
//
|
|
6
|
+
// Verificación de propiedades sobre sistemas de transiciones
|
|
7
|
+
// finitos (Kripke structures) mediante exploración explícita
|
|
8
|
+
// del espacio de estados:
|
|
9
|
+
//
|
|
10
|
+
// - Reachability: BFS desde estados iniciales con cota opcional.
|
|
11
|
+
// - Safety (G p / invariant p): DFS que falla en el primer
|
|
12
|
+
// estado donde p no se cumple y devuelve traza desde inicial.
|
|
13
|
+
// - Liveness (GF p, FG p): exploración con detección de ciclos
|
|
14
|
+
// accesibles (lasso = stem + loop) para encontrar contraejemplos
|
|
15
|
+
// o testigos.
|
|
16
|
+
// - Bounded model checking: BFS truncado a profundidad k.
|
|
17
|
+
// - Detección de deadlock: estado alcanzable sin sucesores.
|
|
18
|
+
//
|
|
19
|
+
// El espacio de estados es genérico: el usuario provee una
|
|
20
|
+
// función `successors`, una función `hash` (clave canónica para
|
|
21
|
+
// detección de visitados) y `labels` (proposiciones atómicas que
|
|
22
|
+
// hold en el estado, opcional para uso futuro con LTL completo).
|
|
23
|
+
//
|
|
24
|
+
// Diseño:
|
|
25
|
+
// - Cada estado se canoniza por `hash(s)` (string). El usuario
|
|
26
|
+
// debe garantizar que estados equivalentes produzcan el mismo
|
|
27
|
+
// hash, y estados distintos hashes distintos.
|
|
28
|
+
// - Trazas y lassos se devuelven como arrays de S (no de hashes)
|
|
29
|
+
// para que el caller pueda inspeccionar los estados.
|
|
30
|
+
// - Las funciones nunca lanzan: cap `maxStates` para evitar
|
|
31
|
+
// explosión combinatoria sobre sistemas no acotados.
|
|
32
|
+
//
|
|
33
|
+
// Convención: las propiedades reciben un estado y devuelven boolean;
|
|
34
|
+
// se asume que son puras y deterministas sobre el estado.
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.reachableStates = reachableStates;
|
|
37
|
+
exports.checkSafety = checkSafety;
|
|
38
|
+
exports.checkInvariant = checkInvariant;
|
|
39
|
+
exports.bmc = bmc;
|
|
40
|
+
exports.hasDeadlock = hasDeadlock;
|
|
41
|
+
exports.checkAlwaysEventually = checkAlwaysEventually;
|
|
42
|
+
exports.checkEventuallyAlways = checkEventuallyAlways;
|
|
43
|
+
exports.mutualExclusionSpace = mutualExclusionSpace;
|
|
44
|
+
exports.diningPhilosophersSpace = diningPhilosophersSpace;
|
|
45
|
+
exports.readerWriterSpace = readerWriterSpace;
|
|
46
|
+
const DEFAULT_MAX_STATES = 100_000;
|
|
47
|
+
// ── Reachability (BFS) ───────────────────────────────────────
|
|
48
|
+
/**
|
|
49
|
+
* Calcula el conjunto de estados alcanzables desde los estados
|
|
50
|
+
* iniciales del espacio. BFS por nivel; se detiene al agotar la
|
|
51
|
+
* frontera o al alcanzar `maxStates`.
|
|
52
|
+
*/
|
|
53
|
+
function reachableStates(space, opts = {}) {
|
|
54
|
+
const maxStates = opts.maxStates ?? DEFAULT_MAX_STATES;
|
|
55
|
+
const visited = new Map();
|
|
56
|
+
const queue = [];
|
|
57
|
+
for (const s of space.initial) {
|
|
58
|
+
const h = space.hash(s);
|
|
59
|
+
if (!visited.has(h)) {
|
|
60
|
+
visited.set(h, s);
|
|
61
|
+
queue.push(s);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
let truncated = false;
|
|
65
|
+
while (queue.length > 0) {
|
|
66
|
+
if (visited.size >= maxStates) {
|
|
67
|
+
truncated = true;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
const cur = queue.shift();
|
|
71
|
+
for (const next of space.successors(cur)) {
|
|
72
|
+
const h = space.hash(next);
|
|
73
|
+
if (!visited.has(h)) {
|
|
74
|
+
visited.set(h, next);
|
|
75
|
+
queue.push(next);
|
|
76
|
+
if (visited.size >= maxStates) {
|
|
77
|
+
truncated = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (truncated)
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
states: Array.from(visited.values()),
|
|
87
|
+
explored: visited.size,
|
|
88
|
+
truncated,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// ── Reconstrucción de trazas ────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Reconstruye una traza desde un estado raíz (de `roots`) hasta
|
|
94
|
+
* el estado con hash `targetHash`, usando el mapa `parent` que
|
|
95
|
+
* mapea hash de hijo → { parent: S, hash: string } o null si root.
|
|
96
|
+
*/
|
|
97
|
+
function reconstructTrace(parent, byHash, targetHash) {
|
|
98
|
+
const path = [];
|
|
99
|
+
let h = targetHash;
|
|
100
|
+
const guard = new Set();
|
|
101
|
+
while (h !== null) {
|
|
102
|
+
if (guard.has(h))
|
|
103
|
+
break; // safety: no debería pasar
|
|
104
|
+
guard.add(h);
|
|
105
|
+
const node = byHash.get(h);
|
|
106
|
+
if (node === undefined)
|
|
107
|
+
break;
|
|
108
|
+
path.push(node);
|
|
109
|
+
const p = parent.get(h);
|
|
110
|
+
if (!p)
|
|
111
|
+
break;
|
|
112
|
+
h = p.parentHash;
|
|
113
|
+
}
|
|
114
|
+
return path.reverse();
|
|
115
|
+
}
|
|
116
|
+
// ── Safety (BFS con tracking de padre) ──────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* Verifica que `predicate` se cumple en *todos* los estados
|
|
119
|
+
* alcanzables. Si encuentra un estado violador, devuelve una
|
|
120
|
+
* traza mínima desde algún initial hasta él. Equivale a G p.
|
|
121
|
+
*/
|
|
122
|
+
function checkSafety(space, predicate, opts = {}) {
|
|
123
|
+
const maxStates = opts.maxStates ?? DEFAULT_MAX_STATES;
|
|
124
|
+
const visited = new Set();
|
|
125
|
+
const byHash = new Map();
|
|
126
|
+
const parent = new Map();
|
|
127
|
+
const queue = [];
|
|
128
|
+
for (const s of space.initial) {
|
|
129
|
+
const h = space.hash(s);
|
|
130
|
+
if (visited.has(h))
|
|
131
|
+
continue;
|
|
132
|
+
visited.add(h);
|
|
133
|
+
byHash.set(h, s);
|
|
134
|
+
parent.set(h, null);
|
|
135
|
+
if (!predicate(s)) {
|
|
136
|
+
return { safe: false, violatingState: s, trace: [s] };
|
|
137
|
+
}
|
|
138
|
+
queue.push(s);
|
|
139
|
+
}
|
|
140
|
+
while (queue.length > 0 && visited.size < maxStates) {
|
|
141
|
+
const cur = queue.shift();
|
|
142
|
+
const curHash = space.hash(cur);
|
|
143
|
+
for (const next of space.successors(cur)) {
|
|
144
|
+
const h = space.hash(next);
|
|
145
|
+
if (visited.has(h))
|
|
146
|
+
continue;
|
|
147
|
+
visited.add(h);
|
|
148
|
+
byHash.set(h, next);
|
|
149
|
+
parent.set(h, { parent: cur, parentHash: curHash });
|
|
150
|
+
if (!predicate(next)) {
|
|
151
|
+
return {
|
|
152
|
+
safe: false,
|
|
153
|
+
violatingState: next,
|
|
154
|
+
trace: reconstructTrace(parent, byHash, h),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
queue.push(next);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { safe: true };
|
|
161
|
+
}
|
|
162
|
+
/** Alias semántico: invariante = safety check con el mismo predicado. */
|
|
163
|
+
function checkInvariant(space, invariant, opts = {}) {
|
|
164
|
+
return checkSafety(space, invariant, opts);
|
|
165
|
+
}
|
|
166
|
+
// ── Bounded Model Checking ──────────────────────────────────
|
|
167
|
+
/**
|
|
168
|
+
* BMC: busca un estado donde `predicate` falla dentro de los
|
|
169
|
+
* primeros `depth` pasos desde initial. No certifica safety
|
|
170
|
+
* global; sirve para encontrar contraejemplos cortos.
|
|
171
|
+
*
|
|
172
|
+
* `depth=0` solo evalúa estados iniciales.
|
|
173
|
+
*/
|
|
174
|
+
function bmc(space, predicate, depth) {
|
|
175
|
+
if (!Number.isFinite(depth) || depth < 0) {
|
|
176
|
+
return { safe: true };
|
|
177
|
+
}
|
|
178
|
+
const visited = new Set();
|
|
179
|
+
const byHash = new Map();
|
|
180
|
+
const parent = new Map();
|
|
181
|
+
// Cola con profundidad (BFS por nivel).
|
|
182
|
+
const queue = [];
|
|
183
|
+
for (const s of space.initial) {
|
|
184
|
+
const h = space.hash(s);
|
|
185
|
+
if (visited.has(h))
|
|
186
|
+
continue;
|
|
187
|
+
visited.add(h);
|
|
188
|
+
byHash.set(h, s);
|
|
189
|
+
parent.set(h, null);
|
|
190
|
+
if (!predicate(s)) {
|
|
191
|
+
return { safe: false, violatingState: s, trace: [s] };
|
|
192
|
+
}
|
|
193
|
+
queue.push({ s, d: 0 });
|
|
194
|
+
}
|
|
195
|
+
while (queue.length > 0) {
|
|
196
|
+
const item = queue.shift();
|
|
197
|
+
if (!item)
|
|
198
|
+
break;
|
|
199
|
+
const { s: cur, d } = item;
|
|
200
|
+
if (d >= depth)
|
|
201
|
+
continue;
|
|
202
|
+
const curHash = space.hash(cur);
|
|
203
|
+
for (const next of space.successors(cur)) {
|
|
204
|
+
const h = space.hash(next);
|
|
205
|
+
if (visited.has(h))
|
|
206
|
+
continue;
|
|
207
|
+
visited.add(h);
|
|
208
|
+
byHash.set(h, next);
|
|
209
|
+
parent.set(h, { parent: cur, parentHash: curHash });
|
|
210
|
+
if (!predicate(next)) {
|
|
211
|
+
return {
|
|
212
|
+
safe: false,
|
|
213
|
+
violatingState: next,
|
|
214
|
+
trace: reconstructTrace(parent, byHash, h),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
queue.push({ s: next, d: d + 1 });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return { safe: true };
|
|
221
|
+
}
|
|
222
|
+
// ── Deadlock ────────────────────────────────────────────────
|
|
223
|
+
/**
|
|
224
|
+
* Detecta el primer estado alcanzable sin sucesores. Devuelve la
|
|
225
|
+
* traza desde initial hasta ese estado para diagnóstico.
|
|
226
|
+
*/
|
|
227
|
+
function hasDeadlock(space, opts = {}) {
|
|
228
|
+
const maxStates = opts.maxStates ?? DEFAULT_MAX_STATES;
|
|
229
|
+
const visited = new Set();
|
|
230
|
+
const byHash = new Map();
|
|
231
|
+
const parent = new Map();
|
|
232
|
+
const queue = [];
|
|
233
|
+
for (const s of space.initial) {
|
|
234
|
+
const h = space.hash(s);
|
|
235
|
+
if (visited.has(h))
|
|
236
|
+
continue;
|
|
237
|
+
visited.add(h);
|
|
238
|
+
byHash.set(h, s);
|
|
239
|
+
parent.set(h, null);
|
|
240
|
+
queue.push(s);
|
|
241
|
+
}
|
|
242
|
+
while (queue.length > 0 && visited.size < maxStates) {
|
|
243
|
+
const cur = queue.shift();
|
|
244
|
+
const curHash = space.hash(cur);
|
|
245
|
+
const succ = space.successors(cur);
|
|
246
|
+
if (succ.length === 0) {
|
|
247
|
+
return {
|
|
248
|
+
deadlocked: true,
|
|
249
|
+
state: cur,
|
|
250
|
+
trace: reconstructTrace(parent, byHash, curHash),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
for (const next of succ) {
|
|
254
|
+
const h = space.hash(next);
|
|
255
|
+
if (visited.has(h))
|
|
256
|
+
continue;
|
|
257
|
+
visited.add(h);
|
|
258
|
+
byHash.set(h, next);
|
|
259
|
+
parent.set(h, { parent: cur, parentHash: curHash });
|
|
260
|
+
queue.push(next);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { deadlocked: false };
|
|
264
|
+
}
|
|
265
|
+
// ── Búsqueda de lassos para liveness ────────────────────────
|
|
266
|
+
/**
|
|
267
|
+
* Tarjan-like SCC sobre el sub-grafo de estados que cumplen
|
|
268
|
+
* `accepting`. Para GF p (always-eventually p): el sistema GF p
|
|
269
|
+
* holds sii toda SCC no-trivial accesible contiene al menos un
|
|
270
|
+
* estado donde p hold. Simplificación útil: bucle infinito
|
|
271
|
+
* factible sii hay un ciclo (≥1 arista) en la parte alcanzable
|
|
272
|
+
* cuyo support incluye un estado con p=true.
|
|
273
|
+
*
|
|
274
|
+
* Para FG p (eventually-always): hay ciclo enteramente dentro
|
|
275
|
+
* del subgrafo p=true accesible. Equivalente: una SCC no trivial
|
|
276
|
+
* contenida en el subgrafo restringido a estados p=true.
|
|
277
|
+
*/
|
|
278
|
+
function tarjanSCCs(space, nodes, edgeFilter) {
|
|
279
|
+
const indices = new Map();
|
|
280
|
+
const lowlink = new Map();
|
|
281
|
+
const onStack = new Set();
|
|
282
|
+
const stack = [];
|
|
283
|
+
const sccs = [];
|
|
284
|
+
let idx = 0;
|
|
285
|
+
const callStack = [];
|
|
286
|
+
for (const [startHash, startNode] of nodes) {
|
|
287
|
+
if (indices.has(startHash))
|
|
288
|
+
continue;
|
|
289
|
+
indices.set(startHash, idx);
|
|
290
|
+
lowlink.set(startHash, idx);
|
|
291
|
+
idx += 1;
|
|
292
|
+
stack.push(startHash);
|
|
293
|
+
onStack.add(startHash);
|
|
294
|
+
const startSucc = space
|
|
295
|
+
.successors(startNode)
|
|
296
|
+
.filter((n) => {
|
|
297
|
+
if (!edgeFilter(startNode, n))
|
|
298
|
+
return false;
|
|
299
|
+
return nodes.has(space.hash(n));
|
|
300
|
+
})
|
|
301
|
+
.map((n) => space.hash(n));
|
|
302
|
+
callStack.push({ hash: startHash, node: startNode, succHashes: startSucc, i: 0 });
|
|
303
|
+
while (callStack.length > 0) {
|
|
304
|
+
const frame = callStack[callStack.length - 1];
|
|
305
|
+
if (!frame)
|
|
306
|
+
break;
|
|
307
|
+
if (frame.i < frame.succHashes.length) {
|
|
308
|
+
const wHash = frame.succHashes[frame.i];
|
|
309
|
+
frame.i += 1;
|
|
310
|
+
if (!indices.has(wHash)) {
|
|
311
|
+
const wNode = nodes.get(wHash);
|
|
312
|
+
if (!wNode)
|
|
313
|
+
continue;
|
|
314
|
+
indices.set(wHash, idx);
|
|
315
|
+
lowlink.set(wHash, idx);
|
|
316
|
+
idx += 1;
|
|
317
|
+
stack.push(wHash);
|
|
318
|
+
onStack.add(wHash);
|
|
319
|
+
const wSucc = space
|
|
320
|
+
.successors(wNode)
|
|
321
|
+
.filter((n) => {
|
|
322
|
+
if (!edgeFilter(wNode, n))
|
|
323
|
+
return false;
|
|
324
|
+
return nodes.has(space.hash(n));
|
|
325
|
+
})
|
|
326
|
+
.map((n) => space.hash(n));
|
|
327
|
+
callStack.push({ hash: wHash, node: wNode, succHashes: wSucc, i: 0 });
|
|
328
|
+
}
|
|
329
|
+
else if (onStack.has(wHash)) {
|
|
330
|
+
const wIdx = indices.get(wHash);
|
|
331
|
+
const cur = lowlink.get(frame.hash);
|
|
332
|
+
if (wIdx < cur)
|
|
333
|
+
lowlink.set(frame.hash, wIdx);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// Pop the frame, propagate lowlink up.
|
|
338
|
+
const finishedHash = frame.hash;
|
|
339
|
+
callStack.pop();
|
|
340
|
+
const flow = lowlink.get(finishedHash);
|
|
341
|
+
const findex = indices.get(finishedHash);
|
|
342
|
+
if (flow === findex) {
|
|
343
|
+
const scc = [];
|
|
344
|
+
while (stack.length > 0) {
|
|
345
|
+
const top = stack.pop();
|
|
346
|
+
onStack.delete(top);
|
|
347
|
+
scc.push(top);
|
|
348
|
+
if (top === finishedHash)
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
sccs.push(scc);
|
|
352
|
+
}
|
|
353
|
+
const parentFrame = callStack[callStack.length - 1];
|
|
354
|
+
if (parentFrame) {
|
|
355
|
+
const pl = lowlink.get(parentFrame.hash);
|
|
356
|
+
if (flow < pl)
|
|
357
|
+
lowlink.set(parentFrame.hash, flow);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return sccs;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Determina si una SCC es no-trivial: tiene >1 nodos o un único
|
|
366
|
+
* nodo con auto-bucle (self-loop) bajo `edgeFilter`.
|
|
367
|
+
*/
|
|
368
|
+
function sccNonTrivial(space, scc, nodes, edgeFilter) {
|
|
369
|
+
if (scc.length === 0)
|
|
370
|
+
return false;
|
|
371
|
+
if (scc.length > 1)
|
|
372
|
+
return true;
|
|
373
|
+
const onlyHash = scc[0];
|
|
374
|
+
const onlyNode = nodes.get(onlyHash);
|
|
375
|
+
if (!onlyNode)
|
|
376
|
+
return false;
|
|
377
|
+
for (const next of space.successors(onlyNode)) {
|
|
378
|
+
if (!edgeFilter(onlyNode, next))
|
|
379
|
+
continue;
|
|
380
|
+
if (space.hash(next) === onlyHash)
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Encuentra un ciclo (loop) dentro de una SCC con ≥1 arista y
|
|
387
|
+
* lo devuelve como array de estados (cerrado: último → primero).
|
|
388
|
+
* Asume SCC no-trivial bajo `edgeFilter`.
|
|
389
|
+
*/
|
|
390
|
+
function findCycleInSCC(space, scc, nodes, edgeFilter) {
|
|
391
|
+
const sccSet = new Set(scc);
|
|
392
|
+
if (scc.length === 1) {
|
|
393
|
+
const onlyHash = scc[0];
|
|
394
|
+
const onlyNode = nodes.get(onlyHash);
|
|
395
|
+
if (!onlyNode)
|
|
396
|
+
return [];
|
|
397
|
+
return [onlyNode];
|
|
398
|
+
}
|
|
399
|
+
// BFS dentro de la SCC desde un nodo arbitrario hasta volver.
|
|
400
|
+
const startHash = scc[0];
|
|
401
|
+
const start = nodes.get(startHash);
|
|
402
|
+
if (!start)
|
|
403
|
+
return [];
|
|
404
|
+
// Buscamos el camino más corto start → start con ≥1 arista.
|
|
405
|
+
const parent = new Map();
|
|
406
|
+
const queue = [];
|
|
407
|
+
const succs = space
|
|
408
|
+
.successors(start)
|
|
409
|
+
.filter((n) => edgeFilter(start, n) && sccSet.has(space.hash(n)));
|
|
410
|
+
for (const n of succs) {
|
|
411
|
+
const h = space.hash(n);
|
|
412
|
+
if (!parent.has(h)) {
|
|
413
|
+
parent.set(h, { parentHash: startHash, node: start });
|
|
414
|
+
if (h === startHash) {
|
|
415
|
+
// self-loop (no debería entrar aquí porque scc.length>1, pero por defensa)
|
|
416
|
+
return [start];
|
|
417
|
+
}
|
|
418
|
+
queue.push({ hash: h, node: n });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
while (queue.length > 0) {
|
|
422
|
+
const item = queue.shift();
|
|
423
|
+
if (!item)
|
|
424
|
+
break;
|
|
425
|
+
const { hash: curHash, node: cur } = item;
|
|
426
|
+
if (curHash === startHash)
|
|
427
|
+
break;
|
|
428
|
+
for (const next of space.successors(cur)) {
|
|
429
|
+
if (!edgeFilter(cur, next))
|
|
430
|
+
continue;
|
|
431
|
+
const nh = space.hash(next);
|
|
432
|
+
if (!sccSet.has(nh))
|
|
433
|
+
continue;
|
|
434
|
+
if (parent.has(nh))
|
|
435
|
+
continue;
|
|
436
|
+
parent.set(nh, { parentHash: curHash, node: cur });
|
|
437
|
+
if (nh === startHash) {
|
|
438
|
+
// reconstruir start → ... → cur → start
|
|
439
|
+
const path = [start];
|
|
440
|
+
const inverse = [];
|
|
441
|
+
let h = curHash;
|
|
442
|
+
while (h !== startHash) {
|
|
443
|
+
const p = parent.get(h);
|
|
444
|
+
if (!p)
|
|
445
|
+
break;
|
|
446
|
+
inverse.push(p.node);
|
|
447
|
+
if (p.parentHash === startHash)
|
|
448
|
+
break;
|
|
449
|
+
h = p.parentHash;
|
|
450
|
+
}
|
|
451
|
+
// inverse va desde cur hacia atrás hasta el sucesor inmediato de start.
|
|
452
|
+
// path es [start, ...inverse.reverse(), cur]
|
|
453
|
+
// pero start ya está; añadimos camino inverso revertido (sin start), y luego cur.
|
|
454
|
+
// Cuidado: si solo hay 1 paso (start→cur→start), inverse = [start] (porque parent[cur] = start)
|
|
455
|
+
// path final esperado: [start, cur]
|
|
456
|
+
const inner = inverse.reverse();
|
|
457
|
+
// inner ahora incluye start como primer elemento si vino directo; lo limpiamos:
|
|
458
|
+
if (inner.length > 0 && space.hash(inner[0]) === startHash) {
|
|
459
|
+
inner.shift();
|
|
460
|
+
}
|
|
461
|
+
for (const x of inner)
|
|
462
|
+
path.push(x);
|
|
463
|
+
path.push(cur);
|
|
464
|
+
return path;
|
|
465
|
+
}
|
|
466
|
+
queue.push({ hash: nh, node: next });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Fallback: ciclo trivial alrededor de start (no debería ocurrir si SCC es real).
|
|
470
|
+
return [start];
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Camino más corto desde algún initial hasta `targetHash`,
|
|
474
|
+
* dentro del sub-grafo restringido por `nodes` y `edgeFilter`.
|
|
475
|
+
* Si no hay camino, devuelve [].
|
|
476
|
+
*/
|
|
477
|
+
function bfsStem(space, targetHash, nodes, edgeFilter) {
|
|
478
|
+
const parent = new Map();
|
|
479
|
+
const visited = new Set();
|
|
480
|
+
const byHash = new Map();
|
|
481
|
+
const queue = [];
|
|
482
|
+
for (const s of space.initial) {
|
|
483
|
+
const h = space.hash(s);
|
|
484
|
+
if (!nodes.has(h))
|
|
485
|
+
continue;
|
|
486
|
+
if (visited.has(h))
|
|
487
|
+
continue;
|
|
488
|
+
visited.add(h);
|
|
489
|
+
byHash.set(h, s);
|
|
490
|
+
if (h === targetHash) {
|
|
491
|
+
return [s];
|
|
492
|
+
}
|
|
493
|
+
queue.push(s);
|
|
494
|
+
}
|
|
495
|
+
while (queue.length > 0) {
|
|
496
|
+
const cur = queue.shift();
|
|
497
|
+
const curHash = space.hash(cur);
|
|
498
|
+
for (const next of space.successors(cur)) {
|
|
499
|
+
if (!edgeFilter(cur, next))
|
|
500
|
+
continue;
|
|
501
|
+
const nh = space.hash(next);
|
|
502
|
+
if (!nodes.has(nh))
|
|
503
|
+
continue;
|
|
504
|
+
if (visited.has(nh))
|
|
505
|
+
continue;
|
|
506
|
+
visited.add(nh);
|
|
507
|
+
byHash.set(nh, next);
|
|
508
|
+
parent.set(nh, { parentHash: curHash, node: cur });
|
|
509
|
+
if (nh === targetHash) {
|
|
510
|
+
// Reconstruir
|
|
511
|
+
const path = [next];
|
|
512
|
+
let h = curHash;
|
|
513
|
+
while (true) {
|
|
514
|
+
const node = byHash.get(h);
|
|
515
|
+
if (!node)
|
|
516
|
+
break;
|
|
517
|
+
path.push(node);
|
|
518
|
+
const p = parent.get(h);
|
|
519
|
+
if (!p)
|
|
520
|
+
break;
|
|
521
|
+
h = p.parentHash;
|
|
522
|
+
}
|
|
523
|
+
return path.reverse();
|
|
524
|
+
}
|
|
525
|
+
queue.push(next);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return [];
|
|
529
|
+
}
|
|
530
|
+
// ── Liveness: GF p (always eventually p) ────────────────────
|
|
531
|
+
/**
|
|
532
|
+
* GF p: en todo camino infinito, p ocurre infinitas veces.
|
|
533
|
+
* Contraejemplo: lasso accesible (stem + loop) tal que NINGÚN
|
|
534
|
+
* estado del loop satisface p (porque entonces existe un camino
|
|
535
|
+
* infinito que evita p eventualmente).
|
|
536
|
+
*
|
|
537
|
+
* Algoritmo: SCCs no-triviales accesibles desde initial; si alguna
|
|
538
|
+
* NO contiene estado p=true → contraejemplo. Si todas las SCCs
|
|
539
|
+
* no-triviales accesibles contienen al menos un estado p=true,
|
|
540
|
+
* holds.
|
|
541
|
+
*/
|
|
542
|
+
function checkAlwaysEventually(space, p, opts = {}) {
|
|
543
|
+
const reach = reachableStates(space, opts);
|
|
544
|
+
const nodes = new Map();
|
|
545
|
+
for (const s of reach.states)
|
|
546
|
+
nodes.set(space.hash(s), s);
|
|
547
|
+
const allEdges = (_a, _b) => true;
|
|
548
|
+
const sccs = tarjanSCCs(space, nodes, allEdges);
|
|
549
|
+
for (const scc of sccs) {
|
|
550
|
+
if (!sccNonTrivial(space, scc, nodes, allEdges))
|
|
551
|
+
continue;
|
|
552
|
+
// ¿Hay algún estado en la SCC que satisfaga p?
|
|
553
|
+
let containsP = false;
|
|
554
|
+
for (const h of scc) {
|
|
555
|
+
const node = nodes.get(h);
|
|
556
|
+
if (node && p(node)) {
|
|
557
|
+
containsP = true;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (!containsP) {
|
|
562
|
+
// Construir contraejemplo lasso.
|
|
563
|
+
const loop = findCycleInSCC(space, scc, nodes, allEdges);
|
|
564
|
+
const target = loop[0];
|
|
565
|
+
if (!target)
|
|
566
|
+
continue;
|
|
567
|
+
const stem = bfsStem(space, space.hash(target), nodes, allEdges);
|
|
568
|
+
if (stem.length === 0)
|
|
569
|
+
continue;
|
|
570
|
+
return { holds: false, lasso: { stem, loop } };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return { holds: true };
|
|
574
|
+
}
|
|
575
|
+
// ── Liveness: FG p (eventually always p) ────────────────────
|
|
576
|
+
/**
|
|
577
|
+
* FG p: existe un punto a partir del cual p siempre holds.
|
|
578
|
+
*
|
|
579
|
+
* Holds sii: existe un lasso accesible cuyo loop está contenido
|
|
580
|
+
* íntegramente en {s | p(s)}. Es decir, SCC no-trivial accesible
|
|
581
|
+
* dentro del sub-grafo inducido por p=true.
|
|
582
|
+
*/
|
|
583
|
+
function checkEventuallyAlways(space, p, opts = {}) {
|
|
584
|
+
const reach = reachableStates(space, opts);
|
|
585
|
+
// Sub-grafo de estados con p=true.
|
|
586
|
+
const pNodes = new Map();
|
|
587
|
+
for (const s of reach.states) {
|
|
588
|
+
if (p(s))
|
|
589
|
+
pNodes.set(space.hash(s), s);
|
|
590
|
+
}
|
|
591
|
+
// Aristas internas (from y to en pNodes).
|
|
592
|
+
const internalEdges = (from, to) => {
|
|
593
|
+
return pNodes.has(space.hash(from)) && pNodes.has(space.hash(to));
|
|
594
|
+
};
|
|
595
|
+
const sccs = tarjanSCCs(space, pNodes, internalEdges);
|
|
596
|
+
for (const scc of sccs) {
|
|
597
|
+
if (!sccNonTrivial(space, scc, pNodes, internalEdges))
|
|
598
|
+
continue;
|
|
599
|
+
const loop = findCycleInSCC(space, scc, pNodes, internalEdges);
|
|
600
|
+
const target = loop[0];
|
|
601
|
+
if (!target)
|
|
602
|
+
continue;
|
|
603
|
+
// El stem se construye en el grafo completo, no en el sub-grafo.
|
|
604
|
+
const allNodes = new Map();
|
|
605
|
+
for (const s of reach.states)
|
|
606
|
+
allNodes.set(space.hash(s), s);
|
|
607
|
+
const allEdges = (_a, _b) => true;
|
|
608
|
+
const stem = bfsStem(space, space.hash(target), allNodes, allEdges);
|
|
609
|
+
if (stem.length === 0)
|
|
610
|
+
continue;
|
|
611
|
+
return { holds: true, lasso: { stem, loop } };
|
|
612
|
+
}
|
|
613
|
+
return { holds: false };
|
|
614
|
+
}
|
|
615
|
+
function mutualExclusionSpace() {
|
|
616
|
+
const initial = [
|
|
617
|
+
{ p1: 'idle', p2: 'idle', turn: 1 },
|
|
618
|
+
{ p1: 'idle', p2: 'idle', turn: 2 },
|
|
619
|
+
];
|
|
620
|
+
function step(proc, s) {
|
|
621
|
+
const my = proc === 1 ? s.p1 : s.p2;
|
|
622
|
+
const other = proc === 1 ? s.p2 : s.p1;
|
|
623
|
+
const otherProc = proc === 1 ? 2 : 1;
|
|
624
|
+
const out = [];
|
|
625
|
+
const set = (mine, turn) => {
|
|
626
|
+
if (proc === 1)
|
|
627
|
+
return { p1: mine, p2: s.p2, turn };
|
|
628
|
+
return { p1: s.p1, p2: mine, turn };
|
|
629
|
+
};
|
|
630
|
+
if (my === 'idle') {
|
|
631
|
+
// Peterson: al entrar en waiting, cedo turn al otro.
|
|
632
|
+
out.push(set('waiting', otherProc));
|
|
633
|
+
}
|
|
634
|
+
else if (my === 'waiting') {
|
|
635
|
+
// Entra a critical sii turn==self o other==idle.
|
|
636
|
+
if (s.turn === proc || other === 'idle') {
|
|
637
|
+
out.push(set('critical', s.turn));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
// critical → idle, conserva turn (el otro ya lo tendrá si tocó).
|
|
642
|
+
out.push(set('idle', s.turn));
|
|
643
|
+
}
|
|
644
|
+
return out;
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
initial,
|
|
648
|
+
successors: (s) => [...step(1, s), ...step(2, s)],
|
|
649
|
+
labels: (s) => {
|
|
650
|
+
const ls = new Set();
|
|
651
|
+
if (s.p1 === 'critical')
|
|
652
|
+
ls.add('p1_critical');
|
|
653
|
+
if (s.p2 === 'critical')
|
|
654
|
+
ls.add('p2_critical');
|
|
655
|
+
if (s.p1 === 'critical' && s.p2 === 'critical')
|
|
656
|
+
ls.add('mutex_violation');
|
|
657
|
+
return ls;
|
|
658
|
+
},
|
|
659
|
+
equals: (a, b) => a.p1 === b.p1 && a.p2 === b.p2 && a.turn === b.turn,
|
|
660
|
+
hash: (s) => `${s.p1}|${s.p2}|${s.turn}`,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function diningPhilosophersSpace(n) {
|
|
664
|
+
if (n < 2)
|
|
665
|
+
throw new Error('n>=2 required');
|
|
666
|
+
const initialPhils = Array.from({ length: n }, () => 'thinking');
|
|
667
|
+
const initialForks = Array.from({ length: n }, () => false);
|
|
668
|
+
const initial = [{ phils: initialPhils, forks: initialForks }];
|
|
669
|
+
function leftFork(i) {
|
|
670
|
+
return i;
|
|
671
|
+
}
|
|
672
|
+
function rightFork(i) {
|
|
673
|
+
return (i + 1) % n;
|
|
674
|
+
}
|
|
675
|
+
function step(i, s) {
|
|
676
|
+
const phil = s.phils[i];
|
|
677
|
+
const out = [];
|
|
678
|
+
const cloneArr = (a) => a.slice();
|
|
679
|
+
if (phil === 'thinking') {
|
|
680
|
+
const lf = leftFork(i);
|
|
681
|
+
if (!s.forks[lf]) {
|
|
682
|
+
const newPhils = cloneArr(s.phils);
|
|
683
|
+
newPhils[i] = 'has_left';
|
|
684
|
+
const newForks = cloneArr(s.forks);
|
|
685
|
+
newForks[lf] = true;
|
|
686
|
+
out.push({ phils: newPhils, forks: newForks });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
else if (phil === 'has_left') {
|
|
690
|
+
const rf = rightFork(i);
|
|
691
|
+
if (!s.forks[rf]) {
|
|
692
|
+
const newPhils = cloneArr(s.phils);
|
|
693
|
+
newPhils[i] = 'eating';
|
|
694
|
+
const newForks = cloneArr(s.forks);
|
|
695
|
+
newForks[rf] = true;
|
|
696
|
+
out.push({ phils: newPhils, forks: newForks });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
// eating → thinking, libera ambos.
|
|
701
|
+
const newPhils = cloneArr(s.phils);
|
|
702
|
+
newPhils[i] = 'thinking';
|
|
703
|
+
const newForks = cloneArr(s.forks);
|
|
704
|
+
newForks[leftFork(i)] = false;
|
|
705
|
+
newForks[rightFork(i)] = false;
|
|
706
|
+
out.push({ phils: newPhils, forks: newForks });
|
|
707
|
+
}
|
|
708
|
+
return out;
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
initial,
|
|
712
|
+
successors: (s) => {
|
|
713
|
+
const out = [];
|
|
714
|
+
for (let i = 0; i < n; i += 1) {
|
|
715
|
+
for (const next of step(i, s))
|
|
716
|
+
out.push(next);
|
|
717
|
+
}
|
|
718
|
+
return out;
|
|
719
|
+
},
|
|
720
|
+
labels: (s) => {
|
|
721
|
+
const ls = new Set();
|
|
722
|
+
for (let i = 0; i < n; i += 1) {
|
|
723
|
+
if (s.phils[i] === 'eating')
|
|
724
|
+
ls.add(`eating_${i}`);
|
|
725
|
+
}
|
|
726
|
+
return ls;
|
|
727
|
+
},
|
|
728
|
+
equals: (a, b) => {
|
|
729
|
+
if (a.phils.length !== b.phils.length)
|
|
730
|
+
return false;
|
|
731
|
+
for (let i = 0; i < a.phils.length; i += 1) {
|
|
732
|
+
if (a.phils[i] !== b.phils[i])
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
if (a.forks.length !== b.forks.length)
|
|
736
|
+
return false;
|
|
737
|
+
for (let i = 0; i < a.forks.length; i += 1) {
|
|
738
|
+
if (a.forks[i] !== b.forks[i])
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
return true;
|
|
742
|
+
},
|
|
743
|
+
hash: (s) => `P:${s.phils.join(',')}|F:${s.forks.map((b) => (b ? '1' : '0')).join('')}`,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function readerWriterSpace(numReaders) {
|
|
747
|
+
if (numReaders < 1)
|
|
748
|
+
throw new Error('numReaders>=1 required');
|
|
749
|
+
const initial = [{ readers: 0, writer: false, maxReaders: numReaders }];
|
|
750
|
+
return {
|
|
751
|
+
initial,
|
|
752
|
+
successors: (s) => {
|
|
753
|
+
const out = [];
|
|
754
|
+
// Adquirir lectura.
|
|
755
|
+
if (!s.writer && s.readers < s.maxReaders) {
|
|
756
|
+
out.push({ readers: s.readers + 1, writer: false, maxReaders: s.maxReaders });
|
|
757
|
+
}
|
|
758
|
+
// Liberar lectura.
|
|
759
|
+
if (s.readers > 0) {
|
|
760
|
+
out.push({ readers: s.readers - 1, writer: false, maxReaders: s.maxReaders });
|
|
761
|
+
}
|
|
762
|
+
// Adquirir escritura.
|
|
763
|
+
if (s.readers === 0 && !s.writer) {
|
|
764
|
+
out.push({ readers: 0, writer: true, maxReaders: s.maxReaders });
|
|
765
|
+
}
|
|
766
|
+
// Liberar escritura.
|
|
767
|
+
if (s.writer) {
|
|
768
|
+
out.push({ readers: 0, writer: false, maxReaders: s.maxReaders });
|
|
769
|
+
}
|
|
770
|
+
return out;
|
|
771
|
+
},
|
|
772
|
+
labels: (s) => {
|
|
773
|
+
const ls = new Set();
|
|
774
|
+
if (s.writer)
|
|
775
|
+
ls.add('writer');
|
|
776
|
+
if (s.readers > 0)
|
|
777
|
+
ls.add('reading');
|
|
778
|
+
if (s.writer && s.readers > 0)
|
|
779
|
+
ls.add('rw_violation');
|
|
780
|
+
return ls;
|
|
781
|
+
},
|
|
782
|
+
equals: (a, b) => a.readers === b.readers && a.writer === b.writer && a.maxReaders === b.maxReaders,
|
|
783
|
+
hash: (s) => `R:${s.readers}|W:${s.writer ? '1' : '0'}|M:${s.maxReaders}`,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
//# sourceMappingURL=index.js.map
|