tenda_runtime/
runtime_error.rs

1use tenda_common::span::SourceSpan;
2use tenda_reporting::{Diagnostic, DiagnosticConfig, HasDiagnosticHooks};
3use tenda_reporting_derive::Diagnostic;
4use thiserror::Error;
5
6use crate::{
7    associative_array::AssociativeArrayKey,
8    value::{Value, ValueType},
9};
10
11pub type Result<T> = std::result::Result<T, Box<RuntimeError>>;
12
13#[derive(Debug, Clone, PartialEq)]
14pub enum FunctionName {
15    Anonymous,
16    TopLevel,
17    Named(String),
18}
19
20#[derive(Debug, Clone, PartialEq)]
21pub struct StackFrame {
22    pub function_name: FunctionName,
23    pub location: Option<SourceSpan>,
24}
25
26impl StackFrame {
27    pub fn new(function_name: FunctionName, location: Option<SourceSpan>) -> Self {
28        Self {
29            function_name,
30            location,
31        }
32    }
33}
34
35impl From<StackFrame> for tenda_reporting::StackFrame<SourceSpan> {
36    fn from(val: StackFrame) -> Self {
37        let function_name = match val.function_name {
38            FunctionName::Anonymous => "<anônimo>".to_string(),
39            FunctionName::TopLevel => "<raiz>".to_string(),
40            FunctionName::Named(name) => name,
41        };
42
43        tenda_reporting::StackFrame::new(function_name, val.location)
44    }
45}
46
47#[derive(Error, Debug, PartialEq, Clone, Diagnostic)]
48#[accept_hooks]
49#[report("erro de execução")]
50pub enum RuntimeError {
51    #[error("divisão por zero não é permitida")]
52    DivisionByZero {
53        #[span]
54        span: Option<SourceSpan>,
55
56        #[metadata]
57        stacktrace: Vec<StackFrame>,
58    },
59
60    #[error("operação inválida para os tipos '{}' e '{}'", .first.to_string(), .second.to_string())]
61    TypeMismatch {
62        first: ValueType,
63        second: ValueType,
64
65        #[span]
66        span: Option<SourceSpan>,
67
68        #[message]
69        message: Option<String>,
70
71        #[metadata]
72        stacktrace: Vec<StackFrame>,
73    },
74
75    #[error("esperado valor de tipo '{}', encontrado '{}'", .expected.to_string(), .found.to_string())]
76    UnexpectedTypeError {
77        expected: ValueType,
78        found: ValueType,
79
80        #[message]
81        message: Option<String>,
82
83        #[span]
84        span: Option<SourceSpan>,
85
86        #[metadata]
87        stacktrace: Vec<StackFrame>,
88    },
89
90    #[error("a variável identificada por '{}' não está definida neste escopo", .var_name)]
91    UndefinedReference {
92        var_name: String,
93
94        #[span]
95        span: Option<SourceSpan>,
96
97        #[help]
98        help: Option<String>,
99
100        #[metadata]
101        stacktrace: Vec<StackFrame>,
102    },
103
104    #[error("variável identificada por {0} já está declarada neste escopo", .var_name)]
105    AlreadyDeclared {
106        var_name: String,
107
108        #[span]
109        span: Option<SourceSpan>,
110
111        #[help]
112        help: Option<String>,
113
114        #[metadata]
115        stacktrace: Vec<StackFrame>,
116    },
117
118    #[error("número de argumentos incorreto: esperado {}, encontrado {}", .expected, .found)]
119    WrongNumberOfArguments {
120        expected: usize,
121        found: usize,
122
123        #[span]
124        span: Option<SourceSpan>,
125
126        #[metadata]
127        stacktrace: Vec<StackFrame>,
128    },
129
130    #[error("índice fora dos limites: índice {}, tamanho {}", .index, .len)]
131    IndexOutOfBounds {
132        index: usize,
133        len: usize,
134
135        #[span]
136        span: Option<SourceSpan>,
137
138        #[help]
139        help: Vec<String>,
140
141        #[metadata]
142        stacktrace: Vec<StackFrame>,
143    },
144
145    #[error("não é possível acessar um valor do tipo '{}'", .value.to_string())]
146    WrongIndexType {
147        value: ValueType,
148
149        #[span]
150        span: Option<SourceSpan>,
151
152        #[metadata]
153        stacktrace: Vec<StackFrame>,
154    },
155
156    #[error("limites de intervalo precisam ser números inteiros finitos: encontrado '{}'", .bound)]
157    InvalidRangeBounds {
158        bound: f64,
159
160        #[span]
161        span: Option<SourceSpan>,
162
163        #[metadata]
164        stacktrace: Vec<StackFrame>,
165    },
166
167    #[error("índice de lista precisa ser um número inteiro positivo e finito: encontrado '{}'", .index)]
168    InvalidIndex {
169        index: f64,
170
171        #[span]
172        span: Option<SourceSpan>,
173
174        #[metadata]
175        stacktrace: Vec<StackFrame>,
176    },
177
178    #[error("chave de dicionário precisa ser número inteiro ou texto: encontrado '{}'", .key)]
179    InvalidNumberAssociativeArrayKey {
180        key: f64,
181
182        #[span]
183        span: Option<SourceSpan>,
184
185        #[metadata]
186        stacktrace: Vec<StackFrame>,
187    },
188
189    #[error("chave de dicionário precisa ser número inteiro ou texto: encontrado '{}'", .key)]
190    InvalidTypeAssociativeArrayKey {
191        key: ValueType,
192
193        #[span]
194        span: Option<SourceSpan>,
195
196        #[metadata]
197        stacktrace: Vec<StackFrame>,
198    },
199
200    #[error("chave de dicionário não encontrada: '{}'", .key.to_string())]
201    AssociativeArrayKeyNotFound {
202        key: AssociativeArrayKey,
203
204        #[span]
205        span: Option<SourceSpan>,
206
207        #[metadata]
208        stacktrace: Vec<StackFrame>,
209    },
210
211    #[error("não é possível iterar sobre um valor do tipo '{}'", .value.to_string())]
212    NotIterable {
213        value: ValueType,
214
215        #[span]
216        span: Option<SourceSpan>,
217
218        #[metadata]
219        stacktrace: Vec<StackFrame>,
220    },
221
222    #[error("o valor do tipo '{}' não é um argumento válido para a função", .value.to_string())]
223    InvalidArgument {
224        value: Value,
225
226        #[span]
227        span: Option<SourceSpan>,
228
229        #[metadata]
230        stacktrace: Vec<StackFrame>,
231    },
232
233    #[error("textos são imutáveis e não podem ser modificados")]
234    ImmutableString {
235        #[span]
236        span: Option<SourceSpan>,
237
238        #[help]
239        help: Option<String>,
240
241        #[metadata]
242        stacktrace: Vec<StackFrame>,
243    },
244
245    #[error("timestamp inválido: {}", .timestamp.to_string())]
246    InvalidTimestamp {
247        timestamp: i64,
248
249        #[span]
250        span: Option<SourceSpan>,
251
252        #[metadata]
253        stacktrace: Vec<StackFrame>,
254    },
255
256    #[error("falha ao analisar data ISO: {}", .source)]
257    DateIsoParseError {
258        source: chrono::ParseError,
259
260        #[span]
261        span: Option<SourceSpan>,
262
263        #[metadata]
264        stacktrace: Vec<StackFrame>,
265    },
266
267    #[error("fuso horário inválido: '{tz_str}'")]
268    InvalidTimeZoneString {
269        tz_str: String,
270
271        #[span]
272        span: Option<SourceSpan>,
273
274        #[metadata]
275        stacktrace: Vec<StackFrame>,
276    },
277
278    #[error("valor inválido para conversão para tipo '{}'", .value.to_string())]
279    InvalidValueForConversion {
280        value: Value,
281
282        #[span]
283        span: Option<SourceSpan>,
284
285        #[metadata]
286        stacktrace: Vec<StackFrame>,
287    },
288}
289
290impl HasDiagnosticHooks<SourceSpan> for RuntimeError {
291    fn hooks() -> &'static [fn(&Self, DiagnosticConfig<SourceSpan>) -> DiagnosticConfig<SourceSpan>]
292    {
293        &[add_stacktrace]
294    }
295}
296
297/// Builds a Vec<StackFrame> from a RuntimeError’s stacktrace:
298/// - Returns `None` if there’s no stacktrace or it’s empty.
299/// - Otherwise, returns `Some(frames)` where `frames.len() == original.len() + 1`:
300///     1. **First frame**: the first entry’s function name paired with the error span.
301///     2. **Middle frames**: for each adjacent pair in the original stacktrace,
302///        the later function name is paired with the previous call-site location.
303///     3. **Last frame**: a “top-level” placeholder (`FunctionName::TopLevel`)
304///        paired with the last call-site location.
305///
306/// Note: at the moment in the runtime we’re still associating function names with
307/// their call sites rather than their actual in-function error sites.
308/// This is a temporary workaround until we can implement a better solution.
309fn build_stack_frames(runtime_error: &RuntimeError) -> Option<Vec<StackFrame>> {
310    let st = runtime_error.get_stacktrace()?;
311
312    if st.is_empty() {
313        return None;
314    }
315
316    let mut frames = Vec::with_capacity(st.len() + 1);
317
318    frames.push(StackFrame::new(
319        st[0].function_name.clone(),
320        runtime_error.get_span().clone(),
321    ));
322
323    for window in st.windows(2) {
324        let (prev, curr) = (&window[0], &window[1]);
325
326        frames.push(StackFrame::new(
327            curr.function_name.clone(),
328            prev.location.clone(),
329        ));
330    }
331
332    frames.push(StackFrame::new(
333        FunctionName::TopLevel,
334        st.last().unwrap().location.clone(),
335    ));
336
337    Some(frames)
338}
339
340fn add_stacktrace(
341    runtime_error: &RuntimeError,
342    config: DiagnosticConfig<SourceSpan>,
343) -> DiagnosticConfig<SourceSpan> {
344    match build_stack_frames(runtime_error) {
345        Some(frames) => config.stacktrace(frames.into_iter().map(Into::into).collect()),
346        None => config,
347    }
348}
349
350macro_rules! attach_span_if_missing {
351    ($err:expr, $span:expr) => {{
352        if $err.get_span().is_none() {
353            $err.set_span($span);
354        }
355
356        $err
357    }};
358}
359
360pub(crate) use attach_span_if_missing;