devalang_core/utils/
error.rs

1use super::logger::{LogLevel, Logger};
2use crate::core::{
3    error::{ErrorResult, Severity, StackFrame},
4    parser::statement::{Statement, StatementKind},
5    shared::value::Value,
6};
7use std::collections::HashMap;
8
9/// Recursively collects errors from a list of statements.
10///
11/// This function traverses the provided statements and aggregates any
12/// `Unknown` or explicit `Error` statements into a flat vector.
13/// It also descends into loop bodies to ensure nested errors are
14/// surfaced.
15pub fn collect_errors_recursively(statements: &[Statement]) -> Vec<ErrorResult> {
16    let mut errors: Vec<ErrorResult> = Vec::new();
17
18    for stmt in statements {
19        match &stmt.kind {
20            StatementKind::Unknown => {
21                errors.push(ErrorResult {
22                    message: format!("Unknown statement at line {}:{}", stmt.line, stmt.column),
23                    line: stmt.line,
24                    column: stmt.column,
25                    severity: Severity::Warning,
26                    stack: vec![StackFrame {
27                        module: None,
28                        context: Some("Unknown".to_string()),
29                        line: stmt.line,
30                        column: stmt.column,
31                    }],
32                });
33            }
34            StatementKind::Error { message } => {
35                errors.push(ErrorResult {
36                    message: message.clone(),
37                    line: stmt.line,
38                    column: stmt.column,
39                    severity: Severity::Critical,
40                    stack: vec![StackFrame {
41                        module: None,
42                        context: Some("Error".to_string()),
43                        line: stmt.line,
44                        column: stmt.column,
45                    }],
46                });
47            }
48            StatementKind::Loop => {
49                if let Some(body_statements) = extract_loop_body_statements(&stmt.value) {
50                    let nested = collect_errors_recursively(body_statements);
51                    errors.extend(nested.into_iter().map(|mut e| {
52                        // push parent frame contextually for a basic callstack-like trace
53                        e.stack.insert(
54                            0,
55                            StackFrame {
56                                module: None,
57                                context: Some("loop".to_string()),
58                                line: stmt.line,
59                                column: stmt.column,
60                            },
61                        );
62                        e
63                    }));
64                }
65            }
66            _ => {}
67        }
68    }
69
70    errors
71}
72
73fn extract_loop_body_statements(value: &Value) -> Option<&[Statement]> {
74    if let Value::Map(map) = value {
75        if let Some(Value::Block(statements)) = map.get("body") {
76            return Some(statements);
77        }
78    }
79    None
80}
81
82pub fn partition_errors(errors: Vec<ErrorResult>) -> (Vec<ErrorResult>, Vec<ErrorResult>) {
83    let mut warnings = Vec::new();
84    let mut criticals = Vec::new();
85    for e in errors {
86        match e.severity {
87            Severity::Warning => warnings.push(e),
88            Severity::Critical => criticals.push(e),
89        }
90    }
91    (warnings, criticals)
92}
93
94pub fn log_errors_with_stack(prefix: &str, warnings: &[ErrorResult], criticals: &[ErrorResult]) {
95    let logger = Logger::new();
96    if !warnings.is_empty() {
97        logger.log_message(
98            LogLevel::Warning,
99            &format!("{}: {} warning(s)", prefix, warnings.len()),
100        );
101        for w in warnings {
102            logger.log_message(LogLevel::Warning, &format!("- {}", w.message));
103            // Print primary location in path:line:col if available
104            if let Some(frame) = w.stack.first() {
105                let module = frame.module.clone().unwrap_or_default();
106                logger.log_message(
107                    LogLevel::Debug,
108                    &format!(
109                        "     ↳ {}:{}:{} {}",
110                        module,
111                        frame.line,
112                        frame.column,
113                        frame.context.clone().unwrap_or_default()
114                    ),
115                );
116            }
117            // Print remaining frames if any
118            if w.stack.len() > 1 {
119                for (i, f) in w.stack.iter().enumerate().skip(1) {
120                    let module = f.module.clone().unwrap_or_default();
121                    logger.log_message(
122                        LogLevel::Debug,
123                        &format!(
124                            "       #{} {}:{}:{} {}",
125                            i,
126                            module,
127                            f.line,
128                            f.column,
129                            f.context.clone().unwrap_or_default()
130                        ),
131                    );
132                }
133            }
134        }
135    }
136    if !criticals.is_empty() {
137        logger.log_message(
138            LogLevel::Error,
139            &format!("{}: {} critical error(s)", prefix, criticals.len()),
140        );
141        for c in criticals {
142            logger.log_message(LogLevel::Error, &format!("- {}", c.message));
143            if let Some(frame) = c.stack.first() {
144                let module = frame.module.clone().unwrap_or_default();
145                logger.log_message(
146                    LogLevel::Error,
147                    &format!(
148                        "     ↳ {}:{}:{} {}",
149                        module,
150                        frame.line,
151                        frame.column,
152                        frame.context.clone().unwrap_or_default()
153                    ),
154                );
155            }
156            if c.stack.len() > 1 {
157                for (i, f) in c.stack.iter().enumerate().skip(1) {
158                    let module = f.module.clone().unwrap_or_default();
159                    logger.log_message(
160                        LogLevel::Error,
161                        &format!(
162                            "       #{} {}:{}:{} {}",
163                            i,
164                            module,
165                            f.line,
166                            f.column,
167                            f.context.clone().unwrap_or_default()
168                        ),
169                    );
170                }
171            }
172        }
173    }
174}
175
176pub fn collect_all_errors_with_modules(
177    modules: &HashMap<String, Vec<Statement>>,
178) -> Vec<ErrorResult> {
179    let mut all = Vec::new();
180    for (module_path, stmts) in modules {
181        let mut errs = collect_errors_recursively(stmts);
182        for e in errs.iter_mut() {
183            // ensure top frame carries the module path
184            if e.stack.is_empty() {
185                e.stack.push(StackFrame {
186                    module: Some(module_path.clone()),
187                    context: None,
188                    line: e.line,
189                    column: e.column,
190                });
191            } else {
192                if e.stack[0].module.is_none() {
193                    e.stack[0].module = Some(module_path.clone());
194                }
195            }
196        }
197        all.extend(errs);
198    }
199    all
200}