Skip to main content

type_inference/
type_inference.rs

1//! Infers the Macaulay2 class of every cell in a file with node-specific `Visit` rules.
2//!
3//! The generated [`Visit`] implementation already walks the whole typed graph:
4//! every method defaults to the free walker that descends into that node's
5//! children. An analysis therefore overrides only the nodes it actually has a
6//! rule for, and each override calls its free walker first when it needs the
7//! facts of its operands. Nothing here dispatches on the shape of an expression
8//! by hand — `visit_integer_literal` runs because the parse produced an integer
9//! literal, exactly like `syn`'s `visit_expr_binary` runs on a binary Rust
10//! expression.
11//!
12//! Inference produces an upper bound rather than an exact answer: a rule records
13//! the most specific class it can prove, joining branches walks the class
14//! hierarchy up to a common ancestor, and every unmodelled expression stays
15//! `Thing`, the ancestor of everything in Macaulay2. Storage is the flat
16//! span-keyed table an analysis or LSP layer would own; the syntax crate only
17//! supplies the traversal.
18
19use std::collections::HashMap;
20use std::fmt;
21use std::ops::Range;
22
23use m2_syn::nodes::{
24    AngleBarList, Array, Cell, ExprAssignSimpleGlobal, ExprAssignSimpleLocal, ExprIf,
25    ExprOpBinRegular, ExprOpPrefix, FloatLiteral, Holder, IntegerLiteral, List, PackedContent,
26    RawStringLiteral, Sequence, StringLiteral, Symbol,
27};
28use m2_syn::visit::{self, Visit};
29use m2_syn::{Lex, Offset, ParseError, Spanned, parse_file};
30
31/// A class of the Macaulay2 hierarchy, read as an upper bound: an expression
32/// inferred as `class` evaluates to `class` or to one of its descendants.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34enum M2Class {
35    /// Integers.
36    ZZ,
37    /// Rationals.
38    QQ,
39    /// Real numbers.
40    RR,
41    /// The inexact numbers, the common ancestor of the floating point classes.
42    InexactNumber,
43    /// The numbers, the common ancestor of every numeric class.
44    Number,
45    /// `true` and `false`.
46    Boolean,
47    /// Strings.
48    String,
49    /// The nets, the common ancestor of the printable text classes.
50    Net,
51    /// The class of `null`, the value of a suppressed or missing expression.
52    Nothing,
53    /// Parenthesized comma packs.
54    Sequence,
55    /// Brace lists.
56    List,
57    /// Bracket lists.
58    Array,
59    /// Angle bar lists.
60    AngleBarList,
61    /// The visible lists, the common ancestor of the four bracketing classes.
62    VisibleList,
63    /// The basic lists, the common ancestor of every list-like class.
64    BasicList,
65    /// The ancestor of everything, inferred where no rule applies.
66    Thing,
67}
68
69impl M2Class {
70    /// The M2 promotion tower `ZZ` ⊂ `QQ` ⊂ `RR`, ordered by widening.
71    const PROMOTION_TOWER: [Self; 3] = [Self::ZZ, Self::QQ, Self::RR];
72
73    /// Returns the class this one inherits from, or `None` for `Thing`.
74    fn parent(self) -> Option<Self> {
75        Some(match self {
76            Self::ZZ | Self::QQ | Self::InexactNumber => Self::Number,
77            Self::RR => Self::InexactNumber,
78            Self::String => Self::Net,
79            Self::Sequence | Self::List | Self::Array | Self::AngleBarList => Self::VisibleList,
80            Self::VisibleList => Self::BasicList,
81            Self::Number | Self::Boolean | Self::Net | Self::Nothing | Self::BasicList => {
82                Self::Thing
83            }
84            Self::Thing => return None,
85        })
86    }
87
88    /// Returns this class and its ancestors, ending at `Thing`.
89    fn ancestry(self) -> impl Iterator<Item = Self> {
90        std::iter::successors(Some(self), |class| class.parent())
91    }
92
93    /// Reports whether `descendant` is this class or inherits from it.
94    fn covers(self, descendant: Self) -> bool {
95        descendant.ancestry().any(|class| class == self)
96    }
97
98    /// Returns the most specific class covering both, their common ancestor.
99    fn join(self, other: Self) -> Self {
100        self.ancestry()
101            .find(|class| class.covers(other))
102            .expect("`Thing` covers every class")
103    }
104
105    /// Returns the class an arithmetic operator produces from two operands,
106    /// widening along the promotion tower.
107    fn promoted_with(self, other: Self) -> Self {
108        let position = |class| Self::PROMOTION_TOWER.iter().position(|&rung| rung == class);
109        match (position(self), position(other)) {
110            (Some(left), Some(right)) if left >= right => self,
111            (Some(_), Some(_)) => other,
112            _ => Self::Thing,
113        }
114    }
115}
116
117impl fmt::Display for M2Class {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(formatter, "{self:?}")
120    }
121}
122
123/// The inferred class of every node a rule could prove something about, keyed by
124/// the source range the node covers.
125#[derive(Default)]
126struct TypeInference {
127    /// The proven classes.
128    classes: HashMap<Range<Offset>, M2Class>,
129}
130
131impl TypeInference {
132    /// Stores the class proven for one node.
133    fn record(&mut self, node: &impl Spanned, class: M2Class) {
134        if let Some(range) = node.try_span().and_then(|span| span.range()) {
135            self.classes.insert(range, class);
136        }
137    }
138
139    /// Returns the class proven for one node, `Thing` when no rule proved one.
140    fn inferred(&self, node: &impl Spanned) -> M2Class {
141        node.try_span()
142            .and_then(|span| span.range())
143            .and_then(|range| self.classes.get(&range).copied())
144            .unwrap_or(M2Class::Thing)
145    }
146}
147
148impl<'ast> Visit<'ast> for TypeInference {
149    fn visit_integer_literal(&mut self, node: &'ast IntegerLiteral) {
150        self.record(node, M2Class::ZZ);
151    }
152
153    fn visit_float_literal(&mut self, node: &'ast FloatLiteral) {
154        self.record(node, M2Class::RR);
155    }
156
157    fn visit_string_literal(&mut self, node: &'ast StringLiteral) {
158        self.record(node, M2Class::String);
159    }
160
161    fn visit_raw_string_literal(&mut self, node: &'ast RawStringLiteral) {
162        self.record(node, M2Class::String);
163    }
164
165    fn visit_symbol(&mut self, node: &'ast Symbol) {
166        // Only the built-in constants are known without a symbol table; every
167        // other name keeps the `Thing` bound the analysis layer would refine.
168        match node.text() {
169            "true" | "false" => self.record(node, M2Class::Boolean),
170            "null" => self.record(node, M2Class::Nothing),
171            _ => {}
172        }
173    }
174
175    fn visit_sequence(&mut self, node: &'ast Sequence) {
176        visit::visit_sequence(self, node);
177        self.record(node, M2Class::Sequence);
178    }
179
180    fn visit_list(&mut self, node: &'ast List) {
181        visit::visit_list(self, node);
182        self.record(node, M2Class::List);
183    }
184
185    fn visit_array(&mut self, node: &'ast Array) {
186        visit::visit_array(self, node);
187        self.record(node, M2Class::Array);
188    }
189
190    fn visit_angle_bar_list(&mut self, node: &'ast AngleBarList) {
191        visit::visit_angle_bar_list(self, node);
192        self.record(node, M2Class::AngleBarList);
193    }
194
195    fn visit_holder(&mut self, node: &'ast Holder) {
196        visit::visit_holder(self, node);
197        let held = match &node.value {
198            Some(value) => self.inferred(value.as_ref()),
199            // `()` is the empty sequence, while `(f x;)` discards its value.
200            None if node.muted.is_empty() => M2Class::Sequence,
201            None => M2Class::Nothing,
202        };
203        self.record(node, held);
204    }
205
206    fn visit_expr_assign_simple_global(&mut self, node: &'ast ExprAssignSimpleGlobal) {
207        visit::visit_expr_assign_simple_global(self, node);
208        self.record(node, self.inferred(node.value.as_ref()));
209    }
210
211    fn visit_expr_assign_simple_local(&mut self, node: &'ast ExprAssignSimpleLocal) {
212        visit::visit_expr_assign_simple_local(self, node);
213        self.record(node, self.inferred(node.value.as_ref()));
214    }
215
216    fn visit_expr_op_prefix(&mut self, node: &'ast ExprOpPrefix) {
217        visit::visit_expr_op_prefix(self, node);
218        let operand = self.inferred(node.operand.as_ref());
219        let result = match node.operator.spelling() {
220            "not" => M2Class::Boolean,
221            "+" | "-" => operand,
222            _ => M2Class::Thing,
223        };
224        self.record(node, result);
225    }
226
227    fn visit_expr_op_bin_regular(&mut self, node: &'ast ExprOpBinRegular) {
228        visit::visit_expr_op_bin_regular(self, node);
229        let left = self.inferred(node.left_operand.as_ref());
230        let right = self.inferred(node.right_operand.as_ref());
231        let result = match node.operator.spelling() {
232            "==" | "!=" | "===" | "=!=" | "<" | "<=" | ">" | ">=" | "and" | "or" | "xor" => {
233                M2Class::Boolean
234            }
235            "+" | "-" | "*" | "%" | "//" => left.promoted_with(right),
236            // Exact division leaves the integers even when both operands are `ZZ`.
237            "/" => left.promoted_with(right).promoted_with(M2Class::QQ),
238            ".." | "..<" => M2Class::Sequence,
239            // Concatenation of two texts or two lists, bitwise `or` on integers.
240            "|" => left.join(right),
241            _ => M2Class::Thing,
242        };
243        self.record(node, result);
244    }
245
246    fn visit_expr_if(&mut self, node: &'ast ExprIf) {
247        visit::visit_expr_if(self, node);
248        let taken = self.inferred(node.then_clause.expr.as_ref());
249        let untaken = node
250            .else_clause
251            .as_ref()
252            .map_or(M2Class::Nothing, |clause| {
253                self.inferred(clause.expr.as_ref())
254            });
255        self.record(node, taken.join(untaken));
256    }
257
258    fn visit_cell(&mut self, node: &'ast Cell) {
259        visit::visit_cell(self, node);
260        let value = if node.semicolon.is_some() {
261            // A trailing `;` suppresses the cell's value.
262            M2Class::Nothing
263        } else {
264            match &node.content {
265                PackedContent::Expr(expression) => self.inferred(expression.as_ref()),
266                PackedContent::Pack(_) => M2Class::Sequence,
267            }
268        };
269        self.record(node, value);
270    }
271}
272
273/// Source exercising one cell per inference rule.
274const DEMONSTRATED_CELLS: &str = r#"6
2751.5
2766 / 4
2776 + 1.5
278"an ideal " | "of a ring"
279answer := 6 * 7
280{1, 2, 3}
281(1, 2)
282[0, 1]
283<|0, 1|>
2840 .. 5
285not (6 < 7)
286(6; 7)
2876;
288if isPrime n then 6 else 1.5
289if isPrime n then "prime"
290resolution someModule
291"#;
292
293fn main() -> Result<(), ParseError> {
294    let file = parse_file(DEMONSTRATED_CELLS)?;
295    let mut inference = TypeInference::default();
296    inference.visit_source_file(&file);
297
298    for cell in &file.cells {
299        println!(
300            "{:<34} => {}",
301            cell.to_m2().trim_end(),
302            inference.inferred(cell)
303        );
304    }
305    Ok(())
306}
307
308#[cfg(test)]
309mod tests {
310    use super::{M2Class, TypeInference};
311    use m2_syn::parse_file;
312    use m2_syn::visit::Visit;
313
314    fn inferred_class(source: &str) -> M2Class {
315        let file = parse_file(source).expect("the demonstrated sources parse");
316        let [cell] = file.cells.as_slice() else {
317            panic!("expected exactly one cell");
318        };
319        let mut inference = TypeInference::default();
320        inference.visit_source_file(&file);
321        inference.inferred(cell)
322    }
323
324    #[test]
325    fn infers_the_class_macaulay2_would_report() {
326        for (source, expected) in [
327            ("6", M2Class::ZZ),
328            ("1.5", M2Class::RR),
329            ("6 / 4", M2Class::QQ),
330            ("6 // 4", M2Class::ZZ),
331            ("6 + 1.5", M2Class::RR),
332            (r#""an ideal " | "of a ring""#, M2Class::String),
333            ("answer := 6 * 7", M2Class::ZZ),
334            ("answer = 6 * 7", M2Class::ZZ),
335            ("{1, 2, 3}", M2Class::List),
336            ("(1, 2)", M2Class::Sequence),
337            ("[0, 1]", M2Class::Array),
338            ("<|0, 1|>", M2Class::AngleBarList),
339            ("0 .. 5", M2Class::Sequence),
340            ("not (6 < 7)", M2Class::Boolean),
341            ("(6)", M2Class::ZZ),
342            ("()", M2Class::Sequence),
343            ("(6; 7)", M2Class::ZZ),
344            ("6;", M2Class::Nothing),
345            ("null", M2Class::Nothing),
346        ] {
347            assert_eq!(inferred_class(source), expected, "{source}");
348        }
349    }
350
351    #[test]
352    fn branches_and_unmodelled_expressions_infer_an_ancestor_bound() {
353        for (source, expected) in [
354            ("if isPrime n then 6 else 7", M2Class::ZZ),
355            ("if isPrime n then 6 else 1.5", M2Class::Number),
356            ("if isPrime n then (1, 2) else {3}", M2Class::VisibleList),
357            ("if isPrime n then 6", M2Class::Thing),
358            ("resolution someModule", M2Class::Thing),
359            ("6 + someRing", M2Class::Thing),
360        ] {
361            assert_eq!(inferred_class(source), expected, "{source}");
362        }
363    }
364}