1use plsql_core::{Diagnostic, Evidence, Severity, SymbolInterner};
22use plsql_parser::Ast;
23use plsql_parser::ast::AstDecl;
24use tracing::instrument;
25
26const ANTLR_RULE_PATH_EVIDENCE_CODE: &str = "ANTLR_RULE_PATH";
32const ANTLR_RULE_PATH_ATTR_KEY: &str = "antlr_rule_path";
33
34fn stamp_antlr_rule_path(diag: Diagnostic, rule_path: Option<&str>) -> Diagnostic {
42 match rule_path {
43 Some(path) if !path.is_empty() => diag.with_evidence(
44 Evidence::new(
45 ANTLR_RULE_PATH_EVIDENCE_CODE,
46 "ANTLR grammar rule position of the unlowered declaration",
47 )
48 .with_attribute(
49 ANTLR_RULE_PATH_ATTR_KEY,
50 serde_json::Value::String(path.to_string()),
51 ),
52 ),
53 _ => diag,
54 }
55}
56
57use crate::decl::{
58 DeclCommon, Declaration, FunctionDecl, PackageDecl, ProcedureDecl, TriggerDecl, TypeDecl,
59 ViewDecl,
60};
61
62#[derive(Clone, Debug, Default, PartialEq)]
64pub struct LoweredFile {
65 pub declarations: Vec<Declaration>,
68 pub diagnostics: Vec<Diagnostic>,
71}
72
73impl LoweredFile {
74 #[must_use]
75 pub fn is_empty(&self) -> bool {
76 self.declarations.is_empty() && self.diagnostics.is_empty()
77 }
78}
79
80#[must_use]
86#[instrument(level = "trace", skip(ast, interner))]
87pub fn lower_top_level(ast: &Ast, interner: &mut SymbolInterner) -> LoweredFile {
88 let mut out = LoweredFile::default();
89 for decl in &ast.root.declarations {
90 match decl {
91 AstDecl::PackageSpec { name, span } => {
92 let common = make_common(name, *span, interner);
93 out.declarations.push(Declaration::Package(PackageDecl {
94 common,
95 members: Vec::new(),
96 body: None,
97 }));
98 }
99 AstDecl::PackageBody { name, span } => {
100 let common = make_common(name, *span, interner);
106 out.declarations.push(Declaration::Package(PackageDecl {
107 common,
108 members: Vec::new(),
109 body: None,
110 }));
111 }
112 AstDecl::Procedure { name, span } => {
113 let common = make_common(name, *span, interner);
114 out.declarations.push(Declaration::Procedure(ProcedureDecl {
115 common,
116 params: Vec::new(),
117 }));
118 }
119 AstDecl::Function { name, span } => {
120 let common = make_common(name, *span, interner);
121 out.declarations.push(Declaration::Function(FunctionDecl {
122 common,
123 params: Vec::new(),
124 return_type: None,
125 }));
126 }
127 AstDecl::Trigger { name, span } => {
128 let common = make_common(name, *span, interner);
129 out.declarations
130 .push(Declaration::Trigger(TriggerDecl { common }));
131 }
132 AstDecl::View { name, span } => {
133 let common = make_common(name, *span, interner);
134 out.declarations.push(Declaration::View(ViewDecl {
135 common,
136 columns: Vec::new(),
137 }));
138 }
139 AstDecl::TypeSpec { name, span } | AstDecl::TypeBody { name, span } => {
140 let common = make_common(name, *span, interner);
141 out.declarations
142 .push(Declaration::Type(TypeDecl { common }));
143 }
144 AstDecl::Ddl {
145 kind,
146 span,
147 antlr_rule_path,
148 } => {
149 let mut diagnostic = Diagnostic::new(
153 "IR_DDL_NOT_LOWERED",
154 Severity::Info,
155 format!("DDL `{kind}` recorded but not lowered (handled by ChangeSet path)"),
156 );
157 diagnostic.primary_span = Some(*span);
158 let diagnostic = stamp_antlr_rule_path(diagnostic, antlr_rule_path.as_deref());
162 out.diagnostics.push(diagnostic);
163 }
164 AstDecl::Unknown {
165 span,
166 antlr_rule_path,
167 } => {
168 let mut diagnostic = Diagnostic::new(
172 "IR_UNCLASSIFIED_DECL",
173 Severity::Warn,
174 "AST classifier returned `Unknown` — declaration not lowered",
175 );
176 diagnostic.primary_span = Some(*span);
177 diagnostic
178 .unknown_reasons
179 .push(plsql_core::UnknownReason::ParserRecoveryRegion);
180 let diagnostic = stamp_antlr_rule_path(diagnostic, antlr_rule_path.as_deref());
181 out.diagnostics.push(diagnostic);
182 }
183 }
184 }
185 out
186}
187
188fn make_common(name: &str, span: plsql_core::Span, interner: &mut SymbolInterner) -> DeclCommon {
189 let interned = interner.intern(name).unwrap_or_else(|| {
190 plsql_core::SymbolId::new(0)
195 });
196 DeclCommon::new(interned, span)
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use plsql_core::{FileId, Position};
203 use plsql_parser::ast::SourceFile;
204
205 fn span(offset: u32, len: u32) -> plsql_core::Span {
206 plsql_core::Span::new(
207 FileId::new(0),
208 Position::new(1, 1, offset),
209 Position::new(1, 1, offset + len),
210 )
211 }
212
213 fn ast_with(decls: Vec<AstDecl>) -> Ast {
214 Ast {
215 root: SourceFile {
216 span: span(0, 0),
217 declarations: decls,
218 },
219 source_map: plsql_parser::ast::SourceMap::new(),
220 body_statements: Vec::new(),
221 }
222 }
223
224 #[test]
225 fn empty_ast_yields_empty_lowered_file() {
226 let mut interner = SymbolInterner::new();
227 let out = lower_top_level(&ast_with(vec![]), &mut interner);
228 assert!(out.is_empty());
229 }
230
231 #[test]
232 fn package_spec_lowers_to_package_decl() {
233 let mut interner = SymbolInterner::new();
234 let out = lower_top_level(
235 &ast_with(vec![AstDecl::PackageSpec {
236 name: String::from("BILLING_API"),
237 span: span(0, 12),
238 }]),
239 &mut interner,
240 );
241 assert_eq!(out.declarations.len(), 1);
242 assert!(matches!(out.declarations[0], Declaration::Package(_)));
243 let symbol = out.declarations[0].common().name;
245 assert_eq!(interner.resolve(symbol), Some("BILLING_API"));
246 }
247
248 #[test]
249 fn body_pairs_with_spec_in_source_order() {
250 let mut interner = SymbolInterner::new();
251 let out = lower_top_level(
252 &ast_with(vec![
253 AstDecl::PackageSpec {
254 name: String::from("BILLING_API"),
255 span: span(0, 12),
256 },
257 AstDecl::PackageBody {
258 name: String::from("BILLING_API"),
259 span: span(13, 12),
260 },
261 ]),
262 &mut interner,
263 );
264 assert_eq!(out.declarations.len(), 2);
265 assert!(out.diagnostics.is_empty());
266 }
267
268 #[test]
269 fn procedure_function_trigger_view_each_lower() {
270 let mut interner = SymbolInterner::new();
271 let out = lower_top_level(
272 &ast_with(vec![
273 AstDecl::Procedure {
274 name: String::from("RESET_BALANCE"),
275 span: span(0, 8),
276 },
277 AstDecl::Function {
278 name: String::from("CURRENT_BALANCE"),
279 span: span(10, 8),
280 },
281 AstDecl::Trigger {
282 name: String::from("INVOICES_BIU"),
283 span: span(20, 8),
284 },
285 AstDecl::View {
286 name: String::from("V_BALANCE"),
287 span: span(30, 8),
288 },
289 AstDecl::TypeSpec {
290 name: String::from("ADDRESS_T"),
291 span: span(40, 8),
292 },
293 AstDecl::TypeBody {
294 name: String::from("ADDRESS_T"),
295 span: span(50, 8),
296 },
297 ]),
298 &mut interner,
299 );
300 assert_eq!(out.declarations.len(), 6);
301 assert!(matches!(out.declarations[0], Declaration::Procedure(_)));
303 assert!(matches!(out.declarations[1], Declaration::Function(_)));
304 assert!(matches!(out.declarations[2], Declaration::Trigger(_)));
305 assert!(matches!(out.declarations[3], Declaration::View(_)));
306 assert!(matches!(out.declarations[4], Declaration::Type(_)));
307 assert!(matches!(out.declarations[5], Declaration::Type(_)));
308 }
309
310 #[test]
311 fn ddl_emits_informational_diagnostic_no_declaration() {
312 let mut interner = SymbolInterner::new();
313 let out = lower_top_level(
314 &ast_with(vec![AstDecl::Ddl {
315 kind: String::from("CREATE TABLE"),
316 span: span(0, 12),
317 antlr_rule_path: None,
318 }]),
319 &mut interner,
320 );
321 assert!(out.declarations.is_empty());
322 assert_eq!(out.diagnostics.len(), 1);
323 assert_eq!(out.diagnostics[0].code, "IR_DDL_NOT_LOWERED");
324 assert_eq!(out.diagnostics[0].severity, Severity::Info);
325 assert!(
327 out.diagnostics[0]
328 .evidence
329 .iter()
330 .all(|e| e.code != "ANTLR_RULE_PATH")
331 );
332 }
333
334 #[test]
335 fn ddl_rule_path_is_stamped_as_capture_evidence() {
336 let mut interner = SymbolInterner::new();
337 let out = lower_top_level(
338 &ast_with(vec![AstDecl::Ddl {
339 kind: String::from("CREATE SEQUENCE"),
340 span: span(0, 15),
341 antlr_rule_path: Some(String::from("unit_statement>create_sequence")),
342 }]),
343 &mut interner,
344 );
345 let diag = &out.diagnostics[0];
346 assert_eq!(diag.code, "IR_DDL_NOT_LOWERED");
347 let ev = diag
350 .evidence
351 .iter()
352 .find(|e| e.code == "ANTLR_RULE_PATH")
353 .expect("rule-path evidence must be stamped");
354 assert_eq!(
355 ev.attributes
356 .get("antlr_rule_path")
357 .and_then(|v| v.as_str()),
358 Some("unit_statement>create_sequence")
359 );
360 }
361
362 #[test]
363 fn unknown_decl_emits_typed_warning_with_unknown_reason() {
364 let mut interner = SymbolInterner::new();
365 let out = lower_top_level(
366 &ast_with(vec![AstDecl::Unknown {
367 span: span(0, 4),
368 antlr_rule_path: None,
369 }]),
370 &mut interner,
371 );
372 assert!(out.declarations.is_empty());
373 assert_eq!(out.diagnostics.len(), 1);
374 assert_eq!(out.diagnostics[0].code, "IR_UNCLASSIFIED_DECL");
375 assert_eq!(out.diagnostics[0].severity, Severity::Warn);
376 assert!(
377 out.diagnostics[0]
378 .unknown_reasons
379 .contains(&plsql_core::UnknownReason::ParserRecoveryRegion)
380 );
381 }
382
383 #[test]
384 fn span_propagates_into_declcommon() {
385 let mut interner = SymbolInterner::new();
386 let in_span = span(42, 8);
387 let out = lower_top_level(
388 &ast_with(vec![AstDecl::Procedure {
389 name: String::from("FOO"),
390 span: in_span,
391 }]),
392 &mut interner,
393 );
394 assert_eq!(out.declarations[0].common().span, in_span);
395 }
396}