Skip to main content

vexil_codegen_rust/
boxing.rs

1use std::collections::HashSet;
2use vexil_lang::ir::{CompiledSchema, ResolvedType, TypeDef, TypeId};
3
4/// Returns set of (type_id, field_index) pairs that need `Box<T>` wrapping.
5pub fn detect_boxing(compiled: &CompiledSchema) -> HashSet<(TypeId, usize)> {
6    let mut needs_box = HashSet::new();
7    for &id in &compiled.declarations {
8        let mut path = Vec::new();
9        path.push(id);
10        match compiled.registry.get(id) {
11            Some(TypeDef::Message(msg)) => {
12                for (fi, field) in msg.fields.iter().enumerate() {
13                    walk_for_boxing(
14                        &field.resolved_type,
15                        id,
16                        fi,
17                        &path,
18                        compiled,
19                        &mut needs_box,
20                    );
21                }
22            }
23            Some(TypeDef::Union(un)) => {
24                for variant in &un.variants {
25                    for (fi, field) in variant.fields.iter().enumerate() {
26                        walk_for_boxing(
27                            &field.resolved_type,
28                            id,
29                            fi,
30                            &path,
31                            compiled,
32                            &mut needs_box,
33                        );
34                    }
35                }
36            }
37            _ => {}
38        }
39    }
40    needs_box
41}
42
43fn walk_for_boxing(
44    ty: &ResolvedType,
45    parent_id: TypeId,
46    field_index: usize,
47    path: &[TypeId],
48    compiled: &CompiledSchema,
49    needs_box: &mut HashSet<(TypeId, usize)>,
50) {
51    match ty {
52        ResolvedType::Optional(inner) => {
53            check_inner_for_cycle(inner, parent_id, field_index, path, compiled, needs_box);
54        }
55        ResolvedType::Result(ok, err) => {
56            check_inner_for_cycle(ok, parent_id, field_index, path, compiled, needs_box);
57            check_inner_for_cycle(err, parent_id, field_index, path, compiled, needs_box);
58        }
59        ResolvedType::Named(id) => {
60            if path.contains(id) {
61                // Direct cycle through a Named type — this field must be boxed
62                // to break infinite struct size (e.g. ExprKind::Binary { left: Expr })
63                needs_box.insert((parent_id, field_index));
64                return;
65            }
66            let mut new_path = path.to_vec();
67            new_path.push(*id);
68            match compiled.registry.get(*id) {
69                Some(TypeDef::Message(msg)) => {
70                    for (fi, field) in msg.fields.iter().enumerate() {
71                        walk_for_boxing(
72                            &field.resolved_type,
73                            *id,
74                            fi,
75                            &new_path,
76                            compiled,
77                            needs_box,
78                        );
79                    }
80                }
81                Some(TypeDef::Union(un)) => {
82                    for variant in &un.variants {
83                        for (fi, field) in variant.fields.iter().enumerate() {
84                            walk_for_boxing(
85                                &field.resolved_type,
86                                *id,
87                                fi,
88                                &new_path,
89                                compiled,
90                                needs_box,
91                            );
92                        }
93                    }
94                }
95                _ => {}
96            }
97        }
98        ResolvedType::Array(_) | ResolvedType::Map(_, _) => {
99            // Heap-allocated containers — no boxing needed through them
100        }
101        _ => {} // Primitive, SubByte, Semantic — terminal
102    }
103}
104
105fn check_inner_for_cycle(
106    ty: &ResolvedType,
107    parent_id: TypeId,
108    field_index: usize,
109    path: &[TypeId],
110    compiled: &CompiledSchema,
111    needs_box: &mut HashSet<(TypeId, usize)>,
112) {
113    if let ResolvedType::Named(id) = ty {
114        if path.contains(id) {
115            needs_box.insert((parent_id, field_index));
116            return;
117        }
118    }
119    walk_for_boxing(ty, parent_id, field_index, path, compiled, needs_box);
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    fn analyze(src: &str) -> HashSet<(TypeId, usize)> {
127        let result = vexil_lang::compile(src);
128        let compiled = result.compiled.unwrap();
129        detect_boxing(&compiled)
130    }
131
132    #[test]
133    fn no_recursion_no_boxing() {
134        let needs = analyze(
135            r#"
136            namespace test.box
137            message Simple { name @0 : string }
138        "#,
139        );
140        assert!(needs.is_empty());
141    }
142
143    #[test]
144    fn optional_self_reference_needs_box() {
145        let needs = analyze(
146            r#"
147            namespace test.box
148            message Node {
149                value @0 : i32
150                next  @1 : optional<Node>
151            }
152        "#,
153        );
154        assert!(!needs.is_empty());
155    }
156
157    #[test]
158    fn mutual_recursion_needs_box() {
159        let needs = analyze(
160            r#"
161            namespace test.box
162            message Expr {
163                kind @0 : ExprKind
164            }
165            union ExprKind {
166                Literal @0 { value @0 : i64 }
167                Binary  @1 { left @0 : Expr  op @1 : u8  right @2 : Expr }
168            }
169        "#,
170        );
171        assert!(!needs.is_empty());
172    }
173
174    #[test]
175    fn array_self_reference_no_box() {
176        let needs = analyze(
177            r#"
178            namespace test.box
179            message Tree {
180                value    @0 : i32
181                children @1 : array<Tree>
182            }
183        "#,
184        );
185        assert!(needs.is_empty());
186    }
187}