bend/fun/check/
unbound_vars.rs

1use crate::{
2  diagnostics::Diagnostics,
3  fun::{transform::desugar_bend, Ctx, Name, Pattern, Term},
4  maybe_grow,
5};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
9pub enum UnboundVarErr {
10  Local(Name),
11  Global { var: Name, declared: usize, used: usize },
12}
13
14impl Ctx<'_> {
15  /// Checks that there are no unbound variables in all definitions.
16  pub fn check_unbound_vars(&mut self) -> Result<(), Diagnostics> {
17    for (def_name, def) in self.book.defs.iter_mut() {
18      let mut errs = Vec::new();
19      for rule in &mut def.rules {
20        // Note: Using a Vec instead of a Map is a deliberate optimization.
21        let mut scope = rule.pats.iter().flat_map(|pat| pat.binds()).map(|x| x.as_ref()).collect::<Vec<_>>();
22        rule.body.check_unbound_vars(&mut scope, &mut errs);
23      }
24
25      for err in errs {
26        self.info.add_function_error(err, def_name.clone(), def.source.clone());
27      }
28    }
29
30    self.info.fatal(())
31  }
32}
33
34impl Term {
35  /// Checks that all variables are bound.
36  /// Precondition: References have been resolved, implicit binds have been solved.
37  pub fn check_unbound_vars<'a>(
38    &'a mut self,
39    scope: &mut Vec<Option<&'a Name>>,
40    errs: &mut Vec<UnboundVarErr>,
41  ) {
42    let mut globals = HashMap::new();
43    check_uses(self, scope, &mut globals, errs);
44
45    // Check global vars
46    for (nam, (declared, used)) in globals.into_iter().filter(|(_, (d, u))| !(*d == 1 && *u == 1)) {
47      errs.push(UnboundVarErr::Global { var: nam.clone(), declared, used });
48    }
49  }
50}
51
52/// Scope has the number of times a name was declared in the current scope
53/// Globals has how many times a global var name was declared and used.
54pub fn check_uses<'a>(
55  term: &'a mut Term,
56  scope: &mut Vec<Option<&'a Name>>,
57  globals: &mut HashMap<Name, (usize, usize)>,
58  errs: &mut Vec<UnboundVarErr>,
59) {
60  maybe_grow(move || match term {
61    Term::Var { nam } => {
62      if !scope_contains(nam, scope) {
63        errs.push(UnboundVarErr::Local(nam.clone()));
64        *term = Term::Err;
65      }
66    }
67    Term::Link { nam } => {
68      globals.entry(nam.clone()).or_default().1 += 1;
69    }
70
71    _ => {
72      if let Some(pat) = term.pattern() {
73        check_global_binds(pat, globals)
74      }
75      for (child, binds) in term.children_mut_with_binds() {
76        for bind in binds.clone() {
77          scope.push(bind.as_ref());
78        }
79        check_uses(child, scope, globals, errs);
80        for _ in binds {
81          scope.pop();
82        }
83      }
84    }
85  })
86}
87
88pub fn check_global_binds(pat: &Pattern, globals: &mut HashMap<Name, (usize, usize)>) {
89  match pat {
90    Pattern::Chn(nam) => {
91      globals.entry(nam.clone()).or_default().0 += 1;
92    }
93    _ => {
94      for child in pat.children() {
95        check_global_binds(child, globals)
96      }
97    }
98  }
99}
100
101fn scope_contains(nam: &Name, scope: &[Option<&Name>]) -> bool {
102  scope.iter().rev().any(|scope_nam| scope_nam == nam)
103}
104
105impl std::fmt::Display for UnboundVarErr {
106  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107    match self {
108      UnboundVarErr::Local(var) => {
109        if var == desugar_bend::RECURSIVE_KW {
110          write!(
111            f,
112            "Unbound variable '{}'.\n    Note: '{}' is only a keyword inside the 'when' arm of a 'bend'.",
113            var,
114            desugar_bend::RECURSIVE_KW
115          )
116        } else if let Some((pre, suf)) = var.rsplit_once('-') {
117          write!(
118            f,
119            "Unbound variable '{var}'. If you wanted to subtract '{pre}' from '{suf}', you must separate it with spaces ('{pre} - {suf}') since '-' is a valid name character."
120          )
121        } else {
122          write!(f, "Unbound variable '{var}'.")
123        }
124      }
125      UnboundVarErr::Global { var, declared, used } => match (declared, used) {
126        (0, _) => write!(f, "Unbound unscoped variable '${var}'."),
127        (_, 0) => write!(f, "Unscoped variable from lambda 'λ${var}' is never used."),
128        (1, _) => write!(f, "Unscoped variable '${var}' used more than once."),
129        (_, 1) => write!(f, "Unscoped lambda 'λ${var}' declared more than once."),
130        (_, _) => {
131          write!(f, "Unscoped lambda 'λ${var}' and unscoped variable '${var}' used more than once.")
132        }
133      },
134    }
135  }
136}