Skip to content

Commit 1a73487

Browse files
committed
a bit more
1 parent 259e9ac commit 1a73487

5 files changed

Lines changed: 52 additions & 2 deletions

File tree

README_LemmaScript.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ Gates connections so the graph stays acyclic. Node-level (source/target ids), ne
1919

2020
**Acyclicity bridge**`acyclic(edges) ==> ( acyclic(edges + e) <==> !wouldCreateCycle(edges, src, tgt) )`: gated insertion never creates a cycle, and never blocks a safe edge.
2121

22-
Predicate `reach` and the path lemmas are hand-written in `graph.dfy` (additions-only). Trust manifest: proof is over `EdgeBase[]`/node ids (no React), and holds only if every commit routes through the gate. Demonstrated in the `CycleGate` example (`examples/react/src/examples/CycleGate/`) via `onConnect` (commit) + `isValidConnection` (drag feedback); drag a loop and it is rejected on screen.
22+
**Topological-rank witness**`rank(n)` = number of nodes that can reach `n`; proven (`TopoRankMonotone`) to strictly increase along every edge of an acyclic graph, so sorting nodes by `rank` is a safe evaluation order. `canReach` is exported as a public primitive; the count + sort are trusted glue over it.
23+
24+
Predicate `reach` and the path lemmas are hand-written in `graph.dfy` (additions-only). Trust manifest: proof is over `EdgeBase[]`/node ids (no React), and holds only if every commit routes through the gate. Demonstrated in the `CycleGate` example (`examples/react/src/examples/CycleGate/`) via `onConnect` (commit) + `isValidConnection` (drag feedback): drag a loop and it is rejected on screen (red dashed line), while a live "safe evaluation order" reflects the topological rank.
2325

2426
### Edge Utilities (`packages/system/src/utils/edges/general.ts`)
2527

examples/react/src/examples/CycleGate/index.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
useEdgesState,
1111
useReactFlow,
1212
wouldCreateCycle,
13+
canReach,
1314
MarkerType,
1415
type Node,
1516
type Edge,
@@ -64,6 +65,14 @@ const CycleGateFlow = () => {
6465
[getEdges, setEdges]
6566
);
6667

68+
// Stage 3: a safe evaluation order. rank(n) = #nodes that can reach n; sorting by
69+
// ascending rank is a topological order — `TopoRankMonotone` (graph.dfy) proves rank
70+
// strictly increases along every edge of an acyclic graph. The verified primitive is
71+
// `canReach`; the count + sort here are trusted glue over it.
72+
const ids = nodes.map((n) => n.id);
73+
const rankOf = (n: string) => ids.filter((m) => canReach(edges, m, n)).length;
74+
const order = [...ids].sort((a, b) => rankOf(a) - rankOf(b));
75+
6776
return (
6877
<>
6978
{/* Make a cycle-creating drag obviously rejected: React Flow tags the in-progress
@@ -106,6 +115,9 @@ const CycleGateFlow = () => {
106115
<span style={{ color: '#e74c3c', fontWeight: 600 }}>red &amp; dashed</span> and is refused —
107116
proven in <code>graph.ts</code>.
108117
</div>
118+
<div style={{ marginTop: 8, color: '#2c3e50' }}>
119+
Safe evaluation order: <strong>{order.join(' → ')}</strong>
120+
</div>
109121
</div>
110122
</Panel>
111123
</ReactFlow>

packages/react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export {
141141
getOutgoers,
142142
getConnectedEdges,
143143
wouldCreateCycle,
144+
canReach,
144145
} from '@xyflow/system';
145146

146147
export { addEdge, reconnectEdge } from './utils/edges';

packages/system/src/utils/graph.dfy

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,3 +324,38 @@ lemma AcyclicBridge(edges: seq<EdgeBase>, e: EdgeBase)
324324
}
325325
}
326326
}
327+
328+
// ── Stage 3: verified topological-rank witness ─────────────────────────────────────
329+
// rank(n) = number of nodes that can reach n (its ancestors). For an acyclic graph this
330+
// is a topological numbering: it strictly increases along every edge, so processing
331+
// nodes by increasing rank is a safe evaluation order. (Sorting by the rank is trusted
332+
// glue; the monotonicity is what is proven.)
333+
ghost function ancestorsWithin(edges: seq<EdgeBase>, nodes: set<string>, n: string): set<string>
334+
{
335+
set m | m in nodes && reach(edges, m, n)
336+
}
337+
338+
lemma TopoRankMonotone(edges: seq<EdgeBase>, nodes: set<string>, u: string, v: string)
339+
requires acyclic(edges)
340+
requires hasEdge(edges, u, v)
341+
requires u in nodes && v in nodes
342+
ensures |ancestorsWithin(edges, nodes, u)| < |ancestorsWithin(edges, nodes, v)|
343+
{
344+
HasEdgeReach(edges, u, v); // reach(u, v)
345+
// Every ancestor of u is an ancestor of v (m ->* u -> v).
346+
forall m | m in ancestorsWithin(edges, nodes, u)
347+
ensures m in ancestorsWithin(edges, nodes, v)
348+
{
349+
ReachTrans(edges, m, u, v);
350+
}
351+
ReachRefl(edges, v);
352+
assert !reach(edges, v, u); // acyclic: hasEdge(u,v) ==> !reach(v,u)
353+
var au := ancestorsWithin(edges, nodes, u);
354+
var av := ancestorsWithin(edges, nodes, v);
355+
assert au <= av; // from the forall
356+
assert v in av && v !in au; // v is an ancestor of v, not of u
357+
// |av| = |au| + |av - au| (disjoint union), and av - au is non-empty (contains v).
358+
assert au !! (av - au) && au + (av - au) == av;
359+
assert |av| == |au| + |av - au|;
360+
assert |av - au| >= 1;
361+
}

packages/system/src/utils/graph.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType exten
143143

144144
// Reflexive reachability over the edge list: bounded BFS from `from`, terminating
145145
// because `visited` grows monotonically toward the finite set of node ids.
146-
function canReach(edges: EdgeBase[], from: string, to: string): boolean {
146+
export function canReach(edges: EdgeBase[], from: string, to: string): boolean {
147147
//@ verify
148148
let frontier: string[] = [from];
149149
let visited: string[] = [];

0 commit comments

Comments
 (0)