plotnik_lib/query/
infer_dump.rs

1//! Dump helpers for type inference inspection and testing.
2
3use std::fmt::Write;
4
5use crate::ir::{TYPE_NODE, TYPE_STR, TYPE_VOID, TypeId, TypeKind};
6
7use super::infer::TypeInferenceResult;
8
9impl TypeInferenceResult<'_> {
10    pub fn dump(&self) -> String {
11        let mut out = String::new();
12        let printer = TypePrinter::new(self);
13        printer.format(&mut out).expect("String write never fails");
14        out
15    }
16
17    pub fn dump_diagnostics(&self, source: &str) -> String {
18        self.diagnostics.render_filtered(source)
19    }
20
21    pub fn has_errors(&self) -> bool {
22        !self.errors.is_empty()
23    }
24}
25
26struct TypePrinter<'a, 'src> {
27    result: &'a TypeInferenceResult<'src>,
28    width: usize,
29}
30
31impl<'a, 'src> TypePrinter<'a, 'src> {
32    fn new(result: &'a TypeInferenceResult<'src>) -> Self {
33        let total_types = 3 + result.type_defs.len();
34        let width = if total_types == 0 {
35            1
36        } else {
37            ((total_types as f64).log10().floor() as usize) + 1
38        };
39        Self { result, width }
40    }
41
42    fn format(&self, w: &mut String) -> std::fmt::Result {
43        // Entrypoints (skip redundant Foo = Foo)
44        for (name, type_id) in &self.result.entrypoint_types {
45            let type_name = self.get_type_name(*type_id);
46            if type_name.as_deref() == Some(*name) {
47                continue;
48            }
49            writeln!(w, "{} = {}", name, self.format_type(*type_id))?;
50        }
51
52        let has_entrypoints = self
53            .result
54            .entrypoint_types
55            .iter()
56            .any(|(name, id)| self.get_type_name(*id).as_deref() != Some(*name));
57
58        // Type definitions (skip inlinable types)
59        let mut first_typedef = true;
60        for (idx, def) in self.result.type_defs.iter().enumerate() {
61            let type_id = 3 + idx as TypeId;
62
63            if self.is_inlinable(type_id) {
64                continue;
65            }
66
67            if first_typedef && has_entrypoints {
68                writeln!(w)?;
69            }
70            first_typedef = false;
71
72            let header = self.format_type_header(type_id, def.name);
73
74            match def.kind {
75                TypeKind::Record => {
76                    if def.members.len() == 1 {
77                        let m = &def.members[0];
78                        writeln!(
79                            w,
80                            "{} = {{ {}: {} }}",
81                            header,
82                            m.name,
83                            self.format_type(m.ty)
84                        )?;
85                    } else {
86                        writeln!(w, "{} = {{", header)?;
87                        for member in &def.members {
88                            writeln!(w, "  {}: {}", member.name, self.format_type(member.ty))?;
89                        }
90                        writeln!(w, "}}")?;
91                    }
92                }
93                TypeKind::Enum => {
94                    if def.members.len() == 1 {
95                        let m = &def.members[0];
96                        writeln!(
97                            w,
98                            "{} = {{ {} => {} }}",
99                            header,
100                            m.name,
101                            self.format_type(m.ty)
102                        )?;
103                    } else {
104                        writeln!(w, "{} = {{", header)?;
105                        for member in &def.members {
106                            writeln!(w, "  {} => {}", member.name, self.format_type(member.ty))?;
107                        }
108                        writeln!(w, "}}")?;
109                    }
110                }
111                TypeKind::Optional => {
112                    let inner = def
113                        .inner_type
114                        .map(|t| self.format_type(t))
115                        .unwrap_or_default();
116                    writeln!(w, "{} = {}?", header, inner)?;
117                }
118                TypeKind::ArrayStar => {
119                    let inner = def
120                        .inner_type
121                        .map(|t| self.format_type(t))
122                        .unwrap_or_default();
123                    writeln!(w, "{} = [{}]", header, inner)?;
124                }
125                TypeKind::ArrayPlus => {
126                    let inner = def
127                        .inner_type
128                        .map(|t| self.format_type(t))
129                        .unwrap_or_default();
130                    writeln!(w, "{} = [{}]⁺", header, inner)?;
131                }
132            }
133        }
134
135        // Errors
136        if !self.result.errors.is_empty() {
137            if has_entrypoints || !first_typedef {
138                writeln!(w)?;
139            }
140            writeln!(w, "Errors:")?;
141            for err in &self.result.errors {
142                let types = err
143                    .types_found
144                    .iter()
145                    .map(|t| t.to_string())
146                    .collect::<Vec<_>>()
147                    .join(", ");
148                writeln!(
149                    w,
150                    "  field `{}` in `{}`: incompatible types [{}]",
151                    err.field, err.definition, types
152                )?;
153            }
154        }
155
156        Ok(())
157    }
158
159    fn get_type_name(&self, id: TypeId) -> Option<String> {
160        if id < 3 {
161            return None;
162        }
163        let idx = (id - 3) as usize;
164        self.result
165            .type_defs
166            .get(idx)
167            .and_then(|def| def.name.map(|s| s.to_string()))
168    }
169
170    /// Returns true if the type should be inlined rather than shown as separate definition.
171    /// Inlinable: wrapper types (Optional/Array*) around primitives or other inlinable types.
172    fn is_inlinable(&self, id: TypeId) -> bool {
173        if id < 3 {
174            return true; // primitives are always inlinable
175        }
176        let idx = (id - 3) as usize;
177        let Some(def) = self.result.type_defs.get(idx) else {
178            return false;
179        };
180
181        // Named types are not inlined (they have semantic meaning)
182        if def.name.is_some() {
183            return false;
184        }
185
186        match def.kind {
187            TypeKind::Record | TypeKind::Enum => false,
188            TypeKind::Optional | TypeKind::ArrayStar | TypeKind::ArrayPlus => {
189                def.inner_type.map(|t| self.is_inlinable(t)).unwrap_or(true)
190            }
191        }
192    }
193
194    fn format_type_header(&self, type_id: TypeId, name: Option<&str>) -> String {
195        match name {
196            Some(n) => n.to_string(),
197            None => format!("T{:0width$}", type_id, width = self.width),
198        }
199    }
200
201    fn format_type(&self, id: TypeId) -> String {
202        match id {
203            TYPE_VOID => "()".to_string(),
204            TYPE_NODE => "Node".to_string(),
205            TYPE_STR => "str".to_string(),
206            _ => {
207                let idx = (id - 3) as usize;
208                if let Some(def) = self.result.type_defs.get(idx) {
209                    // Named types: use name
210                    if let Some(name) = def.name {
211                        return name.to_string();
212                    }
213
214                    // Inlinable wrappers: format inline
215                    if self.is_inlinable(id) {
216                        let inner = def
217                            .inner_type
218                            .map(|t| self.format_type(t))
219                            .unwrap_or_default();
220                        return match def.kind {
221                            TypeKind::Optional => format!("{}?", inner),
222                            TypeKind::ArrayStar => format!("[{}]", inner),
223                            TypeKind::ArrayPlus => format!("[{}]⁺", inner),
224                            _ => format!("T{:0width$}", id, width = self.width),
225                        };
226                    }
227                }
228                format!("T{:0width$}", id, width = self.width)
229            }
230        }
231    }
232}