bend/fun/check/
unbound_vars.rs1use 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 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 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 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 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
52pub 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}