Skip to main content

zerodds_wire_probe/
recursion.rs

1//! The aggregates of a type graph that sit on a cycle.
2//!
3//! A dump emitter inlines a member's statements into its owner's, and a
4//! type graph with a cycle has no finite inline expansion: the CORBA
5//! C++ emitter followed `recursive-mutual`'s
6//! `A → sequence<B> → B → sequence<A>` until the Rust stack ran out,
7//! and the C and Fast DDS emitters shared the walk and the crash. The
8//! types this module names are emitted as functions instead, and the
9//! expansion calls them where it would have unrolled them.
10//!
11//! ## Which walks ask, and which two did not
12//!
13//! Three emitters carry the answer: [`mod@crate::dump`], [`crate::dump_cpp`]
14//! and [`crate::corba_cpp`]'s dump half, each of which emits one
15//! function per aggregate named here and calls it in place of the
16//! expansion.
17//!
18//! Two ran past it and followed the cycle instead, measured on
19//! 2026-08-31 over `recursive-mutual` — both reached
20//! "has overflowed its stack" and `SIGABRT`:
21//!
22//! - `corba_java::dump_at`. The C++ half of that same file was given the
23//!   machinery; the Java half was not.
24//! - `dump_ros::emit`. Its comment named `ros_idl` as the gate, and
25//!   `ros_idl` does refuse a recursive type — but `wireprobe-dump --ros`
26//!   is a separate process invocation and no gate on this function.
27//!
28//! Neither has a shape to emit a function into — a Java method body and
29//! a C block inside the ROS decode template — and neither column takes a
30//! recursive type at all, so both now refuse one by name, in the words
31//! `corba_idl` and `ros_idl` already use for it. A refusal is a cell
32//! that says why; a stack overflow is a cell that measured nothing.
33//!
34//! The fill emitters do not ask, and do not need to: a fill walk follows
35//! the sample, and a recursive type has finite samples because the
36//! sequence that closes its cycle is eventually empty. That is a
37//! termination argument rather than a bound, and it is the sample's
38//! nesting that bounds them.
39
40use crate::contract;
41use zerodds_ast::{Ast, TypeKind, TypeNode};
42use zerodds_ast_core::Id;
43
44/// The aggregates reachable from `root` that sit on a cycle, in
45/// first-visit order.
46pub fn recursive_aggregates(ast: &Ast, root: Id<TypeNode>) -> Vec<Id<TypeNode>> {
47    let mut all: Vec<Id<TypeNode>> = Vec::new();
48    let mut queue = Vec::new();
49    collect_aggregates(ast, root, &mut queue, &mut Vec::new());
50    while let Some(id) = queue.pop() {
51        if all.contains(&id) {
52            continue;
53        }
54        all.push(id);
55        queue.extend(aggregate_successors(ast, id));
56    }
57    all.into_iter()
58        .filter(|id| reaches(ast, *id, *id, &mut Vec::new()))
59        .collect()
60}
61
62/// Whether `target` is reachable from `from` over aggregate edges.
63///
64/// zerodds-lint: recursion-depth = the number of aggregates in the
65/// model. `seen` holds every aggregate this descent has already entered
66/// and nothing is entered twice, so the depth cannot pass the count of
67/// distinct aggregates the model declares — which is what makes the
68/// walk end on the cycle it is looking for rather than following it.
69fn reaches(
70    ast: &Ast,
71    from: Id<TypeNode>,
72    target: Id<TypeNode>,
73    seen: &mut Vec<Id<TypeNode>>,
74) -> bool {
75    for next in aggregate_successors(ast, from) {
76        if next == target {
77            return true;
78        }
79        if !seen.contains(&next) {
80            seen.push(next);
81            if reaches(ast, next, target, seen) {
82                return true;
83            }
84        }
85    }
86    false
87}
88
89/// The aggregates one step below `type_`: those its members or case
90/// members reach without passing through another aggregate.
91fn aggregate_successors(ast: &Ast, type_: Id<TypeNode>) -> Vec<Id<TypeNode>> {
92    let mut found = Vec::new();
93    let Ok(node) = ast.type_(type_) else {
94        return found;
95    };
96    match &node.kind {
97        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
98            for member in contract::members(ast, type_) {
99                collect_aggregates(ast, member.type_, &mut found, &mut Vec::new());
100            }
101        }
102        TypeKind::Union { .. } => {
103            for case in contract::union_cases(ast, type_) {
104                collect_aggregates(ast, case.type_, &mut found, &mut Vec::new());
105            }
106        }
107        _ => {}
108    }
109    found
110}
111
112/// The first aggregates below `type_`, walking through the connectors
113/// — aliases, sequences, arrays, maps — and stopping at each aggregate
114/// found.
115///
116/// zerodds-lint: recursion-depth = the number of connectors in the
117/// model. `open` holds the connectors this descent has already entered
118/// and one is never entered twice, so the depth cannot pass the count of
119/// distinct typedef, sequence, array and map nodes the model declares.
120///
121/// The guard is not decoration. A connector chain is a tree in a model
122/// written in IDL — a `sequence<sequence<long>>` is two nodes the parser
123/// made, and a typedef names a type that was already declared — but a
124/// typedef that resolves to itself is a chain with no end, and the
125/// frontend admits `typedef Tee Tee;` with no diagnostic. This function
126/// is the one every dump emitter asks *before* it can refuse a cycle, so
127/// it is the one place that cannot answer by running out of stack.
128/// `contract::reject_alias_cycle` refuses the same model at each
129/// emitter's entry; this is what holds when the function is called on
130/// its own.
131fn collect_aggregates(
132    ast: &Ast,
133    type_: Id<TypeNode>,
134    found: &mut Vec<Id<TypeNode>>,
135    open: &mut Vec<Id<TypeNode>>,
136) {
137    let Ok(node) = ast.type_(type_) else {
138        return;
139    };
140    if open.contains(&type_) {
141        // A connector already on this descent: following it again adds
142        // no aggregate that is not found by the first visit.
143        return;
144    }
145    open.push(type_);
146    match &node.kind {
147        TypeKind::Alias { aliased } => {
148            if let Some(target) = aliased.resolved() {
149                collect_aggregates(ast, target, found, open);
150            }
151        }
152        TypeKind::Sequence { element, .. } | TypeKind::Array { element, .. } => {
153            if let Some(target) = element.resolved() {
154                collect_aggregates(ast, target, found, open);
155            }
156        }
157        TypeKind::Map { key, value, .. } => {
158            if let Some(target) = key.resolved() {
159                collect_aggregates(ast, target, found, open);
160            }
161            if let Some(target) = value.resolved() {
162                collect_aggregates(ast, target, found, open);
163            }
164        }
165        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } | TypeKind::Union { .. } => {
166            if !found.contains(&type_) {
167                found.push(type_);
168            }
169        }
170        _ => {}
171    }
172}