Skip to main content

zerodds_ast/
edit.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The edit surface. Spec §5.1.
4//!
5//! Every mutation runs in a transaction. A call made outside an explicit
6//! one opens a single-operation transaction and commits it, so the two
7//! paths cannot diverge: the direct call is the transactional call with
8//! a `begin` and `commit` around it, not a second implementation.
9//!
10//! A rejected commit walks the undo log backwards and leaves the model
11//! exactly as it was before `begin`.
12
13use crate::error::AstError;
14use crate::invariant::{self, InvariantViolation};
15use crate::model::Ast;
16use crate::node::AnyId;
17use crate::node::type_::TypeKind;
18use alloc::vec::Vec;
19use zerodds_ast_core::{
20    Change, Changeset, FieldValue, NodeId, Ref, TxnLog, UndoPayload, UndoRecord,
21};
22
23/// A node removed during a transaction, kept so rollback can put it back
24/// without having serialised it on the success path.
25pub(crate) enum Stashed {
26    /// A type together with the module it belonged to.
27    Type(crate::TypeNode),
28    /// A member together with its position in its aggregate.
29    Member(crate::MemberNode, usize),
30}
31
32/// An open transaction over a model.
33///
34/// Dropping one without calling [`AstTransaction::commit`] rolls it
35/// back: a transaction that goes out of scope unfinished must not leave
36/// half its changes behind.
37pub struct AstTransaction<'a> {
38    ast: &'a mut Ast,
39    finished: bool,
40}
41
42impl<'a> AstTransaction<'a> {
43    pub(crate) fn new(ast: &'a mut Ast) -> Self {
44        ast.sequence = ast.sequence.saturating_add(1);
45        let sequence = ast.sequence;
46        ast.pending = Some(TxnLog::new(sequence));
47        Self {
48            ast,
49            finished: false,
50        }
51    }
52
53    /// The model under edit.
54    ///
55    /// Mutating calls made through this reference join the transaction
56    /// rather than committing on their own.
57    pub fn ast(&mut self) -> &mut Ast {
58        self.ast
59    }
60
61    /// The model under edit, read-only.
62    #[must_use]
63    pub fn model(&self) -> &Ast {
64        self.ast
65    }
66
67    /// Validate and publish the transaction.
68    ///
69    /// # Errors
70    /// [`AstError::Invariant`] when a structural invariant fails, in
71    /// which case the model has been rolled back to its state before
72    /// `begin` and no observer was notified. Spec §8.1.
73    pub fn commit(mut self) -> Result<Changeset, AstError> {
74        self.finished = true;
75        let Some(log) = self.ast.pending.take() else {
76            return Ok(Changeset::new(self.ast.sequence));
77        };
78
79        let touched: Vec<NodeId> = log.nodes_touched().collect();
80        let violations = invariant::check(self.ast, &touched);
81        if !violations.is_empty() {
82            self.ast.apply_rollback(&log);
83            return Err(AstError::Invariant(violations));
84        }
85
86        let changeset = log.into_changeset();
87        self.ast.stash.clear();
88        self.ast.observers.notify(&changeset);
89        Ok(changeset)
90    }
91
92    /// Discard every change the transaction made.
93    pub fn rollback(mut self) {
94        self.finished = true;
95        if let Some(log) = self.ast.pending.take() {
96            self.ast.apply_rollback(&log);
97        }
98    }
99}
100
101impl Drop for AstTransaction<'_> {
102    fn drop(&mut self) {
103        if !self.finished {
104            if let Some(log) = self.ast.pending.take() {
105                self.ast.apply_rollback(&log);
106            }
107        }
108    }
109}
110
111impl Ast {
112    /// Open a transaction.
113    ///
114    /// Mutations made through it are published as one changeset when it
115    /// commits, and reversed entirely if it does not. Spec §5.1.
116    pub fn begin(&mut self) -> AstTransaction<'_> {
117        AstTransaction::new(self)
118    }
119
120    /// Record a created node, and auto-commit when no transaction is
121    /// open.
122    pub(crate) fn record_created(&mut self, any: AnyId) -> Result<(), AstError> {
123        let change = Change::Created {
124            node: any.node_id(),
125            kind: any.kind().tag(),
126        };
127        let undo = UndoRecord {
128            node: any.node_id(),
129            payload: UndoPayload::Remove,
130        };
131        self.record(undo, change)
132    }
133
134    fn record(&mut self, undo: UndoRecord, change: Change) -> Result<(), AstError> {
135        if let Some(log) = self.pending.as_mut() {
136            log.record(undo, change);
137            return Ok(());
138        }
139        self.record_standalone(undo, change)
140    }
141
142    /// Validate and publish one mutation as its own transaction.
143    pub(crate) fn record_standalone(
144        &mut self,
145        undo: UndoRecord,
146        change: Change,
147    ) -> Result<(), AstError> {
148        // No transaction open: this is a single-operation one.
149        self.sequence = self.sequence.saturating_add(1);
150        let mut log = TxnLog::new(self.sequence);
151        log.record(undo, change);
152
153        let touched: Vec<NodeId> = log.nodes_touched().collect();
154        let violations = invariant::check(self, &touched);
155        if !violations.is_empty() {
156            self.apply_rollback(&log);
157            return Err(AstError::Invariant(violations));
158        }
159
160        let changeset = log.into_changeset();
161        self.stash.clear();
162        self.observers.notify(&changeset);
163        Ok(())
164    }
165
166    /// Reverse every record of a log, newest first.
167    pub(crate) fn apply_rollback(&mut self, log: &TxnLog) {
168        for record in log.undo_records().rev() {
169            self.apply_undo(record);
170        }
171        self.stash.clear();
172    }
173
174    fn apply_undo(&mut self, record: &UndoRecord) {
175        match &record.payload {
176            UndoPayload::Remove => {
177                if let Some(any) = self.resolve_node_id(record.node) {
178                    self.detach_and_remove(any);
179                }
180            }
181            UndoPayload::Recreate { stash } => {
182                self.restore_from_stash(*stash as usize);
183            }
184            UndoPayload::RestoreField { field, value } => {
185                self.restore_field(record.node, *field, value);
186            }
187            UndoPayload::RestorePending { key, slot } => {
188                self.restore_pending(record.node, key, *slot);
189            }
190        }
191    }
192
193    /// Put one field back to a recorded value.
194    ///
195    /// The match is over the fields that have a setter; `PARTS` is a
196    /// notification rather than stored state, and its reversal is the
197    /// part's own removal record.
198    fn restore_field(
199        &mut self,
200        node: NodeId,
201        field: zerodds_ast_core::FieldTag,
202        value: &FieldValue,
203    ) {
204        let Some(any) = self.resolve_node_id(node) else {
205            return;
206        };
207        match field {
208            crate::field::COMPLETENESS => {
209                let FieldValue::Bool(complete) = value else {
210                    return;
211                };
212                if let Some(id) = any.as_type() {
213                    if let Ok(t) = self.types.get_mut(id) {
214                        t.completeness = if *complete {
215                            crate::Completeness::Complete
216                        } else {
217                            crate::Completeness::Forward
218                        };
219                    }
220                }
221            }
222            crate::field::NAME => {
223                let FieldValue::Text(name) = value else {
224                    return;
225                };
226                self.restore_name(any, name);
227            }
228            // PARTS and FACET carry no prior value to restore, and
229            // REFERENCE is reversed through RestorePending, which
230            // carries the key that FieldValue cannot.
231            _ => {}
232        }
233    }
234
235    fn restore_name(&mut self, any: AnyId, name: &str) {
236        match any {
237            AnyId::Type(id, _) => {
238                if let Ok(t) = self.types.get_mut(id) {
239                    t.name = name.into();
240                }
241            }
242            AnyId::Member(id, _) => {
243                if let Ok(m) = self.members.get_mut(id) {
244                    m.name = name.into();
245                }
246            }
247            AnyId::Module(id, _) => {
248                if let Ok(m) = self.modules.get_mut(id) {
249                    m.name = name.into();
250                }
251            }
252            _ => {}
253        }
254    }
255
256    /// Put a resolved reference back to pending on the key it waited on.
257    fn restore_pending(&mut self, node: NodeId, key: &zerodds_ast_core::PendingRef, slot: usize) {
258        let Some(any) = self.resolve_node_id(node) else {
259            return;
260        };
261        let back = Ref::Pending(key.clone());
262        match any {
263            AnyId::Type(id, _) => {
264                if let Ok(t) = self.types.get_mut(id) {
265                    if let Some(reference) = t.kind.type_refs_mut().into_iter().nth(slot) {
266                        *reference = back;
267                    }
268                }
269            }
270            AnyId::Member(id, _) => {
271                if let Ok(m) = self.members.get_mut(id) {
272                    m.type_ = back;
273                }
274            }
275            _ => return,
276        }
277        // The reference waits again, so the index has to know again.
278        self.index_pending(any);
279    }
280
281    fn restore_from_stash(&mut self, index: usize) {
282        let Some(entry) = self.stash.get_mut(index).and_then(Option::take) else {
283            return;
284        };
285        match entry {
286            Stashed::Type(node) => {
287                let module = node.module;
288                let node_id = node.node_id;
289                if let Ok(id) = self.types.insert(node) {
290                    let any = AnyId::Type(id, node_id);
291                    let _ = self.by_node_id.insert(node_id, any);
292                    if let Ok(m) = self.modules.get_mut(module) {
293                        m.children.push(any);
294                    }
295                    self.index_node(any);
296                }
297            }
298            Stashed::Member(node, position) => {
299                let parent = node.parent;
300                let node_id = node.node_id;
301                if let Ok(id) = self.members.insert(node) {
302                    let any = AnyId::Member(id, node_id);
303                    let _ = self.by_node_id.insert(node_id, any);
304                    self.index_node(any);
305                    if let Some(type_id) = parent.as_type() {
306                        if let Ok(t) = self.types.get_mut(type_id) {
307                            match &mut t.kind {
308                                TypeKind::Structure { members, .. }
309                                | TypeKind::Valuetype { state: members, .. } => {
310                                    let at = position.min(members.len());
311                                    members.insert(at, id);
312                                }
313                                _ => {}
314                            }
315                        }
316                    }
317                }
318            }
319        }
320    }
321
322    /// Remove a node, cascading to the parts it owns and breaking every
323    /// reference that pointed at it.
324    ///
325    /// A dangling resolved handle must not survive a delete; incoming
326    /// references become `Broken(NotFound)` so the loss is visible
327    /// rather than silent. Spec §5.2.
328    ///
329    /// # Errors
330    /// [`AstError::NotFound`] when the identity is not in the model,
331    /// [`AstError::Invariant`] when the resulting model breaks a
332    /// structural rule, in which case nothing was removed.
333    pub fn remove(&mut self, any: AnyId) -> Result<(), AstError> {
334        if self.resolve_node_id(any.node_id()).is_none() {
335            return Err(AstError::NotFound {
336                node: any.node_id(),
337            });
338        }
339
340        let own_transaction = self.pending.is_none();
341        if own_transaction {
342            self.sequence = self.sequence.saturating_add(1);
343            let sequence = self.sequence;
344            self.pending = Some(TxnLog::new(sequence));
345        }
346
347        self.remove_cascade(any);
348
349        if !own_transaction {
350            return Ok(());
351        }
352
353        let Some(log) = self.pending.take() else {
354            return Ok(());
355        };
356        let touched: Vec<NodeId> = log.nodes_touched().collect();
357        let violations = invariant::check(self, &touched);
358        if !violations.is_empty() {
359            self.apply_rollback(&log);
360            return Err(AstError::Invariant(violations));
361        }
362        let changeset = log.into_changeset();
363        self.stash.clear();
364        self.observers.notify(&changeset);
365        Ok(())
366    }
367
368    fn remove_cascade(&mut self, any: AnyId) {
369        // Owned parts go first, so a child is never orphaned by its
370        // parent's removal.
371        for child in self.owned_parts_of(any) {
372            self.remove_cascade(child);
373        }
374
375        let node_id = any.node_id();
376        let stash_index = self.stash_node(any);
377        self.detach_and_remove(any);
378        self.break_references_to(any);
379
380        let undo = UndoRecord {
381            node: node_id,
382            payload: stash_index
383                .map_or(UndoPayload::Remove, |stash| UndoPayload::Recreate { stash }),
384        };
385        let change = Change::Deleted {
386            node: node_id,
387            kind: any.kind().tag(),
388        };
389        if let Some(log) = self.pending.as_mut() {
390            log.record(undo, change);
391        }
392    }
393
394    /// The nodes a node owns outright, which go with it.
395    ///
396    /// An interface owns its operations and attributes, an operation its
397    /// parameters, an annotation declaration its members. Only types had
398    /// this, so removing an interface left its operations in the arena
399    /// with no owner and its handle in the module's child list.
400    fn owned_parts_of(&self, any: AnyId) -> Vec<AnyId> {
401        match any {
402            AnyId::Type(id, _) => self.owned_parts_of_type(id),
403            AnyId::Interface(id, _) => {
404                let Ok(node) = self.interfaces.get(id) else {
405                    return Vec::new();
406                };
407                let mut out = Vec::new();
408                for operation in &node.operations {
409                    if let Ok(o) = self.operations.get(*operation) {
410                        out.push(AnyId::Operation(*operation, o.node_id));
411                    }
412                }
413                for attribute in &node.attributes {
414                    if let Ok(a) = self.attributes.get(*attribute) {
415                        out.push(AnyId::Attribute(*attribute, a.node_id));
416                    }
417                }
418                out
419            }
420            AnyId::Operation(id, _) => {
421                let Ok(node) = self.operations.get(id) else {
422                    return Vec::new();
423                };
424                node.params
425                    .iter()
426                    .filter_map(|param| {
427                        self.params
428                            .get(*param)
429                            .ok()
430                            .map(|p| AnyId::Param(*param, p.node_id))
431                    })
432                    .collect()
433            }
434            AnyId::AnnotationDef(id, _) => {
435                let Ok(node) = self.annotation_defs.get(id) else {
436                    return Vec::new();
437                };
438                node.members
439                    .iter()
440                    .filter_map(|member| {
441                        self.annotation_members
442                            .get(*member)
443                            .ok()
444                            .map(|m| AnyId::AnnotationMember(*member, m.node_id))
445                    })
446                    .collect()
447            }
448            AnyId::Exception(id, _) => {
449                let Ok(node) = self.exceptions.get(id) else {
450                    return Vec::new();
451                };
452                node.members
453                    .iter()
454                    .filter_map(|member| {
455                        self.members
456                            .get(*member)
457                            .ok()
458                            .map(|m| AnyId::Member(*member, m.node_id))
459                    })
460                    .collect()
461            }
462            _ => Vec::new(),
463        }
464    }
465
466    fn owned_parts_of_type(&self, id: zerodds_ast_core::Id<crate::TypeNode>) -> Vec<AnyId> {
467        let Ok(node) = self.types.get(id) else {
468            return Vec::new();
469        };
470        let mut out = Vec::new();
471        match &node.kind {
472            TypeKind::Structure { members, .. } | TypeKind::Valuetype { state: members, .. } => {
473                for member in members {
474                    if let Ok(m) = self.members.get(*member) {
475                        out.push(AnyId::Member(*member, m.node_id));
476                    }
477                }
478            }
479            TypeKind::Union { cases, .. } => {
480                for case in cases {
481                    if let Ok(c) = self.cases.get(*case) {
482                        if let Ok(m) = self.members.get(c.member) {
483                            out.push(AnyId::Member(c.member, m.node_id));
484                        }
485                        out.push(AnyId::Case(*case, c.node_id));
486                    }
487                }
488            }
489            TypeKind::Enumeration { enumerators } => {
490                for enumerator in enumerators {
491                    if let Ok(e) = self.enumerators.get(*enumerator) {
492                        out.push(AnyId::Enumerator(*enumerator, e.node_id));
493                    }
494                }
495            }
496            TypeKind::Bitmask { bits } => {
497                for bit in bits {
498                    if let Ok(b) = self.bit_values.get(*bit) {
499                        out.push(AnyId::BitValue(*bit, b.node_id));
500                    }
501                }
502            }
503            TypeKind::Bitset { fields, .. } => {
504                for field in fields {
505                    if let Ok(b) = self.bitfields.get(*field) {
506                        out.push(AnyId::Bitfield(*field, b.node_id));
507                    }
508                }
509            }
510            _ => {}
511        }
512        out
513    }
514
515    /// Move a node into the rollback stash, returning its index.
516    ///
517    /// Only the kinds a rollback has to restore are stashed; the rest
518    /// record a plain `Remove`, which is correct for nodes the same
519    /// transaction created.
520    fn stash_node(&mut self, any: AnyId) -> Option<u32> {
521        let entry = match any {
522            AnyId::Type(id, _) => Stashed::Type(self.types.get(id).ok()?.clone()),
523            AnyId::Member(id, _) => {
524                let member = self.members.get(id).ok()?.clone();
525                let position = member
526                    .parent
527                    .as_type()
528                    .and_then(|t| self.types.get(t).ok())
529                    .and_then(|t| t.kind.members().iter().position(|m| *m == id))
530                    .unwrap_or(0);
531                Stashed::Member(member, position)
532            }
533            _ => return None,
534        };
535        let index = u32::try_from(self.stash.len()).ok()?;
536        self.stash.push(Some(entry));
537        Some(index)
538    }
539
540    fn detach_and_remove(&mut self, any: AnyId) {
541        let node_id = any.node_id();
542        let _ = self.by_node_id.remove(&node_id);
543        // Indices are maintained from the change, never rebuilt: a
544        // deleted node leaves every index in the same commit.
545        self.unindex_node(node_id);
546
547        match any {
548            AnyId::Type(id, _) => {
549                if let Ok(node) = self.types.get(id) {
550                    let module = node.module;
551                    if let Ok(m) = self.modules.get_mut(module) {
552                        m.children.retain(|c| c.node_id() != node_id);
553                    }
554                }
555                let _ = self.types.remove(id);
556            }
557            AnyId::Member(id, _) => {
558                if let Ok(node) = self.members.get(id) {
559                    let parent = node.parent;
560                    if let Some(type_id) = parent.as_type() {
561                        if let Ok(t) = self.types.get_mut(type_id) {
562                            match &mut t.kind {
563                                TypeKind::Structure { members, .. }
564                                | TypeKind::Valuetype { state: members, .. } => {
565                                    members.retain(|m| *m != id);
566                                }
567                                _ => {}
568                            }
569                        }
570                    }
571                }
572                let _ = self.members.remove(id);
573            }
574            AnyId::Case(id, _) => {
575                if let Ok(node) = self.cases.get(id) {
576                    let parent = node.parent;
577                    if let Ok(t) = self.types.get_mut(parent) {
578                        if let TypeKind::Union {
579                            cases,
580                            default_case,
581                            ..
582                        } = &mut t.kind
583                        {
584                            cases.retain(|c| *c != id);
585                            if *default_case == Some(id) {
586                                *default_case = None;
587                            }
588                        }
589                    }
590                }
591                let _ = self.cases.remove(id);
592            }
593            AnyId::Enumerator(id, _) => {
594                if let Ok(node) = self.enumerators.get(id) {
595                    let parent = node.parent;
596                    if let Ok(t) = self.types.get_mut(parent) {
597                        if let TypeKind::Enumeration { enumerators } = &mut t.kind {
598                            enumerators.retain(|e| *e != id);
599                        }
600                    }
601                }
602                let _ = self.enumerators.remove(id);
603            }
604            AnyId::BitValue(id, _) => {
605                if let Ok(node) = self.bit_values.get(id) {
606                    let parent = node.parent;
607                    if let Ok(t) = self.types.get_mut(parent) {
608                        if let TypeKind::Bitmask { bits } = &mut t.kind {
609                            bits.retain(|b| *b != id);
610                        }
611                    }
612                }
613                let _ = self.bit_values.remove(id);
614            }
615            AnyId::Bitfield(id, _) => {
616                if let Ok(node) = self.bitfields.get(id) {
617                    let parent = node.parent;
618                    if let Ok(t) = self.types.get_mut(parent) {
619                        if let TypeKind::Bitset { fields, .. } = &mut t.kind {
620                            fields.retain(|f| *f != id);
621                        }
622                    }
623                }
624                let _ = self.bitfields.remove(id);
625            }
626            AnyId::Module(id, _) => {
627                if let Ok(node) = self.modules.get(id) {
628                    if let Some(parent) = node.parent {
629                        if let Ok(m) = self.modules.get_mut(parent) {
630                            m.children.retain(|c| c.node_id() != node_id);
631                        }
632                    }
633                }
634                let _ = self.modules.remove(id);
635            }
636            // Every kind below leaves a handle in whatever list holds
637            // it unless it is taken out here. The arena is generational,
638            // so a stale handle answers `Err` rather than the wrong
639            // node — which is why the omission was silent: the list
640            // still carried an entry that resolved to nothing, and
641            // every consumer walking `interface.attributes` or
642            // `module.children` saw one node too many.
643            AnyId::Const(id, _) => {
644                if let Ok(node) = self.constants.get(id) {
645                    let module = node.module;
646                    if let Ok(m) = self.modules.get_mut(module) {
647                        m.children.retain(|c| c.node_id() != node_id);
648                    }
649                }
650                let _ = self.constants.remove(id);
651            }
652            AnyId::ConstExpr(id, _) => {
653                let _ = self.exprs.remove(id);
654            }
655            AnyId::Interface(id, _) => {
656                if let Ok(node) = self.interfaces.get(id) {
657                    let module = node.module;
658                    if let Ok(m) = self.modules.get_mut(module) {
659                        m.children.retain(|c| c.node_id() != node_id);
660                    }
661                }
662                let _ = self.interfaces.remove(id);
663            }
664            AnyId::Operation(id, _) => {
665                if let Ok(node) = self.operations.get(id) {
666                    if let Some(interface) = node.parent.as_interface() {
667                        if let Ok(i) = self.interfaces.get_mut(interface) {
668                            i.operations.retain(|o| *o != id);
669                        }
670                    }
671                    if let Some(type_id) = node.parent.as_type() {
672                        if let Ok(t) = self.types.get_mut(type_id) {
673                            if let TypeKind::Valuetype { initializers, .. } = &mut t.kind {
674                                initializers.retain(|o| *o != id);
675                            }
676                        }
677                    }
678                }
679                let _ = self.operations.remove(id);
680            }
681            AnyId::Param(id, _) => {
682                if let Ok(node) = self.params.get(id) {
683                    let parent = node.parent;
684                    if let Ok(o) = self.operations.get_mut(parent) {
685                        o.params.retain(|p| *p != id);
686                    }
687                }
688                let _ = self.params.remove(id);
689            }
690            AnyId::Attribute(id, _) => {
691                if let Ok(node) = self.attributes.get(id) {
692                    let parent = node.parent;
693                    if let Ok(i) = self.interfaces.get_mut(parent) {
694                        i.attributes.retain(|a| *a != id);
695                    }
696                }
697                let _ = self.attributes.remove(id);
698            }
699            AnyId::Exception(id, _) => {
700                if let Ok(node) = self.exceptions.get(id) {
701                    let module = node.module;
702                    if let Ok(m) = self.modules.get_mut(module) {
703                        m.children.retain(|c| c.node_id() != node_id);
704                    }
705                }
706                let _ = self.exceptions.remove(id);
707            }
708            AnyId::AnnotationDef(id, _) => {
709                if let Ok(node) = self.annotation_defs.get(id) {
710                    let module = node.module;
711                    if let Ok(m) = self.modules.get_mut(module) {
712                        m.children.retain(|c| c.node_id() != node_id);
713                    }
714                }
715                let _ = self.annotation_defs.remove(id);
716            }
717            AnyId::AnnotationMember(id, _) => {
718                if let Ok(node) = self.annotation_members.get(id) {
719                    let parent = node.parent;
720                    if let Ok(d) = self.annotation_defs.get_mut(parent) {
721                        d.members.retain(|m| *m != id);
722                    }
723                }
724                let _ = self.annotation_members.remove(id);
725            }
726            AnyId::Domain(id, _) => {
727                let _ = self.domains.remove(id);
728            }
729            AnyId::Topic(id, _) => {
730                let _ = self.topics.remove(id);
731            }
732            AnyId::QosProfile(id, _) => {
733                let _ = self.qos_profiles.remove(id);
734            }
735            AnyId::Participant(id, _) => {
736                let _ = self.participants.remove(id);
737            }
738            AnyId::Endpoint(id, _) => {
739                let _ = self.endpoints.remove(id);
740            }
741            AnyId::Library(id, _) => {
742                let _ = self.libraries.remove(id);
743            }
744        }
745    }
746
747    /// Rewrite every resolved reference to a removed node into
748    /// `Broken(NotFound)`.
749    fn break_references_to(&mut self, removed: AnyId) {
750        let Some(type_id) = removed.as_type() else {
751            return;
752        };
753        let cause = zerodds_ast_core::BreakCause::NotFound {
754            what: zerodds_ast_core::PendingRef::ScopedName(zerodds_ast_core::ScopedName::absolute(
755                alloc::vec::Vec::new(),
756            )),
757        };
758
759        for (_, node) in self.types.iter_mut_all() {
760            break_in_kind(&mut node.kind, type_id, &cause);
761        }
762        for (_, member) in self.members.iter_mut_all() {
763            if member.type_.resolved() == Some(type_id) {
764                member.type_ = Ref::Broken(cause.clone());
765            }
766        }
767    }
768}
769
770fn break_in_kind(
771    kind: &mut TypeKind,
772    removed: zerodds_ast_core::Id<crate::TypeNode>,
773    cause: &zerodds_ast_core::BreakCause,
774) {
775    let fix = |reference: &mut Ref<crate::TypeNode>| {
776        if reference.resolved() == Some(removed) {
777            *reference = Ref::Broken(cause.clone());
778        }
779    };
780    match kind {
781        TypeKind::Sequence { element, .. } | TypeKind::Array { element, .. } => fix(element),
782        TypeKind::Map { key, value, .. } => {
783            fix(key);
784            fix(value);
785        }
786        TypeKind::Alias { aliased } => fix(aliased),
787        TypeKind::ValueBox { boxed } => fix(boxed),
788        TypeKind::Union { discriminator, .. } => fix(discriminator),
789        TypeKind::Structure { base, .. } | TypeKind::Bitset { base, .. } => {
790            if let Some(base) = base.as_mut() {
791                fix(base);
792            }
793        }
794        TypeKind::Valuetype { bases, .. } => {
795            for base in bases.iter_mut() {
796                fix(base);
797            }
798        }
799        _ => {}
800    }
801}
802
803/// Re-exported so consumers can match on what a commit rejected.
804pub use crate::invariant::InvariantViolation as CommitViolation;
805
806const _: () = {
807    // Keeps the alias referenced even when no consumer uses it yet.
808    let _ = core::mem::size_of::<InvariantViolation>();
809};