Oregami
Repositories/oxedyne/ore

oxedyne/ore/oracle/src/cyc.rs

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
15use crate::cases::ALPHA;
16use crate::id::{
17 after,
18 ContentId,
19 ContentRange,
20};
21use crate::mrep::MRep;
22use crate::random::Rng;
23use crate::repo::{
24 shared,
25 CycleRule,
26 FileId,
27 Identity,
28 MFlags,
29 MOp,
30 Repo,
31 RepoRender,
32 Shared,
33};
34
35use oxedyne_fe2o3_core::prelude::*;
36
37/// Every rule under trial, in the order the report tables them.
38pub 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)]
48pub 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
69impl 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.
82pub 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.
100pub 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`.
151fn 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.
165fn 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.
192fn 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.
217pub 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.
232pub 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.
259pub 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.
289pub 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.
326pub 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.
357pub 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.
382pub 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.
418pub 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.
438pub 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}