Skip to main content

bubbles/compiler/
validate.rs

1//! Compile-time validation of cross-node references.
2
3use crate::compiler::ast::Stmt;
4use crate::compiler::program::Program;
5use crate::error::{DialogueError, Result};
6
7/// Validates all jump and detour targets in `program` refer to existing nodes.
8///
9/// # Errors
10/// Returns [`DialogueError::Validation`] on the first broken reference found.
11pub fn validate(program: &Program) -> Result<()> {
12    for variants in program.nodes.values() {
13        for node in variants {
14            validate_stmts(node.body.as_ref(), program)?;
15        }
16    }
17    Ok(())
18}
19
20fn validate_stmts(stmts: &[Stmt], program: &Program) -> Result<()> {
21    for stmt in stmts {
22        match stmt {
23            Stmt::Jump(target) | Stmt::Detour(target) if !program.node_exists(target) => {
24                return Err(DialogueError::Validation(format!(
25                    "reference to unknown node '{target}'"
26                )));
27            }
28            Stmt::If {
29                branches,
30                else_body,
31            } => {
32                for b in branches {
33                    validate_stmts(&b.body, program)?;
34                }
35                validate_stmts(else_body, program)?;
36            }
37            Stmt::Once {
38                body, else_body, ..
39            } => {
40                validate_stmts(body, program)?;
41                validate_stmts(else_body, program)?;
42            }
43            Stmt::Options(items) => {
44                for item in items {
45                    validate_stmts(&item.body, program)?;
46                }
47            }
48            _ => {}
49        }
50    }
51    Ok(())
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use crate::compiler::compile;
58
59    #[test]
60    fn valid_jump_passes() {
61        let prog = compile("title: A\n---\n<<jump B>>\n===\ntitle: B\n---\n===\n").unwrap();
62        assert!(validate(&prog).is_ok());
63    }
64
65    #[test]
66    fn unknown_jump_target_fails() {
67        let prog = compile("title: A\n---\n<<jump Nonexistent>>\n===\n").unwrap();
68        assert!(validate(&prog).is_err());
69    }
70
71    #[test]
72    fn unknown_jump_inside_if_branch_fails() {
73        let prog =
74            compile("title: A\n---\n<<if true>>\n    <<jump Ghost>>\n<<endif>>\n===\n").unwrap();
75        assert!(validate(&prog).is_err());
76    }
77
78    #[test]
79    fn unknown_detour_in_else_fails() {
80        let prog = compile(
81            "title: A\n---\n<<if false>>\n    idle\n<<else>>\n    <<detour Missing>>\n<<endif>>\n===\n",
82        )
83        .unwrap();
84        assert!(validate(&prog).is_err());
85    }
86
87    #[test]
88    fn unknown_jump_inside_once_fails() {
89        let prog =
90            compile("title: A\n---\n<<once>>\n    <<jump Nope>>\n<<endonce>>\n===\n").unwrap();
91        assert!(validate(&prog).is_err());
92    }
93
94    #[test]
95    fn unknown_jump_inside_option_body_fails() {
96        let prog = compile("title: A\n---\n-> Go\n    <<jump Bad>>\n===\n").unwrap();
97        assert!(validate(&prog).is_err());
98    }
99
100    #[test]
101    fn unknown_jump_inside_once_else_fails() {
102        let prog = compile(
103            "title: A\n---\n<<once>>\n    ok\n<<else>>\n    <<jump Nope>>\n<<endonce>>\n===\n",
104        )
105        .unwrap();
106        assert!(validate(&prog).is_err());
107    }
108}