Skip to main content

ucglib/ast/typecheck/
mod.rs

1// Copyright 2020 Jeremy Wall
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Implements typechecking for the parsed ucg AST.
16use std::cell::RefCell;
17use std::collections::BTreeMap;
18use std::path::PathBuf;
19use std::rc::Rc;
20
21use crate::ast::walk::{Visitor, Walker};
22use crate::ast::{
23    Expression, FailDef, FuncShapeDef, ImportDef, IncludeDef, Shape, Statement, Value,
24};
25use crate::error::{BuildError, ErrorType};
26use crate::iter::OffsetStrIter;
27use crate::parse::parse;
28
29use super::{
30    BinaryExprType, BinaryOpDef, CallDef, CastType, CopyDef, FuncDef, FuncOpDef, ImportShape,
31    MapFilterOpDef, ModuleDef, ModuleShape, NarrowedShape, NarrowingShape, NotDef, Position,
32    PositionedItem, ReduceOpDef, SelectDef, TupleShape,
33};
34
35/// Trait for shape derivation.
36pub trait DeriveShape {
37    /// Derive a shape using a provided symbol table.
38    fn derive_shape(&self, symbol_table: &mut BTreeMap<Rc<str>, Shape>) -> Shape;
39}
40
41impl DeriveShape for FuncDef {
42    fn derive_shape(&self, symbol_table: &mut BTreeMap<Rc<str>, Shape>) -> Shape {
43        // 1. First set up our symbols.
44        let mut sym_table = self
45            .argdefs
46            .iter()
47            .map(|(sym, constraint)| {
48                let shape = if let Some(c) = constraint {
49                    c.derive_shape(symbol_table)
50                } else {
51                    Shape::Hole(sym.clone())
52                };
53                (sym.val.clone(), shape)
54            })
55            .collect::<BTreeMap<Rc<str>, Shape>>();
56        sym_table.append(&mut symbol_table.clone());
57        // 2. Then determine the shapes of those symbols in our expression.
58        let shape = self.fields.derive_shape(&mut sym_table);
59        // 3. Finally determine what the return shape can be.
60        // only include the closed over shapes.
61        let table = self
62            .argdefs
63            .iter()
64            .map(|(sym, _constraint)| {
65                (
66                    sym.val.clone(),
67                    sym_table
68                        .get(&sym.val)
69                        .unwrap()
70                        .clone()
71                        .with_pos(sym.pos.clone()),
72                )
73            })
74            .collect::<BTreeMap<Rc<str>, Shape>>();
75        Shape::Func(FuncShapeDef {
76            args: table,
77            ret: shape.with_pos(self.pos.clone()).into(),
78        })
79    }
80}
81
82impl DeriveShape for ModuleDef {
83    fn derive_shape(&self, symbol_table: &mut BTreeMap<Rc<str>, Shape>) -> Shape {
84        let mut sym_table: BTreeMap<Rc<str>, Shape> = BTreeMap::new();
85        let mod_key: Rc<str> = "mod".into();
86        let mut mod_shape: TupleShape = Vec::new();
87        if let Some(pos) = self.arg_set.first().map(|(t, _, _)| t.pos.clone()) {
88            mod_shape = self
89                .arg_set
90                .iter()
91                .map(|(tok, _constraint, expr)| {
92                    let default_shape = expr.derive_shape(symbol_table);
93                    let shape = match &default_shape {
94                        // Empty tuple default means "any tuple" - treat as unconstrained
95                        Shape::Tuple(pi) if pi.val.is_empty() => Shape::Narrowed(NarrowedShape {
96                            pos: tok.pos.clone(),
97                            types: NarrowingShape::Any,
98                        }),
99                        _ => default_shape,
100                    };
101                    (tok.into(), shape)
102                })
103                .collect();
104            sym_table.insert(
105                mod_key.clone(),
106                Shape::Tuple(PositionedItem::new(mod_shape.clone(), pos)),
107            );
108        }
109        // TODO(jwall): We should modify the shape_list when we can continue narrowing a type.
110        let mut checker = Checker::new().with_symbol_table(sym_table);
111        checker.walk_statement_list(self.statements.clone().iter_mut().collect());
112        // Derive the out expression shape directly instead of using walk_expression,
113        // since DeriveShape already recurses through expression children.
114        let mut ret = if let Some(out_expr) = &self.out_expr {
115            Box::new(out_expr.derive_shape(&mut checker.symbol_table))
116        } else {
117            Box::new(
118                checker
119                    .pop_shape()
120                    .unwrap_or(Shape::Narrowed(NarrowedShape {
121                        pos: self.pos.clone(),
122                        types: NarrowingShape::Any,
123                    })),
124            )
125        };
126        // Enforce output constraint if present
127        if let Some(ref constraint_expr) = self.out_constraint {
128            let constraint_shape = constraint_expr.derive_shape(symbol_table);
129            let narrowed = ret.narrow(&constraint_shape, symbol_table);
130            if let Shape::TypeErr(_, _) = &narrowed {
131                ret = Box::new(narrowed);
132            } else {
133                ret = Box::new(narrowed);
134            }
135        }
136        // Read back narrowed arg shapes from the checker's symbol table
137        if let Some(Shape::Tuple(mod_tuple)) = checker.symbol_table.get(&mod_key) {
138            mod_shape = mod_tuple.val.clone();
139        }
140        Shape::Module(ModuleShape {
141            items: mod_shape,
142            ret,
143        })
144    }
145}
146
147impl DeriveShape for SelectDef {
148    fn derive_shape(&self, symbol_table: &mut BTreeMap<Rc<str>, Shape>) -> Shape {
149        let SelectDef {
150            val: _,
151            default: _,
152            tuple,
153            pos: _,
154        } = self;
155        let mut narrowed_shape =
156            NarrowedShape::new_with_pos(Vec::with_capacity(tuple.len()), self.pos.clone());
157        for (_, _constraint, expr) in tuple {
158            let shape = expr.derive_shape(symbol_table);
159            narrowed_shape.merge_in_shape(shape, symbol_table);
160        }
161        Shape::Narrowed(narrowed_shape)
162    }
163}
164
165fn derive_include_shape(
166    IncludeDef {
167        pos,
168        path: _path,
169        typ: _typ,
170    }: &IncludeDef,
171) -> Shape {
172    Shape::Narrowed(NarrowedShape::new_with_pos(
173        vec![
174            Shape::Tuple(PositionedItem::new(vec![], pos.clone())),
175            Shape::List(NarrowedShape::new_with_pos(vec![], pos.clone())),
176        ],
177        pos.clone(),
178    ))
179}
180
181fn derive_not_shape(def: &NotDef, symbol_table: &mut BTreeMap<Rc<str>, Shape>) -> Shape {
182    let shape = def.expr.as_ref().derive_shape(symbol_table);
183    match &shape {
184        Shape::Boolean(_) => {
185            return Shape::Boolean(def.pos.clone());
186        }
187        Shape::Hole(_) => {
188            return Shape::Boolean(def.pos.clone());
189        }
190        Shape::Narrowed(NarrowedShape {
191            pos: _,
192            types: NarrowingShape::Any,
193        }) => {
194            return Shape::Boolean(def.pos.clone());
195        }
196        Shape::Narrowed(NarrowedShape {
197            pos: _,
198            types: NarrowingShape::Narrowed(shape_list),
199        }) => {
200            for s in shape_list.iter() {
201                if let Shape::Boolean(_) = s {
202                    return Shape::Boolean(def.pos.clone());
203                }
204            }
205        }
206        _ => {
207            // noop
208        }
209    }
210    return Shape::TypeErr(
211        def.pos.clone(),
212        format!(
213            "Expected Boolean value in Not expression but got: {:?}",
214            shape
215        ),
216    );
217}
218
219fn derive_copy_shape(def: &CopyDef, symbol_table: &mut BTreeMap<Rc<str>, Shape>) -> Shape {
220    let base_shape = def.selector.derive_shape(symbol_table);
221    match &base_shape {
222        // TODO(jwall): Should we allow a stack of these?
223        Shape::TypeErr(_, _) => base_shape,
224        Shape::Boolean(_)
225        | Shape::Int(_)
226        | Shape::Float(_)
227        | Shape::Str(_)
228        | Shape::List(_)
229        | Shape::Func(_) => Shape::TypeErr(
230            def.pos.clone(),
231            format!("Not a Copyable type {}", base_shape.type_name()),
232        ),
233        // This is an interesting one. Do we assume tuple or module here?
234        Shape::Hole(pi) => Shape::Narrowed(NarrowedShape::new_with_pos(
235            vec![
236                Shape::Tuple(PositionedItem::new(vec![], pi.pos.clone())),
237                Shape::Module(ModuleShape {
238                    items: vec![],
239                    ret: Box::new(Shape::Narrowed(NarrowedShape::new_with_pos(
240                        vec![],
241                        pi.pos.clone(),
242                    ))),
243                }),
244                Shape::Import(ImportShape::Unresolved(pi.clone())),
245            ],
246            pi.pos.clone(),
247        )),
248        Shape::Narrowed(NarrowedShape {
249            pos: _,
250            types: NarrowingShape::Any,
251        }) => Shape::Narrowed(NarrowedShape::new_with_pos(
252            vec![
253                Shape::Tuple(PositionedItem {
254                    pos: def.pos.clone(),
255                    val: Vec::new(),
256                }),
257                Shape::Module(ModuleShape {
258                    items: vec![],
259                    ret: Box::new(Shape::Narrowed(NarrowedShape {
260                        pos: def.pos.clone(),
261                        types: NarrowingShape::Any,
262                    })),
263                }),
264            ],
265            def.pos.clone(),
266        )),
267        Shape::Narrowed(NarrowedShape {
268            pos: _,
269            types: NarrowingShape::Narrowed(potentials),
270        }) => {
271            // 1. Do the possible shapes include tuple, module, or import?
272            let filtered = potentials
273                .iter()
274                .filter_map(|v| match v {
275                    Shape::Tuple(_) | Shape::Module(_) | Shape::Import(_) | Shape::Hole(_) => {
276                        Some(v.clone())
277                    }
278                    _ => None,
279                })
280                .collect::<Vec<Shape>>();
281            if !filtered.is_empty() {
282                //  1.1 Then return those and strip the others.
283                Shape::Narrowed(NarrowedShape::new_with_pos(filtered, def.pos.clone()))
284            } else {
285                // 2. Else return a type error
286                Shape::TypeErr(
287                    def.pos.clone(),
288                    format!("Not a Copyable type {}", base_shape.type_name()),
289                )
290            }
291        }
292        // These have understandable ways to resolve the type.
293        Shape::Module(mdef) => {
294            let arg_fields = def
295                .fields
296                .iter()
297                .map(|(tok, _constraint, expr)| {
298                    (tok.fragment.clone(), expr.derive_shape(symbol_table))
299                })
300                .collect::<BTreeMap<Rc<str>, Shape>>();
301            // 1. Do our copyable fields have the right names and shapes based on mdef.items.
302            for (sym, shape) in mdef.items.iter() {
303                if let Some(s) = arg_fields.get(&sym.val) {
304                    if let Shape::TypeErr(pos, msg) = shape.narrow(s, symbol_table) {
305                        return Shape::TypeErr(pos, msg);
306                    }
307                }
308            }
309            //  1.1 If so then return the ret as our shape.
310            mdef.ret.as_ref().clone()
311        }
312        Shape::Tuple(t_def) => {
313            let mut base_fields = t_def.clone();
314            base_fields.val.extend(
315                def.fields
316                    .iter()
317                    .map(|(tok, _constraint, expr)| (tok.into(), expr.derive_shape(symbol_table))),
318            );
319            Shape::Tuple(base_fields).with_pos(def.pos.clone())
320        }
321        Shape::Import(ImportShape::Unresolved(_)) => Shape::Narrowed(NarrowedShape::new_with_pos(
322            vec![Shape::Tuple(PositionedItem::new(vec![], def.pos.clone()))],
323            def.pos.clone(),
324        )),
325        Shape::Import(ImportShape::Resolved(_, tuple_shape)) => {
326            let mut base_fields = tuple_shape.clone();
327            base_fields.extend(
328                def.fields
329                    .iter()
330                    .map(|(tok, _constraint, expr)| (tok.into(), expr.derive_shape(symbol_table))),
331            );
332            Shape::Tuple(PositionedItem::new(base_fields, def.pos.clone()))
333        }
334    }
335}
336
337fn derive_call_shape(def: &CallDef, symbol_table: &mut BTreeMap<Rc<str>, Shape>) -> Shape {
338    let func_shape = def.funcref.derive_shape(symbol_table);
339    match &func_shape {
340        Shape::Func(fdef) => {
341            // Check arg count
342            if fdef.args.len() != def.arglist.len() {
343                return Shape::TypeErr(
344                    def.pos.clone(),
345                    format!(
346                        "Function expects {} arguments but got {}",
347                        fdef.args.len(),
348                        def.arglist.len()
349                    ),
350                );
351            }
352            // Derive arg shapes for side effects (symbol narrowing)
353            // Note: We don't check arg types against the function's declared arg types
354            // because FuncShapeDef.args is a BTreeMap (alphabetical order) while call
355            // args are positional. The function body already constrains types internally.
356            for arg_expr in def.arglist.iter() {
357                arg_expr.derive_shape(symbol_table);
358            }
359            // Return the function's return type
360            fdef.ret.as_ref().clone()
361        }
362        Shape::Hole(_) => {
363            // Unknown function, derive arg shapes but return Any
364            for arg_expr in def.arglist.iter() {
365                arg_expr.derive_shape(symbol_table);
366            }
367            Shape::Narrowed(NarrowedShape {
368                pos: def.pos.clone(),
369                types: NarrowingShape::Any,
370            })
371        }
372        Shape::Narrowed(nshape) => {
373            match &nshape.types {
374                NarrowingShape::Any => {
375                    // Could be a function, derive arg shapes but return Any
376                    for arg_expr in def.arglist.iter() {
377                        arg_expr.derive_shape(symbol_table);
378                    }
379                    Shape::Narrowed(NarrowedShape {
380                        pos: def.pos.clone(),
381                        types: NarrowingShape::Any,
382                    })
383                }
384                NarrowingShape::Narrowed(types) => {
385                    // Filter to Func types and check each
386                    let func_types: Vec<&FuncShapeDef> = types
387                        .iter()
388                        .filter_map(|t| {
389                            if let Shape::Func(fdef) = t {
390                                Some(fdef)
391                            } else {
392                                None
393                            }
394                        })
395                        .collect();
396                    if func_types.is_empty() {
397                        return Shape::TypeErr(def.pos.clone(), "Not a callable type".to_owned());
398                    }
399                    // Derive arg shapes
400                    let arg_shapes: Vec<Shape> = def
401                        .arglist
402                        .iter()
403                        .map(|e| e.derive_shape(symbol_table))
404                        .collect();
405                    // Collect possible return types
406                    let mut ret_shapes = Vec::new();
407                    for fdef in func_types {
408                        if fdef.args.len() == arg_shapes.len() {
409                            ret_shapes.push(fdef.ret.as_ref().clone());
410                        }
411                    }
412                    if ret_shapes.is_empty() {
413                        Shape::TypeErr(
414                            def.pos.clone(),
415                            "No callable candidate matches argument count".to_owned(),
416                        )
417                    } else if ret_shapes.len() == 1 {
418                        ret_shapes.pop().unwrap()
419                    } else {
420                        Shape::Narrowed(NarrowedShape::new_with_pos(ret_shapes, def.pos.clone()))
421                    }
422                }
423            }
424        }
425        _ => Shape::TypeErr(
426            def.pos.clone(),
427            format!("Not a callable type: {}", func_shape.type_name()),
428        ),
429    }
430}
431
432fn derive_func_op_shape(def: &FuncOpDef, symbol_table: &mut BTreeMap<Rc<str>, Shape>) -> Shape {
433    match def {
434        FuncOpDef::Map(MapFilterOpDef { func, target, pos }) => {
435            let target_shape = target.derive_shape(symbol_table);
436            let func_shape = func.derive_shape(symbol_table);
437            // target must be a list
438            match &target_shape {
439                Shape::List(_) | Shape::Hole(_) => {}
440                Shape::Narrowed(NarrowedShape {
441                    types: NarrowingShape::Any,
442                    ..
443                }) => {}
444                _ => {
445                    return Shape::TypeErr(
446                        pos.clone(),
447                        format!(
448                            "map target must be a list, got {}",
449                            target_shape.type_name()
450                        ),
451                    );
452                }
453            }
454            // Return type is List(func.ret)
455            match &func_shape {
456                Shape::Func(fdef) => Shape::List(NarrowedShape::new_with_pos(
457                    vec![fdef.ret.as_ref().clone()],
458                    pos.clone(),
459                )),
460                _ => Shape::List(NarrowedShape {
461                    pos: pos.clone(),
462                    types: NarrowingShape::Any,
463                }),
464            }
465        }
466        FuncOpDef::Filter(MapFilterOpDef { func, target, pos }) => {
467            let target_shape = target.derive_shape(symbol_table);
468            let _func_shape = func.derive_shape(symbol_table);
469            // target must be a list, return type is same list type
470            match &target_shape {
471                Shape::List(_) => target_shape,
472                Shape::Hole(_) => Shape::List(NarrowedShape {
473                    pos: pos.clone(),
474                    types: NarrowingShape::Any,
475                }),
476                Shape::Narrowed(NarrowedShape {
477                    types: NarrowingShape::Any,
478                    ..
479                }) => Shape::List(NarrowedShape {
480                    pos: pos.clone(),
481                    types: NarrowingShape::Any,
482                }),
483                _ => Shape::TypeErr(
484                    pos.clone(),
485                    format!(
486                        "filter target must be a list, got {}",
487                        target_shape.type_name()
488                    ),
489                ),
490            }
491        }
492        FuncOpDef::Reduce(ReduceOpDef {
493            func,
494            acc,
495            target,
496            pos,
497        }) => {
498            let target_shape = target.derive_shape(symbol_table);
499            let acc_shape = acc.derive_shape(symbol_table);
500            let func_shape = func.derive_shape(symbol_table);
501            // target must be a list
502            match &target_shape {
503                Shape::List(_) | Shape::Hole(_) => {}
504                Shape::Narrowed(NarrowedShape {
505                    types: NarrowingShape::Any,
506                    ..
507                }) => {}
508                _ => {
509                    return Shape::TypeErr(
510                        pos.clone(),
511                        format!(
512                            "reduce target must be a list, got {}",
513                            target_shape.type_name()
514                        ),
515                    );
516                }
517            }
518            // Return type is acc's shape narrowed against func.ret
519            match &func_shape {
520                Shape::Func(fdef) => {
521                    let narrowed = acc_shape.narrow(&fdef.ret, symbol_table);
522                    match narrowed {
523                        Shape::TypeErr(_, _) => acc_shape,
524                        other => other,
525                    }
526                }
527                _ => acc_shape,
528            }
529        }
530    }
531}
532
533impl DeriveShape for Expression {
534    fn derive_shape(&self, symbol_table: &mut BTreeMap<Rc<str>, Shape>) -> Shape {
535        match self {
536            Expression::Simple(v) => v.derive_shape(symbol_table),
537            Expression::Format(def) => Shape::Str(def.pos.clone()),
538            Expression::Not(def) => derive_not_shape(def, symbol_table),
539            Expression::Grouped(v, _pos) => v.as_ref().derive_shape(symbol_table),
540            Expression::Range(def) => Shape::List(NarrowedShape::new_with_pos(
541                vec![Shape::Int(def.start.pos().clone())],
542                def.pos.clone(),
543            )),
544            Expression::Cast(def) => match def.cast_type {
545                CastType::Int => Shape::Int(def.pos.clone()),
546                CastType::Str => Shape::Str(def.pos.clone()),
547                CastType::Float => Shape::Float(def.pos.clone()),
548                CastType::Bool => Shape::Boolean(def.pos.clone()),
549            },
550            Expression::Import(def) => Shape::Import(ImportShape::Unresolved(PositionedItem::new(
551                def.path.fragment.clone(),
552                def.path.pos.clone(),
553            ))),
554            Expression::Binary(def) => {
555                let left_shape = def.left.derive_shape(symbol_table);
556                if def.kind == BinaryExprType::DOT {
557                    let shape =
558                        derive_dot_expression(&def.pos, &left_shape, &def.right, symbol_table);
559                    // Update the symbol table with the inferred left shape
560                    if let Expression::Simple(Value::Symbol(pi)) = def.left.as_ref() {
561                        if let Shape::TypeErr(_, _) = &shape {
562                            // Don't update symbol table on type errors
563                        } else {
564                            match &left_shape {
565                                Shape::Hole(_) => {
566                                    let inferred = infer_container_shape_from_dot(
567                                        &left_shape,
568                                        &def.right,
569                                        &shape,
570                                        &def.pos,
571                                        symbol_table,
572                                    );
573                                    symbol_table.insert(pi.val.clone(), inferred);
574                                }
575                                _ => {}
576                            }
577                        }
578                    }
579                    shape
580                } else {
581                    let right_shape = def.right.derive_shape(symbol_table);
582                    match &def.kind {
583                        // Comparison operators return Boolean
584                        BinaryExprType::Equal
585                        | BinaryExprType::NotEqual
586                        | BinaryExprType::GT
587                        | BinaryExprType::LT
588                        | BinaryExprType::GTEqual
589                        | BinaryExprType::LTEqual
590                        | BinaryExprType::REMatch
591                        | BinaryExprType::NotREMatch
592                        | BinaryExprType::IN
593                        | BinaryExprType::IS => {
594                            // Check operands are compatible (narrow checks this)
595                            // but always return Boolean
596                            Shape::Boolean(def.pos.clone())
597                        }
598                        // Boolean operators require boolean operands
599                        BinaryExprType::AND | BinaryExprType::OR => {
600                            // Narrow to check compatibility
601                            let narrowed = left_shape.narrow(&right_shape, symbol_table);
602                            if let Shape::TypeErr(_, _) = &narrowed {
603                                narrowed
604                            } else {
605                                Shape::Boolean(def.pos.clone())
606                            }
607                        }
608                        // Math operators narrow types
609                        _ => left_shape.narrow(&right_shape, symbol_table),
610                    }
611                }
612            }
613            Expression::Copy(def) => derive_copy_shape(def, symbol_table),
614            Expression::Include(def) => derive_include_shape(def),
615            Expression::Call(def) => derive_call_shape(def, symbol_table),
616            Expression::Func(def) => def.derive_shape(symbol_table),
617            Expression::Select(def) => def.derive_shape(symbol_table),
618            Expression::FuncOp(def) => derive_func_op_shape(def, symbol_table),
619            Expression::Module(def) => def.derive_shape(symbol_table),
620            Expression::Fail(def) => {
621                let msg_shape = def.message.derive_shape(symbol_table);
622                match &msg_shape {
623                    Shape::Str(_) | Shape::Hole(_) => {}
624                    Shape::Narrowed(NarrowedShape {
625                        types: NarrowingShape::Any,
626                        ..
627                    }) => {}
628                    _ => {
629                        return Shape::TypeErr(
630                            def.pos.clone(),
631                            format!(
632                                "fail message must be a string, got {}",
633                                msg_shape.type_name()
634                            ),
635                        );
636                    }
637                }
638                // fail never produces a value - any type is compatible
639                Shape::Narrowed(NarrowedShape {
640                    pos: def.pos.clone(),
641                    types: NarrowingShape::Any,
642                })
643            }
644            Expression::Debug(def) => {
645                // Debug is transparent - return the shape of the inner expression
646                def.expr.derive_shape(symbol_table)
647            }
648        }
649    }
650}
651
652/// Infer the container shape from a dot expression.
653/// When we have `hole.field`, we need to infer what the hole must be
654/// (a tuple with that field).
655fn infer_container_shape_from_dot(
656    left_shape: &Shape,
657    right_expr: &Expression,
658    _field_shape: &Shape,
659    _pos: &Position,
660    _symbol_table: &mut BTreeMap<Rc<str>, Shape>,
661) -> Shape {
662    match (left_shape, right_expr) {
663        (Shape::Hole(hole_pi), Expression::Simple(Value::Symbol(pi)))
664        | (Shape::Hole(hole_pi), Expression::Simple(Value::Str(pi))) => {
665            Shape::Tuple(PositionedItem::new(
666                vec![(
667                    PositionedItem::new(pi.val.clone(), pi.pos.clone()),
668                    Shape::Narrowed(NarrowedShape {
669                        pos: hole_pi.pos.clone(),
670                        types: NarrowingShape::Any,
671                    }),
672                )],
673                hole_pi.pos.clone(),
674            ))
675        }
676        (Shape::Hole(hole_pi), Expression::Simple(Value::Int(_))) => Shape::List(NarrowedShape {
677            pos: hole_pi.pos.clone(),
678            types: NarrowingShape::Any,
679        }),
680        _ => left_shape.clone(),
681    }
682}
683
684fn derive_dot_expression(
685    pos: &Position,
686    left_shape: &Shape,
687    right_expr: &Expression,
688    symbol_table: &mut BTreeMap<Rc<str>, Shape>,
689) -> Shape {
690    // left shape is symbol or tuple or array.
691    // right shape is symbol, str, number, grouped expression
692    match (left_shape, right_expr) {
693        // ===== Recursive cases: right side is itself a DOT chain =====
694        (
695            Shape::Tuple(tshape),
696            Expression::Binary(BinaryOpDef {
697                kind: BinaryExprType::DOT,
698                left,
699                right,
700                pos,
701            }),
702        ) => {
703            // left is the next accessor in the chain
704            let accessor_shape = left.derive_shape(symbol_table);
705            // Resolve what field the accessor refers to in this tuple
706            let resolved = resolve_tuple_field(tshape, &accessor_shape, left, pos);
707            match resolved {
708                Shape::TypeErr(_, _) => resolved,
709                field_shape => {
710                    // Recurse with the field's shape as the new left
711                    derive_dot_expression(pos, &field_shape, right, symbol_table)
712                }
713            }
714        }
715        (
716            Shape::List(lshape),
717            Expression::Binary(BinaryOpDef {
718                kind: BinaryExprType::DOT,
719                left,
720                right,
721                pos,
722            }),
723        ) => {
724            let accessor_shape = left.derive_shape(symbol_table);
725            match &accessor_shape {
726                Shape::Int(_) => {
727                    // Indexing a list gives us the element type
728                    let elem_shape = Shape::Narrowed(lshape.clone());
729                    derive_dot_expression(pos, &elem_shape, right, symbol_table)
730                }
731                Shape::Hole(_pi) => {
732                    // Unknown accessor on a list - could be int index
733                    let elem_shape = Shape::Narrowed(lshape.clone());
734                    derive_dot_expression(pos, &elem_shape, right, symbol_table)
735                }
736                _ => Shape::TypeErr(
737                    pos.clone(),
738                    format!(
739                        "Lists can only be indexed by integer, got {}",
740                        accessor_shape.type_name()
741                    ),
742                ),
743            }
744        }
745        (
746            Shape::Narrowed(narrowed_shape),
747            Expression::Binary(BinaryOpDef {
748                kind: BinaryExprType::DOT,
749                left: _,
750                right: _,
751                pos,
752            }),
753        ) => {
754            // For narrowed types with DOT chains, try each candidate
755            match &narrowed_shape.types {
756                NarrowingShape::Any => {
757                    // Unconstrained - result is also unconstrained
758                    Shape::Narrowed(NarrowedShape {
759                        pos: pos.clone(),
760                        types: NarrowingShape::Any,
761                    })
762                }
763                NarrowingShape::Narrowed(types) => {
764                    // Try each candidate type
765                    let mut results = Vec::new();
766                    for t in types {
767                        let inner_result = derive_dot_expression(pos, t, right_expr, symbol_table);
768                        if let Shape::TypeErr(_, _) = &inner_result {
769                            // Skip incompatible candidates
770                        } else {
771                            results.push(inner_result);
772                        }
773                    }
774                    if results.is_empty() {
775                        Shape::TypeErr(
776                            pos.clone(),
777                            "No candidate type is compatible with field access".to_owned(),
778                        )
779                    } else if results.len() == 1 {
780                        results.pop().unwrap()
781                    } else {
782                        Shape::Narrowed(NarrowedShape::new_with_pos(results, pos.clone()))
783                    }
784                }
785            }
786        }
787        (
788            Shape::Hole(_hole_pi),
789            Expression::Binary(BinaryOpDef {
790                kind: BinaryExprType::DOT,
791                left,
792                right,
793                pos,
794            }),
795        ) => {
796            // We don't know what the hole is, but it's being accessed with a dot chain.
797            // The accessor tells us something about the hole's type.
798            let accessor_shape = left.derive_shape(symbol_table);
799            match &accessor_shape {
800                // Symbol/Str accessor means it could be a tuple
801                Shape::Hole(_) | Shape::Str(_) => {
802                    // Infer as tuple with unknown field, then continue chain
803                    let field_shape = Shape::Narrowed(NarrowedShape {
804                        pos: pos.clone(),
805                        types: NarrowingShape::Any,
806                    });
807                    derive_dot_expression(pos, &field_shape, right, symbol_table)
808                }
809                Shape::Int(_) => {
810                    // Infer as list, element is unknown
811                    let elem_shape = Shape::Narrowed(NarrowedShape {
812                        pos: pos.clone(),
813                        types: NarrowingShape::Any,
814                    });
815                    derive_dot_expression(pos, &elem_shape, right, symbol_table)
816                }
817                _ => Shape::Narrowed(NarrowedShape {
818                    pos: pos.clone(),
819                    types: NarrowingShape::Any,
820                }),
821            }
822        }
823
824        // ===== Terminal cases: right side is a simple value =====
825
826        // Tuple field access by name
827        (Shape::Tuple(tshape), Expression::Simple(Value::Str(pi)))
828        | (Shape::Tuple(tshape), Expression::Simple(Value::Symbol(pi))) => {
829            for (field_name, field_shape) in tshape.val.iter() {
830                if field_name.val == pi.val {
831                    return field_shape.clone();
832                }
833            }
834            Shape::TypeErr(
835                pi.pos.clone(),
836                format!("Field '{}' not found in tuple", pi.val),
837            )
838        }
839
840        // Tuple indexed by int - type error
841        (Shape::Tuple(_), Expression::Simple(Value::Int(pi))) => Shape::TypeErr(
842            pi.pos.clone(),
843            "Tuples cannot be indexed by integer".to_owned(),
844        ),
845
846        // List indexed by int - return element shape
847        (Shape::List(lshape), Expression::Simple(Value::Int(_pi))) => {
848            Shape::Narrowed(lshape.clone())
849        }
850
851        // List accessed by name - type error
852        (Shape::List(_), Expression::Simple(Value::Symbol(pi)))
853        | (Shape::List(_), Expression::Simple(Value::Str(pi))) => Shape::TypeErr(
854            pi.pos.clone(),
855            "Lists cannot be accessed by field name".to_owned(),
856        ),
857
858        // Hole accessed by name - infer as tuple with that field
859        (Shape::Hole(hole_pi), Expression::Simple(Value::Symbol(_pi)))
860        | (Shape::Hole(hole_pi), Expression::Simple(Value::Str(_pi))) => {
861            Shape::Narrowed(NarrowedShape {
862                pos: hole_pi.pos.clone(),
863                types: NarrowingShape::Any,
864            })
865        }
866
867        // Hole accessed by int - infer as list
868        (Shape::Hole(hole_pi), Expression::Simple(Value::Int(_pi))) => {
869            Shape::Narrowed(NarrowedShape {
870                pos: hole_pi.pos.clone(),
871                types: NarrowingShape::Any,
872            })
873        }
874
875        // Narrowed accessed by name - filter candidates
876        (Shape::Narrowed(nshape), Expression::Simple(Value::Symbol(pi)))
877        | (Shape::Narrowed(nshape), Expression::Simple(Value::Str(pi))) => {
878            match &nshape.types {
879                NarrowingShape::Any => Shape::Narrowed(NarrowedShape {
880                    pos: pi.pos.clone(),
881                    types: NarrowingShape::Any,
882                }),
883                NarrowingShape::Narrowed(types) if types.is_empty() => {
884                    // Empty candidates = unconstrained, field access is allowed
885                    Shape::Narrowed(NarrowedShape {
886                        pos: pi.pos.clone(),
887                        types: NarrowingShape::Any,
888                    })
889                }
890                NarrowingShape::Narrowed(types) => {
891                    let mut results = Vec::new();
892                    for t in types {
893                        match t {
894                            Shape::Tuple(tshape) => {
895                                for (field_name, field_shape) in tshape.val.iter() {
896                                    if field_name.val == pi.val {
897                                        results.push(field_shape.clone());
898                                    }
899                                }
900                            }
901                            Shape::Hole(_) => {
902                                results.push(Shape::Narrowed(NarrowedShape {
903                                    pos: pi.pos.clone(),
904                                    types: NarrowingShape::Any,
905                                }));
906                            }
907                            _ => { /* not field-accessible, skip */ }
908                        }
909                    }
910                    if results.is_empty() {
911                        Shape::TypeErr(
912                            pi.pos.clone(),
913                            format!("No candidate type has field '{}'", pi.val),
914                        )
915                    } else if results.len() == 1 {
916                        results.pop().unwrap()
917                    } else {
918                        Shape::Narrowed(NarrowedShape::new_with_pos(results, pi.pos.clone()))
919                    }
920                }
921            }
922        }
923
924        // Narrowed accessed by int - filter to list-like candidates
925        (Shape::Narrowed(nshape), Expression::Simple(Value::Int(pi))) => {
926            match &nshape.types {
927                NarrowingShape::Any => Shape::Narrowed(NarrowedShape {
928                    pos: pi.pos.clone(),
929                    types: NarrowingShape::Any,
930                }),
931                NarrowingShape::Narrowed(types) if types.is_empty() => {
932                    // Empty candidates = unconstrained
933                    Shape::Narrowed(NarrowedShape {
934                        pos: pi.pos.clone(),
935                        types: NarrowingShape::Any,
936                    })
937                }
938                NarrowingShape::Narrowed(types) => {
939                    let mut results = Vec::new();
940                    for t in types {
941                        match t {
942                            Shape::List(lshape) => {
943                                results.push(Shape::Narrowed(lshape.clone()));
944                            }
945                            Shape::Hole(_) => {
946                                results.push(Shape::Narrowed(NarrowedShape {
947                                    pos: pi.pos.clone(),
948                                    types: NarrowingShape::Any,
949                                }));
950                            }
951                            _ => { /* not int-indexable, skip */ }
952                        }
953                    }
954                    if results.is_empty() {
955                        Shape::TypeErr(
956                            pi.pos.clone(),
957                            "No candidate type supports integer indexing".to_owned(),
958                        )
959                    } else if results.len() == 1 {
960                        results.pop().unwrap()
961                    } else {
962                        Shape::Narrowed(NarrowedShape::new_with_pos(results, pi.pos.clone()))
963                    }
964                }
965            }
966        }
967
968        // Grouped expression - unwrap and recurse
969        (_, Expression::Grouped(expr, _)) => {
970            derive_dot_expression(pos, left_shape, expr.as_ref(), symbol_table)
971        }
972
973        // Resolved import - treat as a tuple of exported bindings
974        (Shape::Import(ImportShape::Resolved(_, tuple_fields)), _) => {
975            let tshape = PositionedItem::new(tuple_fields.clone(), pos.clone());
976            derive_dot_expression(pos, &Shape::Tuple(tshape), right_expr, symbol_table)
977        }
978
979        // Unresolved import - allow any field access
980        (Shape::Import(ImportShape::Unresolved(_)), _) => Shape::Narrowed(NarrowedShape {
981            pos: pos.clone(),
982            types: NarrowingShape::Any,
983        }),
984
985        // TypeErr propagation
986        (Shape::TypeErr(_, _), _) => left_shape.clone(),
987
988        // Everything else is invalid
989        (_, _) => Shape::TypeErr(pos.clone(), "Invalid field selector".to_owned()),
990    }
991}
992
993/// Helper to resolve a field in a tuple shape given an accessor
994fn resolve_tuple_field(
995    tshape: &PositionedItem<TupleShape>,
996    _accessor_shape: &Shape,
997    accessor_expr: &Expression,
998    pos: &Position,
999) -> Shape {
1000    match accessor_expr {
1001        Expression::Simple(Value::Symbol(pi)) | Expression::Simple(Value::Str(pi)) => {
1002            for (field_name, field_shape) in tshape.val.iter() {
1003                if field_name.val == pi.val {
1004                    return field_shape.clone();
1005                }
1006            }
1007            Shape::TypeErr(
1008                pos.clone(),
1009                format!("Field '{}' not found in tuple", pi.val),
1010            )
1011        }
1012        _ => {
1013            // For computed field access, we can't know the field at type-check time
1014            Shape::Narrowed(NarrowedShape {
1015                pos: pos.clone(),
1016                types: NarrowingShape::Any,
1017            })
1018        }
1019    }
1020}
1021
1022impl DeriveShape for Value {
1023    fn derive_shape(&self, symbol_table: &mut BTreeMap<Rc<str>, Shape>) -> Shape {
1024        match self {
1025            Value::Empty(p) => Shape::Narrowed(NarrowedShape {
1026                pos: p.clone(),
1027                types: NarrowingShape::Any,
1028            }),
1029            Value::Boolean(p) => Shape::Boolean(p.pos.clone()),
1030            Value::Int(p) => Shape::Int(p.pos.clone()),
1031            Value::Float(p) => Shape::Float(p.pos.clone()),
1032            Value::Str(p) => Shape::Str(p.pos.clone()),
1033            Value::Symbol(p) => {
1034                if let Some(s) = symbol_table.get(&p.val) {
1035                    s.clone()
1036                } else {
1037                    Shape::Hole(p.clone())
1038                }
1039            }
1040            Value::Tuple(flds) => derive_field_list_shape(&flds.val, &flds.pos, symbol_table),
1041            Value::List(flds) => {
1042                let mut field_shapes = Vec::new();
1043                for f in &flds.elems {
1044                    field_shapes.push(f.derive_shape(symbol_table));
1045                }
1046                Shape::List(NarrowedShape::new_with_pos(field_shapes, flds.pos.clone()))
1047            }
1048        }
1049    }
1050}
1051
1052fn derive_field_list_shape(
1053    flds: &Vec<(super::Token, Option<Expression>, Expression)>,
1054    pos: &Position,
1055    symbol_table: &mut BTreeMap<Rc<str>, Shape>,
1056) -> Shape {
1057    let mut field_shapes = Vec::new();
1058    for (ref tok, ref constraint, ref expr) in flds {
1059        let value_shape = expr.derive_shape(symbol_table);
1060        let shape = if let Some(c) = constraint {
1061            let constraint_shape = c.derive_shape(symbol_table);
1062            let narrowed = value_shape.narrow(&constraint_shape, symbol_table);
1063            if let Shape::TypeErr(_, _) = &narrowed {
1064                return narrowed;
1065            }
1066            narrowed
1067        } else {
1068            value_shape
1069        };
1070        field_shapes.push((
1071            PositionedItem::new(tok.fragment.clone(), tok.pos.clone()),
1072            shape,
1073        ));
1074    }
1075    Shape::Tuple(PositionedItem::new(field_shapes, pos.clone()))
1076}
1077
1078pub struct Checker {
1079    symbol_table: BTreeMap<Rc<str>, Shape>,
1080    err_stack: Vec<BuildError>,
1081    shape_stack: Vec<Shape>,
1082    // Tracks nesting into Module expressions. When > 0, visit_statement
1083    // is skipped because inner statements are handled by ModuleDef::derive_shape
1084    // with its own Checker.
1085    nested_depth: usize,
1086    // When false, type errors involving Empty/NULL values are suppressed.
1087    strict: bool,
1088    // Working directory for resolving import paths.
1089    working_dir: Option<PathBuf>,
1090    // Cache of already-resolved import shapes, shared across Checker instances.
1091    shape_cache: Rc<RefCell<BTreeMap<PathBuf, Shape>>>,
1092    // Stack of currently-being-resolved imports for cycle detection.
1093    import_stack: Vec<PathBuf>,
1094}
1095
1096impl Checker {
1097    pub fn new() -> Self {
1098        return Self {
1099            symbol_table: BTreeMap::new(),
1100            err_stack: Vec::new(),
1101            shape_stack: Vec::new(),
1102            nested_depth: 0,
1103            strict: true,
1104            working_dir: None,
1105            shape_cache: Rc::new(RefCell::new(BTreeMap::new())),
1106            import_stack: Vec::new(),
1107        };
1108    }
1109
1110    pub fn with_strict(mut self, strict: bool) -> Self {
1111        self.strict = strict;
1112        self
1113    }
1114
1115    pub fn with_working_dir<P: Into<PathBuf>>(mut self, dir: P) -> Self {
1116        self.working_dir = Some(dir.into());
1117        self
1118    }
1119
1120    pub fn with_shape_cache(mut self, cache: Rc<RefCell<BTreeMap<PathBuf, Shape>>>) -> Self {
1121        self.shape_cache = cache;
1122        self
1123    }
1124
1125    pub fn with_import_stack(mut self, stack: Vec<PathBuf>) -> Self {
1126        self.import_stack = stack;
1127        self
1128    }
1129
1130    pub fn with_symbol_table(mut self, symbol_table: BTreeMap<Rc<str>, Shape>) -> Self {
1131        self.symbol_table = symbol_table;
1132        self
1133    }
1134
1135    pub fn pop_shape(&mut self) -> Option<Shape> {
1136        self.shape_stack.pop()
1137    }
1138
1139    fn push_shape_or_err(&mut self, shape: Shape) {
1140        if let Shape::TypeErr(pos, msg) = &shape {
1141            self.err_stack.push(BuildError::with_pos(
1142                msg.clone(),
1143                ErrorType::TypeFail,
1144                pos.clone(),
1145            ));
1146        } else {
1147            self.shape_stack.push(shape);
1148        }
1149    }
1150
1151    /// Resolve an import path to a Shape by reading, parsing, and type-checking
1152    /// the imported file. Returns `ImportShape::Resolved` with the exported tuple
1153    /// shape, or `ImportShape::Unresolved` if no working directory is set.
1154    fn resolve_import(&mut self, path: &str, pos: &Position) -> Shape {
1155        let working_dir = match &self.working_dir {
1156            Some(dir) => dir.clone(),
1157            None => {
1158                return Shape::Import(ImportShape::Unresolved(PositionedItem::new(
1159                    path.into(),
1160                    pos.clone(),
1161                )));
1162            }
1163        };
1164
1165        let resolved_path = working_dir.join(path);
1166
1167        // Check the cache first
1168        if let Some(cached) = self.shape_cache.borrow().get(&resolved_path) {
1169            return cached.clone();
1170        }
1171
1172        // Check for import cycles
1173        if self.import_stack.contains(&resolved_path) {
1174            return Shape::TypeErr(
1175                pos.clone(),
1176                format!("Import cycle detected: {}", resolved_path.display()),
1177            );
1178        }
1179
1180        // Read the file
1181        let contents = match std::fs::read_to_string(&resolved_path) {
1182            Ok(c) => c,
1183            Err(_) => {
1184                // File not found or unreadable — return unresolved rather than error,
1185                // since the file might be generated or available at runtime.
1186                return Shape::Import(ImportShape::Unresolved(PositionedItem::new(
1187                    path.into(),
1188                    pos.clone(),
1189                )));
1190            }
1191        };
1192
1193        // Parse the file
1194        let iter = OffsetStrIter::new(&contents).with_src_file(&resolved_path);
1195        let mut stmts = match parse(iter, None) {
1196            Ok(stmts) => stmts,
1197            Err(_) => {
1198                return Shape::TypeErr(
1199                    pos.clone(),
1200                    format!("Failed to parse imported file: {}", resolved_path.display()),
1201                );
1202            }
1203        };
1204
1205        // Type-check the imported file with a new Checker sharing the cache
1206        let import_dir = resolved_path.parent().map(|p| p.to_path_buf());
1207        let mut import_stack = self.import_stack.clone();
1208        import_stack.push(resolved_path.clone());
1209
1210        let mut child_checker = Checker::new()
1211            .with_shape_cache(self.shape_cache.clone())
1212            .with_import_stack(import_stack)
1213            .with_strict(self.strict);
1214        if let Some(dir) = import_dir {
1215            child_checker = child_checker.with_working_dir(dir);
1216        }
1217
1218        child_checker.walk_statement_list(stmts.iter_mut().collect());
1219
1220        // Check for type errors in the imported file
1221        let symbol_table = match child_checker.result() {
1222            Ok(syms) => syms,
1223            Err(err) => {
1224                // Don't cache type errors — the file may be fixed and re-imported.
1225                return Shape::TypeErr(
1226                    err.pos.unwrap_or_else(|| pos.clone()),
1227                    format!(
1228                        "Type error in imported file {}: {}",
1229                        resolved_path.display(),
1230                        err.msg
1231                    ),
1232                );
1233            }
1234        };
1235
1236        // Collect exported symbols as a tuple shape
1237        let tuple_fields: TupleShape = symbol_table
1238            .iter()
1239            .map(|(name, shape)| {
1240                (
1241                    PositionedItem::new(name.clone(), pos.clone()),
1242                    shape.clone(),
1243                )
1244            })
1245            .collect();
1246
1247        let resolved = Shape::Import(ImportShape::Resolved(pos.clone(), tuple_fields));
1248
1249        // Cache only successful results
1250        self.shape_cache
1251            .borrow_mut()
1252            .insert(resolved_path, resolved.clone());
1253
1254        resolved
1255    }
1256
1257    /// Returns the accumulated symbol table if type checking succeeded,
1258    /// or the first type error encountered (which is typically the root cause).
1259    pub fn result(self) -> Result<BTreeMap<Rc<str>, Shape>, BuildError> {
1260        if self.err_stack.is_empty() {
1261            Ok(self.symbol_table)
1262        } else {
1263            // Return the first error — it's usually the root cause.
1264            // Subsequent errors are often cascading failures.
1265            Err(self.err_stack.into_iter().next().unwrap())
1266        }
1267    }
1268}
1269
1270impl Visitor for Checker {
1271    fn visit_import(&mut self, _i: &mut ImportDef) {
1272        // noop by default;
1273    }
1274
1275    fn leave_import(&mut self) {
1276        // noop by default
1277    }
1278
1279    fn visit_include(&mut self, _i: &mut IncludeDef) {
1280        // noop by default;
1281    }
1282
1283    fn leave_include(&mut self) {
1284        // noop by default
1285    }
1286
1287    fn visit_fail(&mut self, _f: &mut FailDef) {
1288        // noop by default;
1289    }
1290
1291    fn leave_fail(&mut self) {
1292        // noop by default
1293    }
1294
1295    // visit_value is intentionally a noop — DeriveShape handles value shapes.
1296
1297    fn visit_expression(&mut self, expr: &mut Expression) {
1298        // Track entry into Module expressions so we skip their inner statements
1299        // (they're handled by ModuleDef::derive_shape with a separate Checker).
1300        if matches!(expr, Expression::Module(_)) {
1301            self.nested_depth += 1;
1302        }
1303    }
1304
1305    fn leave_expression(&mut self, expr: &Expression) {
1306        if matches!(expr, Expression::Module(_)) {
1307            self.nested_depth -= 1;
1308        }
1309    }
1310
1311    fn visit_statement(&mut self, stmt: &mut Statement) {
1312        // Skip statements inside nested Module expressions — they're handled
1313        // by ModuleDef::derive_shape with its own Checker.
1314        if self.nested_depth > 0 {
1315            return;
1316        }
1317        match stmt {
1318            Statement::Let(def) => {
1319                let name = def.name.fragment.clone();
1320                let mut shape = def.value.derive_shape(&mut self.symbol_table);
1321                // Resolve unresolved imports if we have a working directory
1322                if let Shape::Import(ImportShape::Unresolved(pi)) = &shape {
1323                    shape = self.resolve_import(&pi.val, &pi.pos);
1324                }
1325                // Enforce constraint if present
1326                if let Some(ref constraint_expr) = def.constraint {
1327                    let constraint_shape = constraint_expr.derive_shape(&mut self.symbol_table);
1328                    let narrowed = shape.narrow(&constraint_shape, &mut self.symbol_table);
1329                    if let Shape::TypeErr(pos, msg) = &narrowed {
1330                        self.err_stack.push(BuildError::with_pos(
1331                            msg.clone(),
1332                            ErrorType::TypeFail,
1333                            pos.clone(),
1334                        ));
1335                        return;
1336                    }
1337                    shape = narrowed;
1338                }
1339                if let Shape::TypeErr(pos, msg) = &shape {
1340                    self.err_stack.push(BuildError::with_pos(
1341                        msg.clone(),
1342                        ErrorType::TypeFail,
1343                        pos.clone(),
1344                    ));
1345                } else {
1346                    self.symbol_table.insert(name.clone(), shape.clone());
1347                    self.shape_stack.push(shape);
1348                }
1349            }
1350            Statement::Assert(_pos, ref expr) => {
1351                let shape = expr.derive_shape(&mut self.symbol_table);
1352                // UCG asserts accept either a boolean or a tuple with an `ok` field
1353                // e.g., `assert { ok = expr, desc = "..." };`
1354                self.push_shape_or_err(shape);
1355            }
1356            Statement::Output(_pos, _tok, ref expr) => {
1357                let shape = expr.derive_shape(&mut self.symbol_table);
1358                self.push_shape_or_err(shape);
1359            }
1360            Statement::Print(_pos, _tok, ref expr) => {
1361                let shape = expr.derive_shape(&mut self.symbol_table);
1362                self.push_shape_or_err(shape);
1363            }
1364            Statement::Expression(ref expr) => {
1365                let shape = expr.derive_shape(&mut self.symbol_table);
1366                self.push_shape_or_err(shape);
1367            }
1368        }
1369    }
1370
1371    fn leave_statement(&mut self, _stmt: &Statement) {
1372        // noop by default
1373    }
1374}
1375
1376#[cfg(test)]
1377mod test;