ocelot_parser/
parse_script.rs1use 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
7pub 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}