Skip to main content

nifty_check/checker/
mod.rs

1//! Checks a program's parsed files in phases: declarations, state, functions, then the UI. Functions are
2//! checked before the UI so the UI knows what state every function call reads.
3
4mod args;
5mod builtins;
6mod consts;
7mod env;
8mod expr;
9mod menu;
10mod stmt;
11mod ui;
12
13use std::collections::HashMap;
14use std::collections::hash_map::Entry;
15use std::mem;
16use std::rc::Rc;
17
18use nifty_syntax::ast::{self, Item, TypeExpr, TypeExprKind};
19use nifty_syntax::{Diagnostic, Span};
20
21use crate::elements::{ElementKind, MENU, OPEN, Place, WINDOW};
22use crate::hir::{
23    FieldDef, FnDef, FnId, Program, StateDef, StateId, StructDef, StructId, Type, ViewDef, ViewId,
24    WindowId,
25};
26use crate::{elements, functions};
27use env::{Body, Context, Effects, FnEffects, LocalInfo, LocalKind};
28
29#[derive(Clone, Copy, Debug)]
30enum Global {
31    State(StateId),
32    Struct(StructId),
33    Fn(FnId),
34    View(ViewId),
35    Window(WindowId),
36}
37
38/// A top-level name and where it is declared.
39#[derive(Clone, Copy, Debug)]
40struct Declared {
41    global: Global,
42    span: Span,
43}
44
45struct Signature {
46    params: Vec<(String, Type)>,
47    return_type: Type,
48}
49
50pub(crate) struct Checker {
51    diagnostics: Vec<Diagnostic>,
52    /// Top-level names from every file: a program is one namespace.
53    globals: HashMap<String, Declared>,
54    program: Program,
55    fn_signatures: Vec<Signature>,
56    view_params: Vec<Vec<(String, Type)>>,
57    fn_effects: FnEffects,
58    body: Body,
59    effects: Effects,
60}
61
62/// Checks `files` as one program; the first is the entry file.
63pub(crate) fn check(files: &[&ast::File]) -> (Program, Vec<Diagnostic>) {
64    let mut checker = Checker {
65        diagnostics: Vec::new(),
66        globals: HashMap::new(),
67        program: Program::default(),
68        fn_signatures: Vec::new(),
69        view_params: Vec::new(),
70        fn_effects: FnEffects::default(),
71        body: Body::new(Context::Ui),
72        effects: Effects::default(),
73    };
74    checker.check_files(files);
75    let mut diagnostics = checker.diagnostics;
76    diagnostics.sort_by_key(|diagnostic| diagnostic.span);
77    (checker.program, diagnostics)
78}
79
80impl Checker {
81    fn check_files(&mut self, files: &[&ast::File]) {
82        self.declare_types(files);
83        self.declare_signatures(files);
84        self.declare_windows(files);
85        let state_effects = self.check_states(files);
86        self.check_functions(files);
87        self.check_state_values(&state_effects);
88        self.check_views(files);
89        self.check_top_level(files);
90    }
91
92    fn declare_types(&mut self, files: &[&ast::File]) {
93        let structs: Vec<&ast::StructDecl> = items(files)
94            .filter_map(|item| match item {
95                Item::Struct(decl) => Some(decl),
96                _ => None,
97            })
98            .collect();
99        for decl in &structs {
100            let id = StructId(self.program.structs.len());
101            self.declare_global(&decl.name, Global::Struct(id));
102            self.program.structs.push(StructDef {
103                name: decl.name.name.clone(),
104                fields: Vec::new(),
105            });
106        }
107        for (index, decl) in structs.iter().enumerate() {
108            let mut fields: Vec<FieldDef> = Vec::new();
109            for field in &decl.fields {
110                if fields
111                    .iter()
112                    .any(|existing| existing.name == field.name.name)
113                {
114                    self.error(
115                        format!("field `{}` is declared twice", field.name.name),
116                        field.name.span,
117                    );
118                    continue;
119                }
120                let ty = self.resolve_type(&field.ty);
121                fields.push(FieldDef {
122                    name: field.name.name.clone(),
123                    ty,
124                });
125            }
126            self.program.structs[index].fields = fields;
127        }
128    }
129
130    fn declare_signatures(&mut self, files: &[&ast::File]) {
131        for item in items(files) {
132            match item {
133                Item::Fn(decl) => {
134                    let id = FnId(self.fn_signatures.len());
135                    self.declare_global(&decl.name, Global::Fn(id));
136                    let params = self.resolve_params(&decl.params);
137                    let return_type = decl
138                        .return_type
139                        .as_ref()
140                        .map_or(Type::Void, |ty| self.resolve_type(ty));
141                    self.fn_signatures.push(Signature {
142                        params,
143                        return_type,
144                    });
145                }
146                Item::View(decl) => {
147                    let id = ViewId(self.view_params.len());
148                    self.declare_global(&decl.name, Global::View(id));
149                    let params = self.resolve_params(&decl.params);
150                    self.view_params.push(params);
151                }
152                _ => {}
153            }
154        }
155    }
156
157    /// Names the windows that have one, by their position among the app's windows.
158    fn declare_windows(&mut self, files: &[&ast::File]) {
159        let windows = items(files).filter_map(top_level_call).filter(|(_, stmt)| {
160            ui::element_call(stmt).is_some_and(|call| call.element.kind == ElementKind::Window)
161        });
162        for (index, (name, _)) in windows.enumerate() {
163            if let Some(name) = name {
164                self.declare_global(name, Global::Window(WindowId(index)));
165            }
166        }
167    }
168
169    fn resolve_params(&mut self, params: &[ast::TypedName]) -> Vec<(String, Type)> {
170        let mut resolved: Vec<(String, Type)> = Vec::new();
171        for param in params {
172            if resolved.iter().any(|(name, _)| *name == param.name.name) {
173                self.error(
174                    format!("parameter `{}` is declared twice", param.name.name),
175                    param.name.span,
176                );
177            }
178            let ty = self.resolve_type(&param.ty);
179            resolved.push((param.name.name.clone(), ty));
180        }
181        resolved
182    }
183
184    /// Checks each state's starting value; returns their effects, which are validated once
185    /// the functions they call have been checked.
186    fn check_states(&mut self, files: &[&ast::File]) -> Vec<(Span, Effects)> {
187        let mut all_effects = Vec::new();
188        for item in items(files) {
189            let Item::State(decl) = item else { continue };
190            let annotation = decl.ty.as_ref().map(|ty| self.resolve_type(ty));
191            self.body = Body::new(Context::StateInit);
192            let (init, effects) =
193                self.measure(|checker| checker.expr(&decl.value, annotation.as_ref()));
194            let ty = match annotation {
195                Some(annotation) => {
196                    self.require(&init.ty, &annotation, init.span);
197                    annotation
198                }
199                None => init.ty.clone(),
200            };
201            if ty == Type::Void {
202                self.error("this has no value to start with", init.span);
203            }
204            let id = StateId(self.program.states.len());
205            self.declare_global(&decl.name, Global::State(id));
206            self.program.states.push(StateDef {
207                name: decl.name.name.clone(),
208                ty,
209                init,
210            });
211            all_effects.push((decl.value.span, effects));
212        }
213        all_effects
214    }
215
216    fn check_functions(&mut self, files: &[&ast::File]) {
217        let mut direct = Vec::new();
218        for item in items(files) {
219            let Item::Fn(decl) = item else { continue };
220            let id = FnId(self.program.functions.len());
221            let signature = &self.fn_signatures[id.0];
222            let return_type = signature.return_type.clone();
223            let params = signature.params.clone();
224            self.body = Body::new(Context::Function {
225                return_type: return_type.clone(),
226            });
227            for (name, ty) in &params {
228                self.body.declare(
229                    name,
230                    LocalInfo {
231                        ty: ty.clone(),
232                        kind: LocalKind::Variable,
233                    },
234                );
235            }
236            let (body, effects) = self.measure(|checker| checker.block(&decl.body));
237            if return_type != Type::Void && !stmt::always_returns(&body) {
238                self.error(
239                    format!(
240                        "`{}` can reach its end without returning a `{}`",
241                        decl.name.name,
242                        self.type_name(&return_type)
243                    ),
244                    decl.name.span,
245                );
246            }
247            self.program.functions.push(FnDef {
248                name: decl.name.name.clone(),
249                params: params.len(),
250                return_type,
251                body,
252                locals: local_types(mem::take(&mut self.body.locals)),
253            });
254            direct.push(effects);
255        }
256        self.fn_effects = FnEffects::solve(&direct);
257    }
258
259    fn check_state_values(&mut self, state_effects: &[(Span, Effects)]) {
260        for (span, effects) in state_effects {
261            let (reads, writes) = self.fn_effects.total(effects);
262            if !reads.is_empty() || writes {
263                self.error("a state's starting value can't use other state", *span);
264            }
265        }
266    }
267
268    fn check_views(&mut self, files: &[&ast::File]) {
269        for item in items(files) {
270            let Item::View(decl) = item else { continue };
271            let params = self.view_params[self.program.views.len()].clone();
272            self.body = Body::new(Context::Ui);
273            for (name, ty) in &params {
274                self.body.declare(
275                    name,
276                    LocalInfo {
277                        ty: ty.clone(),
278                        kind: LocalKind::Variable,
279                    },
280                );
281            }
282            let body = self.ui_block(&decl.body.stmts);
283            self.program.views.push(ViewDef {
284                name: decl.name.name.clone(),
285                params: params.len(),
286                body,
287                locals: local_types(mem::take(&mut self.body.locals)),
288            });
289        }
290    }
291
292    /// Windows, then the menus of the menu bar, which share one body for their actions.
293    fn check_top_level(&mut self, files: &[&ast::File]) {
294        let mut top_level_span = None;
295        let mut menus = Vec::new();
296        for (name, stmt) in items(files).filter_map(top_level_call) {
297            top_level_span.get_or_insert(stmt.span);
298            let call = ui::element_call(stmt)
299                .filter(|call| call.element.places.contains(&Place::TopLevel));
300            match call {
301                Some(call) if call.element.kind == ElementKind::Window => {
302                    self.body = Body::new(Context::Ui);
303                    if let Some(window) = self.window(&call, name.is_some()) {
304                        self.program.windows.push(window);
305                    }
306                }
307                Some(menu) => {
308                    if let Some(name) = name {
309                        self.error(
310                            format!("only a `{WINDOW}` takes a name, to open and close it by"),
311                            name.span,
312                        );
313                    }
314                    menus.push(menu);
315                }
316                None => self.error(
317                    format!(
318                        "only declarations, `{WINDOW}(…)` and `{MENU}(…)` can appear at the top level"
319                    ),
320                    stmt.span,
321                ),
322            }
323        }
324        self.body = Body::new(Context::Action);
325        self.program.menu_bar = self.menu_bar(&menus);
326        let span = top_level_span.unwrap_or_default();
327        if self.program.windows.is_empty() {
328            self.error(
329                "a Nifty app needs a `window(\"Title\") { … }` to show",
330                span,
331            );
332        } else if !self.program.windows.iter().any(|window| window.starts_open) {
333            self.error(
334                format!(
335                    "a Nifty app needs a window that opens when it starts; leave `{OPEN}: false` off one"
336                ),
337                span,
338            );
339        }
340    }
341
342    fn declare_global(&mut self, name: &ast::Ident, global: Global) {
343        let built_in = if elements::element(&name.name).is_some() {
344            Some("element")
345        } else if functions::function(&name.name).is_some() {
346            Some("function")
347        } else {
348            None
349        };
350        if let Some(kind) = built_in {
351            self.error(
352                format!("`{}` is a built-in {kind}; choose another name", name.name),
353                name.span,
354            );
355            return;
356        }
357        match self.globals.entry(name.name.clone()) {
358            Entry::Vacant(entry) => {
359                entry.insert(Declared {
360                    global,
361                    span: name.span,
362                });
363            }
364            Entry::Occupied(entry) => {
365                let first = entry.get().span;
366                self.diagnostics.push(
367                    Diagnostic::new(format!("`{}` is already declared", name.name), name.span)
368                        .with_note(format!("`{}` is first declared here", name.name), first),
369                );
370            }
371        }
372    }
373
374    fn global(&self, name: &str) -> Option<Global> {
375        self.globals.get(name).map(|declared| declared.global)
376    }
377
378    fn resolve_type(&mut self, ty: &TypeExpr) -> Type {
379        match &ty.kind {
380            TypeExprKind::List(element) => Type::List(Rc::new(self.resolve_type(element))),
381            TypeExprKind::Named(name) => {
382                if let Some(primitive) = Type::primitive(name) {
383                    primitive
384                } else if let Some(Global::Struct(id)) = self.global(name) {
385                    Type::Struct(id)
386                } else {
387                    self.error(format!("unknown type `{name}`"), ty.span);
388                    Type::Error
389                }
390            }
391        }
392    }
393
394    /// Runs `check` with a fresh record of effects and returns what it read and wrote.
395    fn measure<T>(&mut self, check: impl FnOnce(&mut Self) -> T) -> (T, Effects) {
396        let outer = mem::take(&mut self.effects);
397        let result = check(self);
398        let inner = mem::replace(&mut self.effects, outer);
399        self.effects.merge(&inner);
400        (result, inner)
401    }
402
403    fn type_name(&self, ty: &Type) -> String {
404        if let Some(name) = ty.primitive_name() {
405            return name.to_owned();
406        }
407        match ty {
408            Type::Int | Type::Float | Type::Bool | Type::String => {
409                unreachable!("`Type::primitive_name` spells every primitive type")
410            }
411            Type::List(element) => format!("[{}]", self.type_name(element)),
412            Type::Struct(id) => self.program.structs[id.0].name.clone(),
413            Type::Void => "nothing".to_owned(),
414            Type::Error => "?".to_owned(),
415        }
416    }
417
418    /// Reports a mismatch unless either side already has an error.
419    fn require(&mut self, actual: &Type, expected: &Type, span: Span) {
420        if actual != expected && !actual.is_error() && !expected.is_error() {
421            self.error(
422                format!(
423                    "expected `{}`, found `{}`",
424                    self.type_name(expected),
425                    self.type_name(actual)
426                ),
427                span,
428            );
429        }
430    }
431
432    fn error(&mut self, message: impl Into<String>, span: Span) {
433        self.diagnostics.push(Diagnostic::new(message, span));
434    }
435}
436
437/// Every top-level item of the program, file by file.
438fn items<'ast>(files: &[&'ast ast::File]) -> impl Iterator<Item = &'ast Item> {
439    files.iter().flat_map(|file| &file.items)
440}
441
442/// A top-level statement, such as a window's or a menu's call, and the name written
443/// before it, if any.
444fn top_level_call(item: &Item) -> Option<(Option<&ast::Ident>, &ast::Stmt)> {
445    match item {
446        Item::Named(named) => Some((Some(&named.name), &named.call)),
447        Item::Stmt(stmt) => Some((None, stmt)),
448        Item::State(_) | Item::Struct(_) | Item::Fn(_) | Item::View(_) => None,
449    }
450}
451
452/// The type of each of a body's locals, by id.
453fn local_types(locals: Vec<LocalInfo>) -> Vec<Type> {
454    locals.into_iter().map(|local| local.ty).collect()
455}