15.6 KiB, 1 run
created by r2848102244:75, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Stage five: the cross-file cycle rule, five candidates, predictions first. |
| 2 | //! |
| 3 | //! Every case here is a cycle in the anchor graph that runs between files, and |
| 4 | //! every one of them is rendered under all five rules of [`CycleRule`]. The |
| 5 | //! predictions in this file were written into |
| 6 | //! `oracle/PREDICTIONS_cycle_rule.md` and into these strings *before* the engine |
| 7 | //! could run any of them, and where the engine disagrees the disagreement is |
| 8 | //! reported rather than the prediction quietly corrected. |
| 9 | //! |
| 10 | //! Every case is checked under every delivery order, and every case checks |
| 11 | //! conservation over the whole repository: no byte in two files, no live byte |
| 12 | //! nowhere. A rule that broke either would be out of the running whatever it |
| 13 | //! read like. |
| 14 | |
| 15 | use crate::cases::ALPHA; |
| 16 | use crate::id::{ |
| 17 | after, |
| 18 | ContentId, |
| 19 | ContentRange, |
| 20 | }; |
| 21 | use crate::mrep::MRep; |
| 22 | use crate::random::Rng; |
| 23 | use crate::repo::{ |
| 24 | shared, |
| 25 | CycleRule, |
| 26 | FileId, |
| 27 | Identity, |
| 28 | MFlags, |
| 29 | MOp, |
| 30 | Repo, |
| 31 | RepoRender, |
| 32 | Shared, |
| 33 | }; |
| 34 | |
| 35 | use oxedyne_fe2o3_core::prelude::*; |
| 36 | |
| 37 | /// Every rule under trial, in the order the report tables them. |
| 38 | pub const RULES: [CycleRule; 5] = [ |
| 39 | CycleRule::Demote, |
| 40 | CycleRule::ConfineVictim, |
| 41 | CycleRule::ConfineCycle, |
| 42 | CycleRule::LowestEdge, |
| 43 | CycleRule::WholeCycle, |
| 44 | ]; |
| 45 | |
| 46 | /// The result of running one case under one rule. |
| 47 | #[derive(Clone, Debug)] |
| 48 | pub struct CycOut { |
| 49 | /// Case name. |
| 50 | pub name: &'static str, |
| 51 | /// Which rule. |
| 52 | pub rule: CycleRule, |
| 53 | /// What was predicted before the case was run. |
| 54 | pub expect: String, |
| 55 | /// What happened. |
| 56 | pub got: String, |
| 57 | /// Delivery orders checked. |
| 58 | pub orders: usize, |
| 59 | /// Bytes rendered in more than one place. |
| 60 | pub duplicated: usize, |
| 61 | /// Live bytes rendered nowhere at all. |
| 62 | pub lost: usize, |
| 63 | /// Live files that render nothing. |
| 64 | pub emptied: usize, |
| 65 | /// Everything the renderer noticed. |
| 66 | pub flags: MFlags, |
| 67 | } |
| 68 | |
| 69 | impl CycOut { |
| 70 | /// Whether the case met its stated prediction. |
| 71 | pub fn met(&self) -> bool { |
| 72 | self.expect == self.got |
| 73 | } |
| 74 | |
| 75 | /// Whether conservation held. |
| 76 | pub fn conserved(&self) -> bool { |
| 77 | self.duplicated == 0 && self.lost == 0 |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | /// A one-line rendering of the flags a cycle rule can raise. |
| 82 | pub fn summary(f: &MFlags) -> String { |
| 83 | fmt!( |
| 84 | "demoted={:?} crossed={:?} confined={:?} won={:?} torn={:?} dropped={:?}", |
| 85 | f.demoted.iter().map(|(o, s)| fmt!("{}+{}", o, s)).collect::<Vec<_>>(), |
| 86 | f.crossed.iter() |
| 87 | .map(|(o, s, from, to)| fmt!("{}+{}:{}->{}", o, s, from, to)) |
| 88 | .collect::<Vec<_>>(), |
| 89 | f.confined.iter() |
| 90 | .map(|(o, home, denied)| fmt!("{}:{}!{}", o, home, denied)) |
| 91 | .collect::<Vec<_>>(), |
| 92 | f.won.iter().map(|i| i.to_string()).collect::<Vec<_>>(), |
| 93 | f.torn.iter().map(|i| i.to_string()).collect::<Vec<_>>(), |
| 94 | f.dropped.iter().map(|(o, s)| fmt!("{}+{}", o, s)).collect::<Vec<_>>(), |
| 95 | ) |
| 96 | } |
| 97 | |
| 98 | /// Applies an operation set in every delivery order and checks that every one |
| 99 | /// renders the same bytes and raises the same flags. |
| 100 | pub fn converge(rule: CycleRule, ops: &[MOp], meta: &Shared) |
| 101 | -> Outcome<(RepoRender, usize)> |
| 102 | { |
| 103 | let n = ops.len(); |
| 104 | let mut orders: Vec<Vec<usize>> = Vec::new(); |
| 105 | if n <= 8 { |
| 106 | let mut idx: Vec<usize> = (0..n).collect(); |
| 107 | permute(&mut idx, 0, &mut orders); |
| 108 | } else { |
| 109 | for k in 0..n { |
| 110 | orders.push((0..n).map(|i| (i + k) % n).collect()); |
| 111 | } |
| 112 | orders.push((0..n).rev().collect()); |
| 113 | let mut rng = Rng::new(0x0_5EED_u64.wrapping_add(n as u64)); |
| 114 | for _ in 0..1000 { |
| 115 | let mut idx: Vec<usize> = (0..n).collect(); |
| 116 | rng.shuffle(&mut idx); |
| 117 | orders.push(idx); |
| 118 | } |
| 119 | } |
| 120 | let mut first: Option<RepoRender> = None; |
| 121 | for ord in &orders { |
| 122 | let mut repo = Repo::with_rule(Identity::Derived, rule, meta.clone()); |
| 123 | for i in ord { |
| 124 | repo.apply(ops[*i].clone()); |
| 125 | } |
| 126 | let r = res!(repo.render()); |
| 127 | match &first { |
| 128 | None => first = Some(r), |
| 129 | Some(f) => { |
| 130 | if f.listing() != r.listing() { |
| 131 | return Err(err!( |
| 132 | "{}: delivery order changed the render:\n {}\n {}", |
| 133 | rule.name(), f.listing(), r.listing(); Mismatch, Data)); |
| 134 | } |
| 135 | if summary(&f.flags) != summary(&r.flags) { |
| 136 | return Err(err!( |
| 137 | "{}: delivery order changed the flags:\n {}\n {}", |
| 138 | rule.name(), summary(&f.flags), summary(&r.flags); |
| 139 | Mismatch, Data)); |
| 140 | } |
| 141 | }, |
| 142 | } |
| 143 | } |
| 144 | match first { |
| 145 | Some(f) => Ok((f, orders.len())), |
| 146 | None => Err(err!("No delivery order was tried."; Bug)), |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | /// Generates every permutation of `idx`. |
| 151 | fn permute(idx: &mut Vec<usize>, k: usize, out: &mut Vec<Vec<usize>>) { |
| 152 | if k == idx.len() { |
| 153 | out.push(idx.clone()); |
| 154 | return; |
| 155 | } |
| 156 | for i in k..idx.len() { |
| 157 | idx.swap(k, i); |
| 158 | permute(idx, k + 1, out); |
| 159 | idx.swap(k, i); |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | /// Creates the named files on a replica that everybody has seen, then hands out |
| 164 | /// `n` further replicas sharing one metadata table. |
| 165 | fn stage(rule: CycleRule, files: &[(&[u8], &[u8])], n: u32) |
| 166 | -> Outcome<(Vec<MRep>, Vec<MOp>, Vec<FileId>, Shared)> |
| 167 | { |
| 168 | let meta = shared(); |
| 169 | let mut origin = MRep::with_rule(0, Identity::Derived, rule, meta.clone()); |
| 170 | let mut ops: Vec<MOp> = Vec::new(); |
| 171 | let mut ids: Vec<FileId> = Vec::new(); |
| 172 | for (path, text) in files { |
| 173 | let (op, id) = res!(origin.create(path)); |
| 174 | ops.push(op); |
| 175 | ids.push(id); |
| 176 | if !text.is_empty() { |
| 177 | ops.push(res!(origin.insert(id, 0, text))); |
| 178 | } |
| 179 | } |
| 180 | let mut reps: Vec<MRep> = Vec::new(); |
| 181 | for i in 1..=n { |
| 182 | let mut r = MRep::with_rule(i, Identity::Derived, rule, meta.clone()); |
| 183 | for op in &ops { |
| 184 | r.recv(op.clone()); |
| 185 | } |
| 186 | reps.push(r); |
| 187 | } |
| 188 | Ok((reps, ops, ids, meta)) |
| 189 | } |
| 190 | |
| 191 | /// Builds a case result from an operation set and a prediction. |
| 192 | fn run( |
| 193 | name: &'static str, |
| 194 | expect: &str, |
| 195 | rule: CycleRule, |
| 196 | ops: Vec<MOp>, |
| 197 | meta: &Shared, |
| 198 | ) |
| 199 | -> Outcome<CycOut> |
| 200 | { |
| 201 | let (r, orders) = res!(converge(rule, &ops, meta)); |
| 202 | let emptied = r.live().iter().filter(|f| f.bytes.is_empty()).count(); |
| 203 | Ok(CycOut { |
| 204 | name, |
| 205 | rule, |
| 206 | expect: expect.to_string(), |
| 207 | got: r.listing(), |
| 208 | orders, |
| 209 | duplicated: r.flags.duplicated, |
| 210 | lost: r.stats.lost, |
| 211 | emptied, |
| 212 | flags: r.flags, |
| 213 | }) |
| 214 | } |
| 215 | |
| 216 | /// Every cycle case, under one rule. |
| 217 | pub fn all(rule: CycleRule) -> Outcome<Vec<CycOut>> { |
| 218 | Ok(vec![ |
| 219 | res!(c1_two_file_cycle(rule)), |
| 220 | res!(c2_three_file_cycle(rule)), |
| 221 | res!(c3_four_file_cycle(rule)), |
| 222 | res!(c4_mixed_cycle(rule)), |
| 223 | res!(c5_cycle_with_inner_edit(rule)), |
| 224 | res!(c6_supersession_cycle(rule)), |
| 225 | res!(c7_in_file_cycle(rule)), |
| 226 | res!(c8_self_anchoring_move(rule)), |
| 227 | ]) |
| 228 | } |
| 229 | |
| 230 | /// C1. The shipped case: two agents reorganising two files at once, each moving |
| 231 | /// one file's whole contents into the other. |
| 232 | pub fn c1_two_file_cycle(rule: CycleRule) -> Outcome<CycOut> { |
| 233 | let (mut reps, mut ops, ids, meta) = res!(stage( |
| 234 | rule, &[(b"a.txt", b"abc\n"), (b"b.txt", b"xyz\n")], 2)); |
| 235 | let (a, b) = (ids[0], ids[1]); |
| 236 | let (r1, r2) = reps.split_at_mut(1); |
| 237 | ops.push(res!(r1[0].move_across(a, 0, 4, b, 1))); |
| 238 | ops.push(res!(r2[0].move_across(b, 0, 4, a, 1))); |
| 239 | let expect = match rule { |
| 240 | // The demoted move lands in the other file and the winner follows it |
| 241 | // there: a.txt is emptied into b.txt. |
| 242 | CycleRule::Demote => "a.txt=\"\" b.txt=\"axyz\\nbc\\n\"", |
| 243 | // Confining only the demotion victim does not stop a file emptying: M1 |
| 244 | // is confined, M2's anchor is then home in a.txt, and M2 completes into |
| 245 | // a.txt carrying the whole of b.txt with it. The collapse is mirrored, |
| 246 | // not prevented. |
| 247 | CycleRule::ConfineVictim => "a.txt=\"axyz\\nbc\\n\" b.txt=\"\"", |
| 248 | // Every cross-file member is confined, so each file keeps its own block. |
| 249 | CycleRule::ConfineCycle => "a.txt=\"abc\\n\" b.txt=\"xyz\\n\"", |
| 250 | // The lower move loses and returns home; the higher one completes. |
| 251 | CycleRule::LowestEdge => "a.txt=\"axyz\\nbc\\n\" b.txt=\"\"", |
| 252 | // The same, by a different route: B and C coincide on a two-cycle. |
| 253 | CycleRule::WholeCycle => "a.txt=\"axyz\\nbc\\n\" b.txt=\"\"", |
| 254 | }; |
| 255 | run("C1 two-file cycle", expect, rule, ops, &meta) |
| 256 | } |
| 257 | |
| 258 | /// C2. The cycle at length three, every member crossing a boundary. |
| 259 | pub fn c2_three_file_cycle(rule: CycleRule) -> Outcome<CycOut> { |
| 260 | let (mut reps, mut ops, ids, meta) = res!(stage( |
| 261 | rule, |
| 262 | &[(b"a.txt", b"abc\n"), (b"b.txt", b"xyz\n"), (b"c.txt", b"123\n")], |
| 263 | 3, |
| 264 | )); |
| 265 | let (a, b, c) = (ids[0], ids[1], ids[2]); |
| 266 | ops.push(res!(reps[0].move_across(a, 0, 4, b, 1))); |
| 267 | ops.push(res!(reps[1].move_across(b, 0, 4, c, 1))); |
| 268 | ops.push(res!(reps[2].move_across(c, 0, 4, a, 1))); |
| 269 | let expect = match rule { |
| 270 | CycleRule::Demote => |
| 271 | "a.txt=\"\" b.txt=\"a1xyz\\n23\\nbc\\n\" c.txt=\"\"", |
| 272 | // Breaking one edge leaves an acyclic chain, and the two survivors |
| 273 | // cascade into one file just as the status quo does. |
| 274 | CycleRule::ConfineVictim => |
| 275 | "a.txt=\"a1xyz\\n23\\nbc\\n\" b.txt=\"\" c.txt=\"\"", |
| 276 | CycleRule::ConfineCycle => |
| 277 | "a.txt=\"abc\\n\" b.txt=\"xyz\\n\" c.txt=\"123\\n\"", |
| 278 | CycleRule::LowestEdge => |
| 279 | "a.txt=\"a1xyz\\n23\\nbc\\n\" b.txt=\"\" c.txt=\"\"", |
| 280 | // One move wins wholly and the other two go home: this is where B and C |
| 281 | // come apart. |
| 282 | CycleRule::WholeCycle => |
| 283 | "a.txt=\"a123\\nbc\\n\" b.txt=\"xyz\\n\" c.txt=\"\"", |
| 284 | }; |
| 285 | run("C2 three-file cycle", expect, rule, ops, &meta) |
| 286 | } |
| 287 | |
| 288 | /// C3. The cycle at length four. |
| 289 | pub fn c3_four_file_cycle(rule: CycleRule) -> Outcome<CycOut> { |
| 290 | let (mut reps, mut ops, ids, meta) = res!(stage( |
| 291 | rule, |
| 292 | &[ |
| 293 | (b"a.txt", b"abc\n"), |
| 294 | (b"b.txt", b"xyz\n"), |
| 295 | (b"c.txt", b"123\n"), |
| 296 | (b"d.txt", b"pqr\n"), |
| 297 | ], |
| 298 | 4, |
| 299 | )); |
| 300 | let (a, b, c, d) = (ids[0], ids[1], ids[2], ids[3]); |
| 301 | ops.push(res!(reps[0].move_across(a, 0, 4, b, 1))); |
| 302 | ops.push(res!(reps[1].move_across(b, 0, 4, c, 1))); |
| 303 | ops.push(res!(reps[2].move_across(c, 0, 4, d, 1))); |
| 304 | ops.push(res!(reps[3].move_across(d, 0, 4, a, 1))); |
| 305 | let expect = match rule { |
| 306 | CycleRule::Demote => |
| 307 | "a.txt=\"\" b.txt=\"ap1xyz\\n23\\nqr\\nbc\\n\" c.txt=\"\" d.txt=\"\"", |
| 308 | CycleRule::ConfineVictim => |
| 309 | "a.txt=\"ap1xyz\\n23\\nqr\\nbc\\n\" b.txt=\"\" c.txt=\"\" d.txt=\"\"", |
| 310 | CycleRule::ConfineCycle => |
| 311 | "a.txt=\"abc\\n\" b.txt=\"xyz\\n\" c.txt=\"123\\n\" d.txt=\"pqr\\n\"", |
| 312 | CycleRule::LowestEdge => |
| 313 | "a.txt=\"ap1xyz\\n23\\nqr\\nbc\\n\" b.txt=\"\" c.txt=\"\" d.txt=\"\"", |
| 314 | CycleRule::WholeCycle => |
| 315 | "a.txt=\"apqr\\nbc\\n\" b.txt=\"xyz\\n\" c.txt=\"123\\n\" d.txt=\"\"", |
| 316 | }; |
| 317 | run("C3 four-file cycle", expect, rule, ops, &meta) |
| 318 | } |
| 319 | |
| 320 | /// C4. A cycle of three whose middle member never leaves its file. |
| 321 | /// |
| 322 | /// A two-cycle cannot be mixed -- each member lands inside the other's source, |
| 323 | /// so either both cross a boundary or neither does -- and three is the shortest |
| 324 | /// cycle that can hold one in-file move. It is the case that prices whole-cycle |
| 325 | /// arbitration, which voids that innocent move along with the others. |
| 326 | pub fn c4_mixed_cycle(rule: CycleRule) -> Outcome<CycOut> { |
| 327 | let (mut reps, mut ops, ids, meta) = res!(stage( |
| 328 | rule, &[(b"a.txt", b"abc\n"), (b"b.txt", b"wxyz")], 3)); |
| 329 | let (a, b) = (ids[0], ids[1]); |
| 330 | // M1 crosses: the whole of a.txt into b.txt, after 'w'. |
| 331 | ops.push(res!(reps[0].move_across(a, 0, 4, b, 1))); |
| 332 | // M2 stays home: "wx" to after 'y', within b.txt. |
| 333 | ops.push(res!(reps[1].move_across(b, 0, 2, b, 3))); |
| 334 | // M3 crosses back: "yz" out of b.txt into a.txt, after 'a'. |
| 335 | ops.push(res!(reps[2].move_across(b, 2, 2, a, 1))); |
| 336 | let expect = match rule { |
| 337 | // The predictions note said "everything, 8 bytes", which is what happened; |
| 338 | // the string first written here said `awywxzbc\n`, which is nine, and the |
| 339 | // slip was in the hand-trace of where b.txt's own 'w' ends up rather than |
| 340 | // in the design. Corrected to what ran, and recorded rather than hidden. |
| 341 | CycleRule::Demote => "a.txt=\"\" b.txt=\"aywxzbc\\n\"", |
| 342 | CycleRule::ConfineVictim => "a.txt=\"aywxzbc\\n\" b.txt=\"\"", |
| 343 | // The only rule that lets the in-file move complete. |
| 344 | CycleRule::ConfineCycle => "a.txt=\"abc\\n\" b.txt=\"ywxz\"", |
| 345 | CycleRule::LowestEdge => "a.txt=\"aywxzbc\\n\" b.txt=\"\"", |
| 346 | // M2 is voided for being in the cycle, though it never crossed anything. |
| 347 | CycleRule::WholeCycle => "a.txt=\"ayzbc\\n\" b.txt=\"wx\"", |
| 348 | }; |
| 349 | run("C4 mixed cycle", expect, rule, ops, &meta) |
| 350 | } |
| 351 | |
| 352 | /// C5. C1, with a third replica editing inside a cycling block. |
| 353 | /// |
| 354 | /// The question is whether the edit survives and where it lands. It should |
| 355 | /// stay with its block under every rule, since its anchor names content and |
| 356 | /// nothing else. |
| 357 | pub fn c5_cycle_with_inner_edit(rule: CycleRule) -> Outcome<CycOut> { |
| 358 | let (mut reps, mut ops, ids, meta) = res!(stage( |
| 359 | rule, &[(b"a.txt", b"abc\n"), (b"b.txt", b"xyz\n")], 3)); |
| 360 | let (a, b) = (ids[0], ids[1]); |
| 361 | ops.push(res!(reps[0].move_across(a, 0, 4, b, 1))); |
| 362 | ops.push(res!(reps[1].move_across(b, 0, 4, a, 1))); |
| 363 | // Concurrently with both, an exclamation mark between 'b' and 'c'. |
| 364 | ops.push(res!(reps[2].insert(a, 2, b"!"))); |
| 365 | let expect = match rule { |
| 366 | CycleRule::Demote => "a.txt=\"\" b.txt=\"axyz\\nb!c\\n\"", |
| 367 | CycleRule::ConfineVictim => "a.txt=\"axyz\\nb!c\\n\" b.txt=\"\"", |
| 368 | CycleRule::ConfineCycle => "a.txt=\"ab!c\\n\" b.txt=\"xyz\\n\"", |
| 369 | CycleRule::LowestEdge => "a.txt=\"axyz\\nb!c\\n\" b.txt=\"\"", |
| 370 | CycleRule::WholeCycle => "a.txt=\"axyz\\nb!c\\n\" b.txt=\"\"", |
| 371 | }; |
| 372 | run("C5 cycle with an inner edit", expect, rule, ops, &meta) |
| 373 | } |
| 374 | |
| 375 | /// C6. An author re-moving their own block, having seen the move that raced it. |
| 376 | /// |
| 377 | /// The first move is superseded and owns nothing, so it leaves the graph; the |
| 378 | /// cycle is between the concurrent move and the re-move, and the re-move is not |
| 379 | /// concurrent with it. A rule that voids the re-move would be discarding an |
| 380 | /// operation its author wrote *knowing* what it was written against, which is |
| 381 | /// the distinction the torn flag had to learn. |
| 382 | pub fn c6_supersession_cycle(rule: CycleRule) -> Outcome<CycOut> { |
| 383 | let (mut reps, mut ops, ids, meta) = res!(stage( |
| 384 | rule, &[(b"a.txt", b"abc\n"), (b"b.txt", b"xyz\n")], 2)); |
| 385 | let (a, b) = (ids[0], ids[1]); |
| 386 | // The splices that wrote the two files' contents. |
| 387 | let (sa, sb) = (ops[1].id(), ops[3].id()); |
| 388 | let (r1, r2) = reps.split_at_mut(1); |
| 389 | let m1 = res!(r1[0].move_across(a, 0, 4, b, 1)); |
| 390 | ops.push(m1); |
| 391 | let m2 = res!(r2[0].move_across(b, 0, 4, a, 1)); |
| 392 | ops.push(m2.clone()); |
| 393 | // Replica 1 receives the move that raced it, and moves its own block again, |
| 394 | // to the end of what b.txt originally held. |
| 395 | r1[0].recv(m2); |
| 396 | let m3 = MOp::Move { |
| 397 | id: r1[0].next_id(), |
| 398 | file: None, |
| 399 | src: vec![res!(ContentRange::new(sa, 0, 4))], |
| 400 | left: after(ContentId::new(sb, 3)), |
| 401 | right: None, |
| 402 | }; |
| 403 | ops.push(res!(r1[0].adopt(m3))); |
| 404 | let expect = match rule { |
| 405 | // The status quo demotes the concurrent move, the re-move then follows |
| 406 | // it, and a block whose author twice said "into b.txt" renders in a.txt. |
| 407 | CycleRule::Demote => "a.txt=\"xyz\\nabc\\n\" b.txt=\"\"", |
| 408 | // Every candidate honours the informed re-move. |
| 409 | _ => "a.txt=\"\" b.txt=\"xyz\\nabc\\n\"", |
| 410 | }; |
| 411 | run("C6 supersession cycle", expect, rule, ops, &meta) |
| 412 | } |
| 413 | |
| 414 | /// C7. An in-file cycle, which no rule may touch. |
| 415 | /// |
| 416 | /// Two moves whose destinations sit inside each other's sources, both inside one |
| 417 | /// file: design note section 5.4's own case, whose outcome stage one settled. |
| 418 | pub fn c7_in_file_cycle(rule: CycleRule) -> Outcome<CycOut> { |
| 419 | let (mut reps, mut ops, ids, meta) = res!(stage(rule, &[(b"f", ALPHA)], 2)); |
| 420 | let f = ids[0]; |
| 421 | let (r1, r2) = reps.split_at_mut(1); |
| 422 | ops.push(res!(r1[0].move_within(f, 0, 5, 12))); |
| 423 | ops.push(res!(r2[0].move_within(f, 10, 5, 2))); |
| 424 | run( |
| 425 | "C7 in-file cycle", |
| 426 | "f=\"5678901ABCDE234FGHIJ\"", |
| 427 | rule, |
| 428 | ops, |
| 429 | &meta, |
| 430 | ) |
| 431 | } |
| 432 | |
| 433 | /// C8. A move whose destination sits inside its own source: a cycle of length |
| 434 | /// one, and in one file, so again no rule may touch it. |
| 435 | /// The prediction is not a string but an invariance: whatever the status quo |
| 436 | /// renders, every candidate renders too, since none of them may reach a cycle |
| 437 | /// that stays inside one file. |
| 438 | pub fn c8_self_anchoring_move(rule: CycleRule) -> Outcome<CycOut> { |
| 439 | let (mut reps, mut ops, ids, meta) = res!(stage(rule, &[(b"f", ALPHA)], 1)); |
| 440 | let f = ids[0]; |
| 441 | ops.push(res!(reps[0].move_within(f, 2, 4, 4))); |
| 442 | // The status quo's own answer, computed here rather than quoted, because |
| 443 | // what is predicted is that no rule changes it. |
| 444 | let (base, _) = res!(converge(CycleRule::Demote, &ops, &meta)); |
| 445 | let expect = base.listing(); |
| 446 | run("C8 self-anchoring move", &expect, rule, ops, &meta) |
| 447 | } |