Skip to content

Commit b08f6f5

Browse files
routing theorems article and quickcheck
1 parent 57854f0 commit b08f6f5

3 files changed

Lines changed: 287 additions & 3 deletions

File tree

article/routing-theorems.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Cast Routing Correctness via Affine Tiling
2+
3+
A correctness contract for Monarch cast actor routing.
4+
5+
This note states the correctness target for Monarch cast actor routing: given an affine target tile and an availability predicate, a cast should deliver exactly once to every live actor in the target tile, never deliver outside the target tile, and never route through unavailable actors.
6+
7+
The companion note, `routing-foundations.md`, explains how the algebra is built. This note states the delivery contract that an implementation can be checked against.
8+
9+
The Haskell model, [`tile`](https://github.com/shayne-fletcher/tile.git), is small enough to support these claims with QuickCheck properties.
10+
11+
## Notation
12+
13+
Let `T` be an affine target tile.
14+
15+
- `R(T)` is the set of ranks in the target tile
16+
- `root(T)` is the offset rank of the tile
17+
18+
Let `E` be a schedule, i.e. a finite list of directed send steps.
19+
20+
- `senders(E)` is the set of sources in `E`
21+
- `receivers(E)` is the set of destinations in `E`
22+
23+
Let `sched(T)` be the fault-free schedule produced by block partitioning.
24+
25+
Let `live ⊆ R(T)` be the set of available members in the target tile. Equivalently, `live` is the complement of an occlusion predicate restricted to `R(T)`.
26+
27+
Let `occSched(T, live)` be the routed schedule produced under that availability set.
28+
29+
## Geometric Laws
30+
31+
The low-level affine facts are prerequisites:
32+
33+
- ranks and points round-trip
34+
- rank enumeration is exact
35+
- affine slicing produces affine subspaces
36+
- affine slicing stays inside its parent space
37+
- decomposition children stay inside their parent tile
38+
39+
These are properties of the representation, not the main routing result. They are the geometry that makes the delivery theorems meaningful.
40+
41+
## Theorem T1: Fault-Free Cast Coverage
42+
43+
Let `T` be a non-empty affine target tile, and let:
44+
45+
```text
46+
n = |R(T)|
47+
sched(T) = E
48+
```
49+
50+
Then `E` is a spanning send tree over `R(T)` rooted at `root(T)`:
51+
52+
```text
53+
|E| = n - 1
54+
receivers(E) = R(T) - {root(T)}
55+
each receiver appears exactly once
56+
senders(E) ⊆ R(T)
57+
receivers(E) ⊆ R(T)
58+
```
59+
60+
Equivalently: every non-root target receives exactly once, the root does not receive, and no actor outside the target tile participates.
61+
62+
This theorem states delivery behavior, not schedule order. DFS and BFS may satisfy the same law with different edge orderings.
63+
64+
## Theorem T2: Occluded Cast Coverage
65+
66+
If `live = ∅`, then:
67+
68+
```text
69+
occSched(T, live) = Nothing
70+
```
71+
72+
If `live ≠ ∅`, then:
73+
74+
```text
75+
occSched(T, live) = Just (ingress, E)
76+
```
77+
78+
where:
79+
80+
```text
81+
ingress ∈ live
82+
receivers(E) = live - {ingress}
83+
each receiver appears exactly once
84+
senders(E) ⊆ live
85+
receivers(E) ⊆ live
86+
```
87+
88+
Equivalently: the ingress is live, every other live target receives exactly once, and unavailable actors neither send nor receive.
89+
90+
The affine geometry is unchanged. Occlusion only changes representative selection and pruning.
91+
92+
## Corollary C1: Jagged Regions
93+
94+
Let `J` be a jagged participant set inside an affine envelope `T`. Define:
95+
96+
```text
97+
live = J
98+
occluded = R(T) - J
99+
```
100+
101+
Then routing the envelope under `live` delivers exactly to the jagged region:
102+
103+
```text
104+
receivers(E) ∪ {ingress} = J
105+
```
106+
107+
Sparse participation and node failure are the same problem at this layer.
108+
109+
## Corollary C2: Monarch Cast Correctness Target
110+
111+
For a Monarch cast implementation, the core correctness target is:
112+
113+
```text
114+
Given an affine target tile and an availability predicate,
115+
cast delivers exactly once to every live actor in the target tile,
116+
never delivers to an actor outside the target tile,
117+
never delivers to an unavailable actor,
118+
and every forwarding actor is live under the representative policy.
119+
```
120+
121+
Internal reshape is allowed to change the routing tree, but not the target set. A reshaped cast should deliver to the same live members of the affine target tile as the unreshaped model. This preservation law is the next piece to model explicitly.
122+
123+
## Supporting Properties
124+
125+
The Haskell model supports these claims with QuickCheck properties in [`test/Main.hs`](https://github.com/shayne-fletcher/tile/blob/main/test/Main.hs):
126+
127+
```text
128+
affine rank/point roundtrip
129+
ranks enumerate the affine space exactly once
130+
affine slicing is closed and included in its parent
131+
structural and communication children are included in their parent
132+
fault-free schedules form a spanning send tree
133+
occluded schedules deliver exactly to live members
134+
```
135+
136+
The first four properties support the geometric assumptions. The last two correspond directly to T1 and T2.

test/Main.hs

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module Main (main) where
33
import Data.List (sort)
44
import Test.Tasty
55
import Test.Tasty.HUnit
6+
import Test.Tasty.QuickCheck
67
import Tile
78

89
main :: IO ()
@@ -15,6 +16,7 @@ tests =
1516
[ layoutTests,
1617
neighborTests,
1718
selectTests,
19+
theoremTests,
1820
inclusionTests,
1921
tilingTests,
2022
scheduleTests,
@@ -51,6 +53,131 @@ neighborTests =
5153
neighbors (rowMajor [2, 2, 2]) 3 @?= [7, 1, 2]
5254
]
5355

56+
theoremTests :: TestTree
57+
theoremTests =
58+
testGroup
59+
"theorems"
60+
[ testProperty "T1 affine rank/point roundtrip" propAffineRoundtrip,
61+
testProperty "T2 ranks enumerate the affine space exactly once" propRanksEnumerateSpace,
62+
testProperty "T3 affine slicing is closed and included in its parent" propAffineSliceIncluded,
63+
testProperty "T4 structural and communication children are included in their parent" propChildrenIncluded,
64+
testProperty "T5 fault-free schedules form a spanning send tree" propFaultFreeScheduleSpansTile,
65+
testProperty "T6 occluded schedules deliver exactly to live members" propOccludedScheduleCoversLiveMembers
66+
]
67+
68+
propAffineRoundtrip :: Property
69+
propAffineRoundtrip =
70+
forAll genShape $ \shape ->
71+
let rankSpace = rowMajor shape
72+
in conjoin
73+
[ rankOf rankSpace (pointOf rankSpace rank) === rank
74+
| rank <- ranks rankSpace
75+
]
76+
77+
propRanksEnumerateSpace :: Property
78+
propRanksEnumerateSpace =
79+
forAll genShape $ \shape ->
80+
let rankSpace = rowMajor shape
81+
rankList = ranks rankSpace
82+
in conjoin
83+
[ length rankList === spaceExtent rankSpace,
84+
sort rankList === [0 .. spaceExtent rankSpace - 1]
85+
]
86+
87+
propAffineSliceIncluded :: Property
88+
propAffineSliceIncluded =
89+
forAll genAffineSlice $ \(shape, dim, begin, end, step) ->
90+
let parent = rowMajor shape
91+
in case select parent dim begin end step of
92+
Nothing -> counterexample "generated invalid affine slice" False
93+
Just child ->
94+
counterexample (show child) $
95+
all (`elem` ranks parent) (ranks child)
96+
97+
propChildrenIncluded :: Property
98+
propChildrenIncluded =
99+
forAll genShape $ \shape ->
100+
let parent = rootTile shape
101+
structuralChildren = map tile (childNodes BlockPartitioning parent)
102+
communicationChildren = children BlockPartitioning parent
103+
in conjoin
104+
[ counterexample "structural child outside parent" $
105+
all (ranksIncludedIn parent) structuralChildren,
106+
counterexample "communication child outside parent" $
107+
all (ranksIncludedIn parent) communicationChildren
108+
]
109+
110+
propFaultFreeScheduleSpansTile :: Property
111+
propFaultFreeScheduleSpansTile =
112+
forAll genShape $ \shape ->
113+
let tile = rootTile shape
114+
memberRanks = tileRanks tile
115+
members = memberRanks
116+
schedule = buildScheduleFrom BFSScheduler BlockPartitioning members tile
117+
senders = map from schedule
118+
receivers = map to schedule
119+
in conjoin
120+
[ length schedule === length memberRanks - 1,
121+
sort receivers === sort (filter (/= root tile) memberRanks),
122+
unique receivers === True,
123+
all (`elem` memberRanks) senders === True,
124+
all (`elem` memberRanks) receivers === True,
125+
(root tile `notElem` receivers) === True
126+
]
127+
128+
propOccludedScheduleCoversLiveMembers :: Property
129+
propOccludedScheduleCoversLiveMembers =
130+
forAll genLiveRanks $ \(shape, liveRanks) ->
131+
let tile = rootTile shape
132+
memberRanks = tileRanks tile
133+
members = memberRanks
134+
live rank = rank `elem` liveRanks
135+
occ = Occlusion (not . live)
136+
in case buildOccludedScheduleFrom BFSScheduler occ BlockPartitioning members tile of
137+
Nothing ->
138+
counterexample "non-empty live set produced no schedule" $
139+
null liveRanks
140+
Just RoutedSchedule {ingress = entry, routedSteps = steps} ->
141+
let senders = map from steps
142+
receivers = map to steps
143+
in conjoin
144+
[ counterexample "ingress is not live" $
145+
live entry === True,
146+
counterexample "sender outside live set" $
147+
all live senders === True,
148+
counterexample "receiver outside live set" $
149+
all live receivers === True,
150+
counterexample "live receiver coverage mismatch" $
151+
sort receivers === sort (filter (/= entry) liveRanks),
152+
counterexample "duplicate live receiver" $
153+
unique receivers === True,
154+
counterexample "receiver outside original tile" $
155+
all (`elem` memberRanks) receivers === True
156+
]
157+
158+
genShape :: Gen Shape
159+
genShape = do
160+
rank <- chooseInt (1, 4)
161+
vectorOf rank (chooseInt (1, 4))
162+
163+
genAffineSlice :: Gen (Shape, Int, Int, Int, Int)
164+
genAffineSlice = do
165+
shape <- genShape
166+
dim <- chooseInt (0, length shape - 1)
167+
let extent = shape !! dim
168+
begin <- chooseInt (0, extent - 1)
169+
end <- chooseInt (begin + 1, extent)
170+
step <- chooseInt (1, extent)
171+
pure (shape, dim, begin, end, step)
172+
173+
genLiveRanks :: Gen (Shape, [Int])
174+
genLiveRanks = do
175+
shape <- genShape
176+
let rankList = ranks (rowMajor shape)
177+
keep <- vectorOf (length rankList) arbitrary
178+
let liveRanks = [rank | (rank, True) <- zip rankList keep]
179+
pure (shape, liveRanks)
180+
54181
inclusionTests :: TestTree
55182
inclusionTests =
56183
testGroup
@@ -410,4 +537,21 @@ assertRanksIncludedIn parent child =
410537

411538
expectTile :: String -> Maybe Tile -> Tile
412539
expectTile _ (Just tile) = tile
413-
expectTile label Nothing = error ("expected " ++ label)
540+
expectTile description Nothing = error ("expected " ++ description)
541+
542+
ranksIncludedIn :: Tile -> Tile -> Bool
543+
ranksIncludedIn parent child =
544+
all (`elem` tileRanks parent) (tileRanks child)
545+
546+
unique :: (Ord a) => [a] -> Bool
547+
unique xs =
548+
sorted == dedupe sorted
549+
where
550+
sorted = sort xs
551+
552+
dedupe :: (Eq a) => [a] -> [a]
553+
dedupe [] = []
554+
dedupe [x] = [x]
555+
dedupe (x : y : rest)
556+
| x == y = dedupe (y : rest)
557+
| otherwise = x : dedupe (y : rest)

tile.cabal

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ tested-with: GHC == 9.14.1
1111
-- copyright:
1212
category: System
1313
build-type: Simple
14-
extra-doc-files: CHANGELOG.md
14+
extra-doc-files:
15+
CHANGELOG.md
16+
article/routing-foundations.md
17+
article/routing-theorems.md
1518
-- extra-source-files:
1619

1720
source-repository head
@@ -64,4 +67,5 @@ test-suite tile-test
6467
base ^>=4.22.0.0,
6568
tile,
6669
tasty >=1.4,
67-
tasty-hunit >=0.10
70+
tasty-hunit >=0.10,
71+
tasty-quickcheck >=0.10

0 commit comments

Comments
 (0)