Skip to main content

ocelot_parser/
parse_script.rs

1use crate::parser::Parser;
2use ocelot_ast::script::Script;
3use ocelot_base::compilation_context::CompilationContext;
4use ocelot_base::result::OcelotResult;
5use ocelot_base::source_file::SourceFile;
6
7/// Parses a source file into a script AST.
8pub fn parse_script(
9    source_file: &SourceFile,
10    compilation_context: &mut CompilationContext,
11) -> OcelotResult<Option<Script>> {
12    Parser::new(source_file, compilation_context).parse_script()
13}
14
15#[cfg(test)]
16mod tests {
17    use super::parse_script;
18    use ocelot_ast::expression_kind::ExpressionKind;
19    use ocelot_ast::item_kind::ItemKind;
20    use ocelot_ast::statement_kind::StatementKind;
21    use ocelot_base::compilation_context::CompilationContext;
22    use ocelot_base::diagnostic_level::DiagnosticLevel;
23    use ocelot_base::source_file::SourceFile;
24    use ocelot_base::span::Span;
25
26    #[test]
27    fn parses_println_string_statement() {
28        let source_file = SourceFile::new("examples/hello.ocelot", "println(\"hello\");");
29        let mut context = CompilationContext::default();
30
31        let script = parse_script(&source_file, &mut context).unwrap().unwrap();
32
33        assert_eq!(script.items.len(), 1);
34        assert!(!context.has_errors());
35
36        match &script.items[0].kind {
37            ItemKind::Statement(statement) => match &statement.kind {
38                StatementKind::Println(println_statement) => match &println_statement.argument.kind
39                {
40                    ExpressionKind::StringLiteral(string_literal) => {
41                        assert_eq!(string_literal.value, "hello");
42                    }
43                    other => panic!("expected string literal, got {other:?}"),
44                },
45            },
46            other => panic!("expected statement item, got {other:?}"),
47        }
48    }
49
50    #[test]
51    fn parses_multiple_println_statements() {
52        let source_file = SourceFile::new(
53            "examples/two-lines.ocelot",
54            "println(\"first\"); println(\"second\");",
55        );
56        let mut context = CompilationContext::default();
57
58        let script = parse_script(&source_file, &mut context).unwrap().unwrap();
59
60        assert_eq!(script.items.len(), 2);
61        assert_eq!(script.span.start(), 0);
62        assert_eq!(script.span.end(), source_file.source().len());
63        assert!(!context.has_errors());
64    }
65
66    #[test]
67    fn parses_test_items_alongside_script_statements() {
68        let source_file = SourceFile::new(
69            "examples/tests.ocelot",
70            "println(\"setup\"); test \"prints one line\" { println(\"hello\"); }",
71        );
72        let mut context = CompilationContext::default();
73
74        let script = parse_script(&source_file, &mut context).unwrap().unwrap();
75
76        assert_eq!(script.items.len(), 2);
77        assert!(!context.has_errors());
78
79        match &script.items[1].kind {
80            ItemKind::Test(test_item) => {
81                assert_eq!(test_item.name, "prints one line");
82                assert_eq!(test_item.body.len(), 1);
83            }
84            other => panic!("expected test item, got {other:?}"),
85        }
86    }
87
88    #[test]
89    fn reports_a_missing_test_name_as_a_source_diagnostic() {
90        let source_file = SourceFile::new("examples/invalid.ocelot", "test { println(\"x\"); }");
91        let mut context = CompilationContext::default();
92
93        let script = parse_script(&source_file, &mut context).unwrap();
94
95        assert!(script.is_none());
96        assert!(context.has_errors());
97        assert_eq!(context.source_diagnostics.diagnostics.len(), 1);
98        assert_eq!(
99            context.source_diagnostics.diagnostics[0].message,
100            "expected test name string"
101        );
102        assert_eq!(
103            context.source_diagnostics.diagnostics[0].excerpts[0].annotations[0].span,
104            Span::new(5, 6)
105        );
106    }
107
108    #[test]
109    fn reports_an_unterminated_test_body_as_a_source_diagnostic() {
110        let source_file = SourceFile::new(
111            "examples/invalid.ocelot",
112            "test \"broken\" { println(\"hello\");",
113        );
114        let mut context = CompilationContext::default();
115
116        let script = parse_script(&source_file, &mut context).unwrap();
117
118        assert!(script.is_none());
119        assert!(context.has_errors());
120        assert_eq!(context.source_diagnostics.diagnostics.len(), 1);
121        assert_eq!(
122            context.source_diagnostics.diagnostics[0].message,
123            "expected `}` to close test body"
124        );
125    }
126
127    #[test]
128    fn reports_zero_argument_println_as_a_source_diagnostic() {
129        let source_file = SourceFile::new("examples/invalid.ocelot", "println();");
130        let mut context = CompilationContext::default();
131
132        let script = parse_script(&source_file, &mut context).unwrap();
133
134        assert!(script.is_none());
135        assert!(context.has_errors());
136        assert_eq!(
137            context.source_diagnostics.diagnostics[0].message,
138            "type error: `println` expects exactly one argument"
139        );
140    }
141
142    #[test]
143    fn reports_unexpected_statement_names_as_a_source_diagnostic() {
144        let source_file = SourceFile::new("examples/invalid.ocelot", "print(\"hello\");");
145        let mut context = CompilationContext::default();
146
147        let script = parse_script(&source_file, &mut context).unwrap();
148
149        assert!(script.is_none());
150        assert_eq!(
151            context.source_diagnostics.diagnostics[0].message,
152            "expected `println` statement"
153        );
154    }
155
156    #[test]
157    fn surfaces_lexer_diagnostics_through_the_shared_compilation_context() {
158        let source_file = SourceFile::new("examples/invalid.ocelot", "println(\"hello);");
159        let mut context = CompilationContext::default();
160
161        let script = parse_script(&source_file, &mut context).unwrap();
162
163        assert!(script.is_none());
164        assert!(context.has_errors());
165        assert_eq!(context.source_diagnostics.diagnostics.len(), 1);
166        assert_eq!(
167            context.source_diagnostics.diagnostics[0].level,
168            DiagnosticLevel::Error
169        );
170        assert_eq!(
171            context.source_diagnostics.diagnostics[0].message,
172            "unterminated string literal"
173        );
174    }
175}