1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34enum M2Class {
35 ZZ,
37 QQ,
39 RR,
41 InexactNumber,
43 Number,
45 Boolean,
47 String,
49 Net,
51 Nothing,
53 Sequence,
55 List,
57 Array,
59 AngleBarList,
61 VisibleList,
63 BasicList,
65 Thing,
67}
68
69impl M2Class {
70 const PROMOTION_TOWER: [Self; 3] = [Self::ZZ, Self::QQ, Self::RR];
72
73 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 fn ancestry(self) -> impl Iterator<Item = Self> {
90 std::iter::successors(Some(self), |class| class.parent())
91 }
92
93 fn covers(self, descendant: Self) -> bool {
95 descendant.ancestry().any(|class| class == self)
96 }
97
98 fn join(self, other: Self) -> Self {
100 self.ancestry()
101 .find(|class| class.covers(other))
102 .expect("`Thing` covers every class")
103 }
104
105 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#[derive(Default)]
126struct TypeInference {
127 classes: HashMap<Range<Offset>, M2Class>,
129}
130
131impl TypeInference {
132 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 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 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 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 "/" => left.promoted_with(right).promoted_with(M2Class::QQ),
238 ".." | "..<" => M2Class::Sequence,
239 "|" => 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 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
273const 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}