Skip to main content

blockworx/doc_ng/
entity.rs

1//! The [`Entity`] trait and the `entity!` macro that implements it: one
2//! field list per kind (`block_model.rs`) expands to the struct, its
3//! total init, its update vocabulary, and the trait impl — the mirror
4//! guard is generation, not transcription.
5
6use crate::doc_ng::{register::Applied, write_order::WriteOrder};
7
8pub trait Entity {
9    type Update;
10    type Init;
11    type Id: Eq + std::hash::Hash + Copy;
12    /// A create is a write of every register at the create op's own
13    /// order — `BOTTOM` stays reserved for the pre-creation zero.
14    fn from_init(init: &Self::Init, order: WriteOrder) -> Self;
15    fn apply(&mut self, update: &Self::Update, order: WriteOrder) -> Applied;
16    /// The chronological sort key: the max write order over every
17    /// register, namespaces included; creation constants carry no order
18    /// and are excluded. Generated from the same field list as the
19    /// struct, so a register cannot go missing from the fold; the
20    /// value-walk tests prove the template.
21    fn max_order(&self) -> WriteOrder;
22    /// The undo journal's baseline reader: the update that restores what
23    /// `update` would displace. Ops carry only the new value, so this is
24    /// the only source of an `Update`'s inverse. No ordering to get wrong
25    /// — the fold produces a new document rather than mutating this one,
26    /// so the pre-image outlives the commit applied to it.
27    fn invert(&self, update: &Self::Update) -> Self::Update;
28}
29
30/// One entity kind from one field list: the struct, its total `Init`
31/// struct, its `Update` enum, and the [`Entity`] impl. Field classes:
32/// `registers` (LWW leaves), `namespaces` (nested vocabularies addressed
33/// by descent), `constants` (creation-time values — no update variant,
34/// excluded from `max_order`). Serde variant names are wire tags:
35/// written explicitly, never derived from field names (`FlipLR` is not
36/// `FlipLr`), never renamed or repurposed — deprecate and add instead.
37macro_rules! entity {
38    (
39        $(#[$meta:meta])*
40        pub struct $entity:ident(init $init:ident, update $update:ident, id $id:ty) {
41            registers { $( $(#[$rmeta:meta])* $rvar:ident => $rfield:ident : $rty:ty ),* $(,)? }
42            namespaces { $( $nvar:ident => $nfield:ident : $nty:ty ),* $(,)? }
43            constants { $( $(#[$cmeta:meta])* $cfield:ident : $cty:ty ),* $(,)? }
44        }
45    ) => {
46        $(#[$meta])*
47        #[derive(Clone, serde::Serialize)]
48        pub struct $entity {
49            $( $(#[$rmeta])* pub $rfield: $crate::doc_ng::register::Register<$rty>, )*
50            $( pub $nfield: $nty, )*
51            $( $(#[$cmeta])* pub $cfield: $cty, )*
52        }
53
54        #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
55        pub struct $init {
56            $( $(#[$rmeta])* pub $rfield: $rty, )*
57            $( pub $nfield: <$nty as $crate::doc_ng::entity::Entity>::Init, )*
58            $( $(#[$cmeta])* pub $cfield: $cty, )*
59        }
60
61        #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
62        pub enum $update {
63            $( $rvar($rty), )*
64            $( $nvar(<$nty as $crate::doc_ng::entity::Entity>::Update), )*
65        }
66
67        impl $crate::doc_ng::entity::Entity for $entity {
68            type Init = $init;
69            type Update = $update;
70            type Id = $id;
71            fn from_init(
72                init: &Self::Init,
73                order: $crate::doc_ng::write_order::WriteOrder,
74            ) -> Self {
75                Self {
76                    $( $rfield: $crate::doc_ng::register::Register::new(&init.$rfield, order), )*
77                    $(
78                        $nfield: <$nty as $crate::doc_ng::entity::Entity>::from_init(
79                            &init.$nfield,
80                            order,
81                        ),
82                    )*
83                    $( $cfield: init.$cfield, )*
84                }
85            }
86            fn apply(
87                &mut self,
88                update: &Self::Update,
89                order: $crate::doc_ng::write_order::WriteOrder,
90            ) -> $crate::doc_ng::register::Applied {
91                match update {
92                    $( $update::$rvar(value) => self.$rfield.apply(value, order), )*
93                    $( $update::$nvar(value) => self.$nfield.apply(value, order), )*
94                }
95            }
96            fn max_order(&self) -> $crate::doc_ng::write_order::WriteOrder {
97                [
98                    $( self.$rfield.order(), )*
99                    $( self.$nfield.max_order(), )*
100                ]
101                .into_iter()
102                .fold(
103                    $crate::doc_ng::write_order::WriteOrder::BOTTOM,
104                    $crate::doc_ng::write_order::WriteOrder::max,
105                )
106            }
107            fn invert(&self, update: &Self::Update) -> Self::Update {
108                match update {
109                    $( $update::$rvar(_) => $update::$rvar(self.$rfield.as_ref().clone()), )*
110                    $( $update::$nvar(value) => $update::$nvar(self.$nfield.invert(value)), )*
111                }
112            }
113        }
114    };
115}
116pub(crate) use entity;
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::doc_ng::fixtures::{block_id, pin_id, route_id};
122    use crate::doc_ng::{
123        block_model::{
124            Block, BlockInit, BlockUpdate, Comment, CommentInit, CommentUpdate, Icon, Image,
125            ImageInit, ImageUpdate, LabelInit, LabelUpdate, Live, Pin, PinInit, PinUpdate, Route,
126            RouteInit, RouteLabel, RouteLabelInit, RouteLabelUpdate, RouteUpdate, Text, TextInit,
127            TextUpdate,
128        },
129        geometry::{
130            FracVal, GridPoint, GridRect, GridSize, ScreenPoint, ScreenRect, ScreenSize, Waypoint,
131        },
132        hash::{AssetHash, AssetKind, Hasher},
133        rev::Rev,
134        values::{LabelSide, PinDir, Role},
135        write_order::Seq,
136    };
137
138    fn at(rev: u64) -> WriteOrder {
139        WriteOrder::new(Rev::new(rev), Seq::new(0))
140    }
141
142    fn rect(x: i32, y: i32, w: u32, h: u32) -> GridRect {
143        GridRect {
144            top_left: GridPoint { x, y },
145            size: GridSize { w, h },
146        }
147    }
148
149    fn screen_rect(x: f32, y: f32) -> ScreenRect {
150        ScreenRect {
151            top_left: ScreenPoint {
152                x: FracVal::from(x),
153                y: FracVal::from(y),
154            },
155            size: ScreenSize {
156                w: FracVal::from(8.0),
157                h: FracVal::from(8.0),
158            },
159        }
160    }
161
162    fn asset(bytes: &[u8]) -> AssetHash {
163        let mut hasher = Hasher::<AssetKind>::new();
164        hasher.update(bytes);
165        hasher.finalize()
166    }
167
168    /// Every field deliberately non-default, so a `from_init` that drops an
169    /// init value for `Default::default()` cannot pass the value walk.
170    fn label_init(name: &str) -> LabelInit {
171        LabelInit {
172            name: name.into(),
173            side: LabelSide::Bottom,
174            offset: FracVal::from(1.5),
175            hidden: true,
176        }
177    }
178
179    #[test]
180    fn a_block_from_init_carries_every_value_at_the_create_order() {
181        let icon = Icon {
182            asset: asset(b"icon"),
183            rect: screen_rect(1.0, 2.0),
184        };
185        let init = BlockInit {
186            parent: block_id(1),
187            rect: rect(2, 3, 4, 5),
188            locked: true,
189            title: label_init("title"),
190            type_label: label_init("type"),
191            icon: icon.clone(),
192        };
193        let block = Block::from_init(&init, at(7));
194
195        assert_eq!(*block.parent.as_ref(), block_id(1));
196        assert_eq!(*block.rect.as_ref(), rect(2, 3, 4, 5));
197        assert!(*block.locked.as_ref());
198        assert_eq!(block.title.name.as_ref().as_str(), "title");
199        assert_eq!(*block.title.side.as_ref(), LabelSide::Bottom);
200        assert_eq!(*block.title.offset.as_ref(), FracVal::from(1.5));
201        assert!(*block.title.hidden.as_ref());
202        assert_eq!(block.type_label.name.as_ref().as_str(), "type");
203        assert_eq!(*block.icon.as_ref(), icon);
204
205        for order in [
206            block.parent.order(),
207            block.rect.order(),
208            block.locked.order(),
209            block.title.name.order(),
210            block.title.side.order(),
211            block.title.offset.order(),
212            block.title.hidden.order(),
213            block.type_label.name.order(),
214            block.type_label.side.order(),
215            block.type_label.offset.order(),
216            block.type_label.hidden.order(),
217            block.icon.order(),
218        ] {
219            assert_eq!(order, at(7));
220        }
221    }
222
223    /// Every register a different value, so a walk cannot pass by reading
224    /// a neighbour.
225    fn distinct_pin() -> PinInit {
226        PinInit {
227            owner: block_id(1),
228            name: "clk".into(),
229            type_name: "clock".into(),
230            tag: "t0".into(),
231            tag_hidden: true,
232            rect: rect(1, 1, 2, 2),
233            dir: PinDir::Output,
234            pin_accent: Role::Accent3,
235            port_accent: Role::Accent4,
236            port_pin_accent: Role::Accent5,
237            flip_lr: true,
238        }
239    }
240
241    #[test]
242    fn a_pin_from_init_carries_every_value_at_the_create_order() {
243        let init = distinct_pin();
244        let pin = Pin::from_init(&init, at(9));
245
246        assert_eq!(*pin.owner.as_ref(), block_id(1));
247        assert_eq!(pin.name.as_ref().as_str(), "clk");
248        assert_eq!(pin.type_name.as_ref().as_str(), "clock");
249        assert_eq!(pin.tag.as_ref().as_str(), "t0");
250        assert!(*pin.tag_hidden.as_ref());
251        assert_eq!(*pin.rect.as_ref(), rect(1, 1, 2, 2));
252        assert_eq!(*pin.dir.as_ref(), PinDir::Output);
253        assert_eq!(*pin.pin_accent.as_ref(), Role::Accent3);
254        assert_eq!(*pin.port_accent.as_ref(), Role::Accent4);
255        assert_eq!(*pin.port_pin_accent.as_ref(), Role::Accent5);
256        assert!(*pin.flip_lr.as_ref());
257
258        for order in [
259            pin.owner.order(),
260            pin.name.order(),
261            pin.type_name.order(),
262            pin.tag.order(),
263            pin.tag_hidden.order(),
264            pin.rect.order(),
265            pin.dir.order(),
266            pin.pin_accent.order(),
267            pin.port_accent.order(),
268            pin.port_pin_accent.order(),
269            pin.flip_lr.order(),
270        ] {
271            assert_eq!(order, at(9));
272        }
273    }
274
275    #[test]
276    fn a_route_from_init_carries_its_values_and_endpoint_constants() {
277        let waypoints = vec![Waypoint {
278            pos: GridPoint { x: 3, y: 4 },
279            locked: true,
280        }];
281        let init = RouteInit {
282            owner: block_id(1),
283            name: "net7".into(),
284            from: pin_id(2),
285            to: pin_id(3),
286            role: Role::Accent2,
287            waypoints: waypoints.clone(),
288        };
289        let route = Route::from_init(&init, at(4));
290
291        assert_eq!(*route.owner.as_ref(), block_id(1));
292        assert_eq!(route.name.as_ref().as_str(), "net7");
293        assert_eq!(route.from, pin_id(2));
294        assert_eq!(route.to, pin_id(3));
295        assert_eq!(*route.role.as_ref(), Role::Accent2);
296        assert_eq!(*route.waypoints.as_ref(), waypoints);
297
298        for order in [
299            route.owner.order(),
300            route.name.order(),
301            route.role.order(),
302            route.waypoints.order(),
303        ] {
304            assert_eq!(order, at(4));
305        }
306    }
307
308    #[test]
309    fn the_small_kinds_from_init_carry_their_values_at_the_create_order() {
310        let route_label = RouteLabel::from_init(
311            &RouteLabelInit {
312                owner: route_id(1),
313                pos: FracVal::from(0.25),
314            },
315            at(2),
316        );
317        assert_eq!(*route_label.owner.as_ref(), route_id(1));
318        assert_eq!(*route_label.pos.as_ref(), FracVal::from(0.25));
319        assert_eq!(route_label.owner.order(), at(2));
320        assert_eq!(route_label.pos.order(), at(2));
321
322        let text = Text::from_init(
323            &TextInit {
324                owner: block_id(1),
325                text: "note".into(),
326                pos: GridPoint { x: 5, y: 6 },
327                role: Role::Accent1,
328            },
329            at(3),
330        );
331        assert_eq!(text.text.as_ref().as_str(), "note");
332        assert_eq!(*text.pos.as_ref(), GridPoint { x: 5, y: 6 });
333        for order in [
334            text.owner.order(),
335            text.text.order(),
336            text.pos.order(),
337            text.role.order(),
338        ] {
339            assert_eq!(order, at(3));
340        }
341
342        let comment = Comment::from_init(
343            &CommentInit {
344                owner: block_id(1),
345                rect: rect(0, 0, 3, 3),
346                role: Role::Accent6,
347                title: label_init("comment"),
348            },
349            at(5),
350        );
351        assert_eq!(comment.title.name.as_ref().as_str(), "comment");
352        for order in [
353            comment.owner.order(),
354            comment.rect.order(),
355            comment.role.order(),
356            comment.title.name.order(),
357            comment.title.side.order(),
358            comment.title.offset.order(),
359            comment.title.hidden.order(),
360        ] {
361            assert_eq!(order, at(5));
362        }
363
364        let image = Image::from_init(
365            &ImageInit {
366                owner: block_id(1),
367                asset: asset(b"png"),
368                rect: screen_rect(3.0, 4.0),
369            },
370            at(6),
371        );
372        assert_eq!(*image.asset.as_ref(), asset(b"png"));
373        for order in [image.owner.order(), image.asset.order(), image.rect.order()] {
374            assert_eq!(order, at(6));
375        }
376    }
377
378    /// The descent-mistargeting hazard: `Title` and `TypeLabel` share the
379    /// `LabelUpdate` vocabulary, so the test must prove the update landed in
380    /// the named namespace and not its twin.
381    #[test]
382    fn namespace_descent_lands_in_the_named_label() {
383        let init = BlockInit {
384            parent: block_id(1),
385            rect: rect(0, 0, 1, 1),
386            locked: false,
387            title: label_init("title"),
388            type_label: label_init("type"),
389            icon: Icon::default(),
390        };
391        let mut block = Block::from_init(&init, at(1));
392
393        let applied = block.apply(
394            &BlockUpdate::Title(LabelUpdate::Name("renamed".into())),
395            at(2),
396        );
397        assert_eq!(applied, Applied::Won);
398        assert_eq!(block.title.name.as_ref().as_str(), "renamed");
399        assert_eq!(
400            block.type_label.name.as_ref().as_str(),
401            "type",
402            "the twin namespace must be untouched"
403        );
404        assert_eq!(
405            block.title.side.order(),
406            at(1),
407            "sibling registers in the named namespace must be untouched"
408        );
409    }
410
411    #[test]
412    fn apply_lands_each_update_in_its_register() {
413        let mut pin = Pin::from_init(
414            &PinInit {
415                owner: block_id(1),
416                name: "d0".into(),
417                type_name: String::new(),
418                tag: String::new(),
419                tag_hidden: false,
420                rect: rect(0, 0, 1, 1),
421                dir: PinDir::Input,
422                pin_accent: Role::Accent0,
423                port_accent: Role::Accent0,
424                port_pin_accent: Role::Accent0,
425                flip_lr: false,
426            },
427            at(1),
428        );
429        assert_eq!(
430            pin.apply(&PinUpdate::Dir(PinDir::InOut), at(2)),
431            Applied::Won
432        );
433        assert_eq!(*pin.dir.as_ref(), PinDir::InOut);
434        assert_eq!(
435            pin.name.order(),
436            at(1),
437            "untouched registers keep their order"
438        );
439
440        let mut route = Route::from_init(
441            &RouteInit {
442                owner: block_id(1),
443                name: String::new(),
444                from: pin_id(2),
445                to: pin_id(3),
446                role: Role::Accent0,
447                waypoints: Vec::new(),
448            },
449            at(1),
450        );
451        let dragged = vec![Waypoint {
452            pos: GridPoint { x: 9, y: 9 },
453            locked: false,
454        }];
455        assert_eq!(
456            route.apply(&RouteUpdate::Waypoints(dragged.clone()), at(2)),
457            Applied::Won
458        );
459        assert_eq!(*route.waypoints.as_ref(), dragged);
460    }
461
462    /// The LWW refusal path through an entity: a stale update reports
463    /// `LostToNewer` and the newer value stands.
464    #[test]
465    fn a_stale_update_is_refused_and_the_value_stands() {
466        let init = BlockInit {
467            parent: block_id(1),
468            rect: rect(2, 3, 4, 5),
469            locked: false,
470            title: label_init("title"),
471            type_label: label_init("type"),
472            icon: Icon::default(),
473        };
474        let mut block = Block::from_init(&init, at(5));
475
476        let applied = block.apply(&BlockUpdate::Rect(rect(9, 9, 9, 9)), at(3));
477        assert_eq!(applied, Applied::LostToNewer);
478        assert_eq!(*block.rect.as_ref(), rect(2, 3, 4, 5));
479        assert_eq!(block.rect.order(), at(5));
480    }
481
482    /// The `max_order` proof: `max_order` is a hand-written fold, so the
483    /// compiler cannot catch a register missing from it. The update
484    /// vocabularies mirror the registers 1:1, so applying *every* variant
485    /// at a distinct ascending order and asserting the max tracks each
486    /// write is register coverage — a register skipped by the fold leaves
487    /// the max behind when its variant's turn comes.
488    fn the_walk_raises_max_order<E: Entity>(entity: &mut E, updates: Vec<E::Update>)
489    where
490        E::Update: std::fmt::Debug,
491    {
492        assert_eq!(
493            entity.max_order(),
494            at(1),
495            "the walk starts at the create order"
496        );
497        for (ndx, update) in updates.into_iter().enumerate() {
498            let order = at(2 + ndx as u64);
499            assert_eq!(
500                entity.apply(&update, order),
501                Applied::Won,
502                "the walk write {update:?} must land"
503            );
504            assert_eq!(
505                entity.max_order(),
506                order,
507                "the register behind {update:?} must join the max_order fold"
508            );
509        }
510    }
511
512    fn pin_fixture(order: WriteOrder) -> Pin {
513        Pin::from_init(
514            &PinInit {
515                owner: block_id(1),
516                name: "d0".into(),
517                type_name: String::new(),
518                tag: String::new(),
519                tag_hidden: false,
520                rect: rect(0, 0, 1, 1),
521                dir: PinDir::Input,
522                pin_accent: Role::Accent0,
523                port_accent: Role::Accent0,
524                port_pin_accent: Role::Accent0,
525                flip_lr: false,
526            },
527            order,
528        )
529    }
530
531    #[test]
532    fn max_order_walks_every_block_register() {
533        let mut block = Block::from_init(
534            &BlockInit {
535                parent: block_id(1),
536                rect: rect(0, 0, 1, 1),
537                locked: false,
538                title: label_init("title"),
539                type_label: label_init("type"),
540                icon: Icon::default(),
541            },
542            at(1),
543        );
544        the_walk_raises_max_order(
545            &mut block,
546            vec![
547                BlockUpdate::Parent(block_id(9)),
548                BlockUpdate::Rect(rect(9, 9, 9, 9)),
549                BlockUpdate::Locked(true),
550                BlockUpdate::Title(LabelUpdate::Name("renamed".into())),
551                BlockUpdate::Title(LabelUpdate::Side(LabelSide::Center)),
552                BlockUpdate::Title(LabelUpdate::Offset(FracVal::from(2.0))),
553                BlockUpdate::Title(LabelUpdate::Hidden(true)),
554                BlockUpdate::TypeLabel(LabelUpdate::Name("renamed".into())),
555                BlockUpdate::TypeLabel(LabelUpdate::Side(LabelSide::Center)),
556                BlockUpdate::TypeLabel(LabelUpdate::Offset(FracVal::from(2.0))),
557                BlockUpdate::TypeLabel(LabelUpdate::Hidden(true)),
558                BlockUpdate::Icon(Icon::default()),
559            ],
560        );
561    }
562
563    #[test]
564    fn max_order_walks_every_pin_register() {
565        let mut pin = pin_fixture(at(1));
566        the_walk_raises_max_order(
567            &mut pin,
568            vec![
569                PinUpdate::Owner(block_id(9)),
570                PinUpdate::Name("clk".into()),
571                PinUpdate::TypeName("clock".into()),
572                PinUpdate::Tag("t1".into()),
573                PinUpdate::TagHidden(true),
574                PinUpdate::Rect(rect(9, 9, 9, 9)),
575                PinUpdate::Dir(PinDir::Output),
576                PinUpdate::PinAccent(Role::Accent1),
577                PinUpdate::PortAccent(Role::Accent2),
578                PinUpdate::PortPinAccent(Role::Accent3),
579                PinUpdate::FlipLR(true),
580            ],
581        );
582    }
583
584    #[test]
585    fn max_order_walks_the_small_kinds_registers() {
586        let mut route = Route::from_init(
587            &RouteInit {
588                owner: block_id(1),
589                name: String::new(),
590                from: pin_id(2),
591                to: pin_id(3),
592                role: Role::Accent0,
593                waypoints: Vec::new(),
594            },
595            at(1),
596        );
597        the_walk_raises_max_order(
598            &mut route,
599            vec![
600                RouteUpdate::Owner(block_id(9)),
601                RouteUpdate::Name("net".into()),
602                RouteUpdate::Role(Role::Accent1),
603                RouteUpdate::Waypoints(Vec::new()),
604            ],
605        );
606
607        let mut route_label = RouteLabel::from_init(
608            &RouteLabelInit {
609                owner: route_id(1),
610                pos: FracVal::from(0.25),
611            },
612            at(1),
613        );
614        the_walk_raises_max_order(
615            &mut route_label,
616            vec![
617                RouteLabelUpdate::Owner(route_id(9)),
618                RouteLabelUpdate::Pos(FracVal::from(0.5)),
619            ],
620        );
621
622        let mut text = Text::from_init(
623            &TextInit {
624                owner: block_id(1),
625                text: "note".into(),
626                pos: GridPoint { x: 5, y: 6 },
627                role: Role::Accent1,
628            },
629            at(1),
630        );
631        the_walk_raises_max_order(
632            &mut text,
633            vec![
634                TextUpdate::Owner(block_id(9)),
635                TextUpdate::Text("edited".into()),
636                TextUpdate::Pos(GridPoint { x: 9, y: 9 }),
637                TextUpdate::Role(Role::Accent2),
638            ],
639        );
640
641        let mut comment = Comment::from_init(
642            &CommentInit {
643                owner: block_id(1),
644                rect: rect(0, 0, 3, 3),
645                role: Role::Accent6,
646                title: label_init("comment"),
647            },
648            at(1),
649        );
650        the_walk_raises_max_order(
651            &mut comment,
652            vec![
653                CommentUpdate::Owner(block_id(9)),
654                CommentUpdate::Rect(rect(9, 9, 9, 9)),
655                CommentUpdate::Role(Role::Accent1),
656                CommentUpdate::Title(LabelUpdate::Name("renamed".into())),
657                CommentUpdate::Title(LabelUpdate::Side(LabelSide::Center)),
658                CommentUpdate::Title(LabelUpdate::Offset(FracVal::from(2.0))),
659                CommentUpdate::Title(LabelUpdate::Hidden(true)),
660            ],
661        );
662
663        let mut image = Image::from_init(
664            &ImageInit {
665                owner: block_id(1),
666                asset: asset(b"png"),
667                rect: screen_rect(3.0, 4.0),
668            },
669            at(1),
670        );
671        the_walk_raises_max_order(
672            &mut image,
673            vec![
674                ImageUpdate::Owner(block_id(9)),
675                ImageUpdate::Asset(asset(b"jpg")),
676                ImageUpdate::Rect(screen_rect(9.0, 9.0)),
677            ],
678        );
679    }
680
681    /// Presence joins the fold through `Live`: delete and restore raise
682    /// the entity's max order, and inner writes raise it through the
683    /// wrapper.
684    #[test]
685    fn presence_and_inner_writes_raise_a_lives_max_order() {
686        let mut live = Live::new(pin_fixture(at(1)), at(1));
687        assert_eq!(live.max_order(), at(1));
688
689        assert_eq!(live.delete(at(2)), Applied::Won);
690        assert_eq!(live.max_order(), at(2), "a delete raises the max");
691        assert_eq!(live.restore(at(3)), Applied::Won);
692        assert_eq!(live.max_order(), at(3), "a restore raises the max");
693        assert_eq!(
694            live.apply_update(&PinUpdate::Dir(PinDir::InOut), at(4)),
695            Applied::Won
696        );
697        assert_eq!(live.max_order(), at(4), "an inner write raises the max");
698    }
699
700    /// `invert` must read the register its variant names. `apply` and
701    /// `invert` are generated from the same `$variant => $field` pair, so
702    /// they cannot disagree — but the pairing itself is worth walking
703    /// once, against a fixture whose registers all differ.
704    #[test]
705    fn invert_reads_the_register_each_variant_targets() {
706        let pin = Pin::from_init(&distinct_pin(), at(1));
707
708        assert!(matches!(
709            pin.invert(&PinUpdate::Owner(block_id(9))),
710            PinUpdate::Owner(was) if was == block_id(1)
711        ));
712        assert!(matches!(
713            pin.invert(&PinUpdate::Name("d0".into())),
714            PinUpdate::Name(was) if was == "clk"
715        ));
716        assert!(matches!(
717            pin.invert(&PinUpdate::TypeName("reset".into())),
718            PinUpdate::TypeName(was) if was == "clock"
719        ));
720        assert!(matches!(
721            pin.invert(&PinUpdate::Tag("t9".into())),
722            PinUpdate::Tag(was) if was == "t0"
723        ));
724        assert!(matches!(
725            pin.invert(&PinUpdate::TagHidden(false)),
726            PinUpdate::TagHidden(true)
727        ));
728        assert!(matches!(
729            pin.invert(&PinUpdate::Rect(rect(9, 9, 9, 9))),
730            PinUpdate::Rect(was) if was == rect(1, 1, 2, 2)
731        ));
732        assert!(matches!(
733            pin.invert(&PinUpdate::Dir(PinDir::InOut)),
734            PinUpdate::Dir(PinDir::Output)
735        ));
736        assert!(matches!(
737            pin.invert(&PinUpdate::PinAccent(Role::Accent0)),
738            PinUpdate::PinAccent(Role::Accent3)
739        ));
740        assert!(matches!(
741            pin.invert(&PinUpdate::PortAccent(Role::Accent0)),
742            PinUpdate::PortAccent(Role::Accent4)
743        ));
744        assert!(matches!(
745            pin.invert(&PinUpdate::PortPinAccent(Role::Accent0)),
746            PinUpdate::PortPinAccent(Role::Accent5)
747        ));
748        assert!(matches!(
749            pin.invert(&PinUpdate::FlipLR(false)),
750            PinUpdate::FlipLR(true)
751        ));
752    }
753
754    /// The descent-mistargeting hazard again, from the journal's side: an
755    /// inverse built for `Title` must carry `Title`'s displaced value, not
756    /// its twin's.
757    #[test]
758    fn invert_descends_into_the_named_namespace() {
759        let block = Block::from_init(
760            &BlockInit {
761                parent: block_id(1),
762                rect: rect(0, 0, 1, 1),
763                locked: false,
764                title: label_init("title"),
765                type_label: label_init("type"),
766                icon: Icon::default(),
767            },
768            at(1),
769        );
770
771        assert!(matches!(
772            block.invert(&BlockUpdate::Title(LabelUpdate::Name("renamed".into()))),
773            BlockUpdate::Title(LabelUpdate::Name(was)) if was == "title"
774        ));
775        assert!(matches!(
776            block.invert(&BlockUpdate::TypeLabel(LabelUpdate::Name("renamed".into()))),
777            BlockUpdate::TypeLabel(LabelUpdate::Name(was)) if was == "type"
778        ));
779    }
780
781    /// Undo is a forward write, not a rewind: the captured value returns,
782    /// at a new order.
783    #[test]
784    fn an_inverse_restores_the_displaced_value_as_a_new_write() {
785        let mut pin = Pin::from_init(&distinct_pin(), at(1));
786        let inverse = pin.invert(&PinUpdate::Tag("t9".into()));
787
788        assert_eq!(pin.apply(&PinUpdate::Tag("t9".into()), at(2)), Applied::Won);
789        assert_eq!(pin.tag.as_ref().as_str(), "t9");
790
791        assert_eq!(pin.apply(&inverse, at(3)), Applied::Won);
792        assert_eq!(pin.tag.as_ref().as_str(), "t0");
793        assert_eq!(pin.tag.order(), at(3), "the undo is the latest write");
794    }
795
796    /// Untouched entities tie at their creation order — and only there:
797    /// distinct ops can never produce equal max orders across entities.
798    #[test]
799    fn untouched_entities_tie_at_their_creation_order() {
800        let one = Live::new(pin_fixture(at(7)), at(7));
801        let other = Live::new(pin_fixture(at(7)), at(7));
802        assert_eq!(one.max_order(), other.max_order());
803        assert_eq!(one.max_order(), at(7));
804    }
805}