-
Notifications
You must be signed in to change notification settings - Fork 114
Expand file tree
/
Copy patherror.rs
More file actions
653 lines (545 loc) · 22.4 KB
/
error.rs
File metadata and controls
653 lines (545 loc) · 22.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
//! Interpreter error types and diagnostic conversion.
//!
//! This module defines the error types that can occur during MIR interpretation
//! and provides conversion to diagnostics for user-friendly error reporting.
use alloc::borrow::Cow;
use core::{
alloc::Allocator,
fmt::{self, Display},
};
use hashql_core::{span::SpanId, symbol::Symbol};
use hashql_diagnostics::{
Diagnostic, Label,
category::{DiagnosticCategory, TerminalDiagnosticCategory},
diagnostic::Message,
severity::Severity,
};
use hashql_hir::node::operation::UnOp;
use super::value::{Int, Ptr, Value, ValueTypeName};
use crate::body::{
local::{Local, LocalDecl},
place::FieldIndex,
rvalue::BinOp,
};
/// Type alias for interpreter diagnostics.
///
/// The default severity kind is [`Severity`], which allows any severity level.
pub(crate) type InterpretDiagnostic<K = Severity> =
Diagnostic<InterpretDiagnosticCategory, SpanId, K>;
// Terminal categories for ICEs
const LOCAL_ACCESS: TerminalDiagnosticCategory = TerminalDiagnosticCategory {
id: "local-access",
name: "Local Access",
};
const TYPE_INVARIANT: TerminalDiagnosticCategory = TerminalDiagnosticCategory {
id: "type-invariant",
name: "Type Invariant",
};
const STRUCTURAL_INVARIANT: TerminalDiagnosticCategory = TerminalDiagnosticCategory {
id: "structural-invariant",
name: "Structural Invariant",
};
const CONTROL_FLOW: TerminalDiagnosticCategory = TerminalDiagnosticCategory {
id: "control-flow",
name: "Control Flow",
};
// Terminal categories for user-facing errors (some are temporarily Error, will become ICE)
const BOUNDS_CHECK: TerminalDiagnosticCategory = TerminalDiagnosticCategory {
id: "bounds-check",
name: "Bounds Check",
};
const RUNTIME_LIMIT: TerminalDiagnosticCategory = TerminalDiagnosticCategory {
id: "runtime-limit",
name: "Runtime Limit",
};
const INPUT_RESOLUTION: TerminalDiagnosticCategory = TerminalDiagnosticCategory {
id: "input-resolution",
name: "Input Resolution",
};
/// Diagnostic category for interpreter errors.
///
/// Each category corresponds to a specific class of error that can occur
/// during interpretation. Categories are used to organize and filter
/// diagnostics in error reporting.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum InterpretDiagnosticCategory {
/// Error accessing a local variable (e.g., uninitialized local).
LocalAccess,
/// Type system invariant violation (should have been caught by typeck).
TypeInvariant,
/// MIR structural invariant violation (malformed MIR).
StructuralInvariant,
/// Invalid control flow (e.g., unreachable code reached).
ControlFlow,
/// Index out of bounds error.
BoundsCheck,
/// Resource limit exceeded (e.g., recursion limit).
RuntimeLimit,
/// Required input not provided.
InputResolution,
}
impl DiagnosticCategory for InterpretDiagnosticCategory {
fn id(&self) -> Cow<'_, str> {
Cow::Borrowed("interpret")
}
fn name(&self) -> Cow<'_, str> {
Cow::Borrowed("Interpret")
}
fn subcategory(&self) -> Option<&dyn DiagnosticCategory> {
match self {
Self::LocalAccess => Some(&LOCAL_ACCESS),
Self::TypeInvariant => Some(&TYPE_INVARIANT),
Self::StructuralInvariant => Some(&STRUCTURAL_INVARIANT),
Self::ControlFlow => Some(&CONTROL_FLOW),
Self::BoundsCheck => Some(&BOUNDS_CHECK),
Self::RuntimeLimit => Some(&RUNTIME_LIMIT),
Self::InputResolution => Some(&INPUT_RESOLUTION),
}
}
}
/// A type name for use in error messages.
///
/// This is a simplified type representation used in diagnostics. It captures
/// enough information to display a meaningful type name without requiring
/// the full type system infrastructure.
#[derive(Debug, Clone)]
pub enum TypeName {
/// A static type name (e.g., "Integer", "String").
Static(Cow<'static, str>),
/// A function pointer type, displaying its definition ID.
Pointer(Ptr),
}
impl TypeName {
/// Creates a type name from a static string.
///
/// Used for simple type names like "Integer", "String", etc.
pub const fn terse(str: &'static str) -> Self {
Self::Static(Cow::Borrowed(str))
}
}
impl Display for TypeName {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Static(name) => Display::fmt(name, fmt),
Self::Pointer(ptr) => Display::fmt(ptr, fmt),
}
}
}
impl<A: Allocator> From<ValueTypeName<'_, '_, A>> for TypeName {
fn from(value: ValueTypeName<'_, '_, A>) -> Self {
value.into_type_name()
}
}
/// Details of a binary operator type mismatch.
///
/// Contains the operator, expected types, and actual values for diagnostic
/// reporting when a binary operation receives operands of incorrect types.
#[derive(Debug, Clone)]
pub struct BinaryTypeMismatch<'heap, A: Allocator> {
/// The binary operator that was applied.
pub op: BinOp,
/// The expected type of the left-hand operand.
pub lhs_expected: TypeName,
/// The expected type of the right-hand operand.
pub rhs_expected: TypeName,
/// The actual left-hand value.
pub lhs: Value<'heap, A>,
/// The actual right-hand value.
pub rhs: Value<'heap, A>,
}
/// Details of a unary operator type mismatch.
///
/// Contains the operator, expected type, and actual value for diagnostic
/// reporting when a unary operation receives an operand of incorrect type.
#[derive(Debug, Clone)]
pub struct UnaryTypeMismatch<'heap, A: Allocator> {
/// The unary operator that was applied.
pub op: UnOp,
/// The expected type of the operand.
pub expected: TypeName,
/// The actual value.
pub value: Value<'heap, A>,
}
/// Errors that can occur during MIR interpretation.
///
/// Most variants represent Internal Compiler Errors (ICEs) that indicate bugs
/// in the compiler. These should never occur in correctly compiled code because
/// earlier phases (type checking, MIR construction) should prevent them.
///
/// A few variants represent legitimate runtime errors that can occur in valid
/// programs (marked in their documentation).
#[derive(Debug, Clone)]
pub enum RuntimeError<'heap, A: Allocator> {
/// Attempted to read an uninitialized local variable.
///
/// This is an ICE: MIR construction should ensure locals are initialized
/// before use, or HIR should have caught the use of undefined variables.
UninitializedLocal {
local: Local,
decl: LocalDecl<'heap>,
},
/// Index operation used an invalid type for the index.
///
/// This is an ICE: type checking should ensure index types are valid.
InvalidIndexType { base: TypeName, index: TypeName },
/// Subscript operation applied to a non-subscriptable type.
///
/// This is an ICE: type checking should ensure subscript targets are
/// lists or dicts.
InvalidSubscriptType { base: TypeName },
/// Field projection applied to a non-projectable type.
///
/// This is an ICE: type checking should ensure projection targets are
/// structs or tuples.
InvalidProjectionType { base: TypeName },
/// Named field projection applied to a non-struct type.
///
/// This is an ICE: type checking should ensure named field access is
/// only used on structs.
InvalidProjectionByNameType { base: TypeName },
/// Field index does not exist on the aggregate type.
///
/// This is an ICE: type checking should validate field indices.
UnknownField { base: TypeName, field: FieldIndex },
/// Field name does not exist on the struct type.
///
/// This is an ICE: type checking should validate field names.
UnknownFieldByName {
base: TypeName,
field: Symbol<'heap>,
},
/// Struct aggregate has mismatched value and field counts.
///
/// This is an ICE: MIR construction should ensure aggregates have the
/// correct number of values for their fields.
StructFieldLengthMismatch { values: usize, fields: usize },
/// Switch discriminant has a non-integer type.
///
/// This is an ICE: type checking should ensure switch discriminants
/// are integers.
InvalidDiscriminantType { r#type: TypeName },
/// Switch discriminant value has no matching branch.
///
/// This is an ICE: MIR construction should ensure all possible
/// discriminant values have corresponding branches.
InvalidDiscriminant { value: Int },
/// Execution reached unreachable code.
///
/// This is an ICE: control flow analysis should prevent reaching
/// unreachable terminators.
UnreachableReached,
/// Binary operator received operands of incorrect types.
///
/// This is an ICE: type checking should ensure operand types match
/// the operator's requirements.
BinaryTypeMismatch(Box<BinaryTypeMismatch<'heap, A>>),
/// Unary operator received an operand of incorrect type.
///
/// This is an ICE: type checking should ensure operand type matches
/// the operator's requirements.
UnaryTypeMismatch(Box<UnaryTypeMismatch<'heap, A>>),
/// Function call applied to a non-pointer value.
///
/// This is an ICE: type checking should ensure only function pointers
/// are called.
ApplyNonPointer { r#type: TypeName },
/// Attempted to step execution with an empty callstack.
///
/// This is an ICE: interpreter logic should prevent this state.
CallstackEmpty,
/// Index is out of bounds for the collection.
///
/// This is currently a user-facing error but may become an ICE once
/// bounds checking is implemented in program analysis.
OutOfRange { length: usize, index: Int },
/// Required input was not provided to the runtime.
///
/// This is currently a user-facing error but may become an ICE once
/// input validation is implemented in program analysis.
InputNotFound { name: Symbol<'heap> },
/// Recursion depth exceeded the configured limit.
///
/// This is a user-facing error that occurs when a program recurses
/// too deeply, likely due to infinite recursion or deeply nested
/// data structures.
RecursionLimitExceeded { limit: usize },
}
impl<A: Allocator> RuntimeError<'_, A> {
/// Converts this runtime error into a diagnostic using the provided callstack.
///
/// The callstack provides span information for error localization. The first
/// frame's span is used as the primary label, and subsequent frames are added
/// as secondary labels to show the call trace.
pub fn into_diagnostic(
self,
callstack: impl IntoIterator<Item = SpanId>,
) -> InterpretDiagnostic {
let mut spans = callstack.into_iter();
let primary_span = spans.next().unwrap_or(SpanId::SYNTHETIC);
let mut diagnostic = self.make_diagnostic(primary_span);
// Add callstack frames as secondary labels
for span in spans {
diagnostic.add_label(Label::new(span, "called from here"));
}
diagnostic
}
fn make_diagnostic(self, span: SpanId) -> InterpretDiagnostic {
match self {
Self::UninitializedLocal { local, decl } => uninitialized_local(span, local, decl),
Self::InvalidIndexType { base, index } => invalid_index_type(span, &base, &index),
Self::InvalidSubscriptType { base } => invalid_subscript_type(span, &base),
Self::InvalidProjectionType { base } => invalid_projection_type(span, &base),
Self::InvalidProjectionByNameType { base } => {
invalid_projection_by_name_type(span, &base)
}
Self::UnknownField { base, field } => unknown_field(span, &base, field),
Self::UnknownFieldByName { base, field } => unknown_field_by_name(span, &base, field),
Self::StructFieldLengthMismatch { values, fields } => {
struct_field_length_mismatch(span, values, fields)
}
Self::InvalidDiscriminantType { r#type } => invalid_discriminant_type(span, &r#type),
Self::InvalidDiscriminant { value } => invalid_discriminant(span, value),
Self::UnreachableReached => unreachable_reached(span),
Self::BinaryTypeMismatch(mismatch) => binary_type_mismatch(span, *mismatch),
Self::UnaryTypeMismatch(mismatch) => unary_type_mismatch(span, *mismatch),
Self::ApplyNonPointer { r#type } => apply_non_pointer(span, &r#type),
Self::CallstackEmpty => callstack_empty(span),
Self::OutOfRange { length, index } => out_of_range(span, length, index),
Self::InputNotFound { name } => input_not_found(span, name),
Self::RecursionLimitExceeded { limit } => recursion_limit_exceeded(span, limit),
}
}
}
fn uninitialized_local(span: SpanId, local: Local, decl: LocalDecl) -> InterpretDiagnostic {
let name = core::fmt::from_fn(|fmt| {
if let Some(symbol) = decl.name {
Display::fmt(&symbol, fmt)
} else {
Display::fmt(&local, fmt)
}
});
let mut diagnostic =
Diagnostic::new(InterpretDiagnosticCategory::LocalAccess, Severity::Bug).primary(
Label::new(span, format!("local `{name}` used before initialization")),
);
diagnostic.add_label(Label::new(decl.span, "local declared here"));
diagnostic.add_message(Message::help(
"MIR construction should ensure all locals are initialized before use",
));
diagnostic
}
// =============================================================================
// ICE: Type Invariant
// =============================================================================
fn invalid_index_type(span: SpanId, base: &TypeName, index: &TypeName) -> InterpretDiagnostic {
let mut diagnostic =
Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary(
Label::new(span, format!("cannot index `{base}` with `{index}`")),
);
diagnostic.add_message(Message::help(
"type checking should have ensured valid index types",
));
diagnostic
}
fn invalid_subscript_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic {
let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug)
.primary(Label::new(span, format!("cannot subscript `{base}`")));
diagnostic.add_message(Message::help(
"type checking should have ensured only subscriptable types are subscripted",
));
diagnostic
}
fn invalid_projection_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic {
let mut diagnostic =
Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary(
Label::new(span, format!("cannot project field from `{base}`")),
);
diagnostic.add_message(Message::help(
"type checking should have ensured only projectable types are projected",
));
diagnostic
}
fn invalid_projection_by_name_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic {
let mut diagnostic =
Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary(
Label::new(span, format!("cannot project named field from `{base}`")),
);
diagnostic.add_message(Message::help(
"type checking should have ensured only struct types are projected by name",
));
diagnostic
}
fn unknown_field(span: SpanId, base: &TypeName, field: FieldIndex) -> InterpretDiagnostic {
let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug)
.primary(Label::new(
span,
format!("field index {field} does not exist on `{base}`"),
));
diagnostic.add_message(Message::help(
"type checking should have ensured field indices are valid",
));
diagnostic
}
fn unknown_field_by_name(span: SpanId, base: &TypeName, field: Symbol) -> InterpretDiagnostic {
let mut diagnostic =
Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary(
Label::new(span, format!("field `{field}` does not exist on `{base}`")),
);
diagnostic.add_message(Message::help(
"type checking should have ensured field names are valid",
));
diagnostic
}
fn invalid_discriminant_type(span: SpanId, r#type: &TypeName) -> InterpretDiagnostic {
let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug)
.primary(Label::new(
span,
format!("switch discriminant has type `{type}`, expected `Integer`"),
));
diagnostic.add_message(Message::help(
"type checking should have ensured discriminants are integers",
));
diagnostic
}
fn binary_type_mismatch<A: Allocator>(
span: SpanId,
BinaryTypeMismatch {
op,
lhs_expected,
rhs_expected,
lhs,
rhs,
}: BinaryTypeMismatch<A>,
) -> InterpretDiagnostic {
let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug)
.primary(Label::new(
span,
format!(
"cannot apply `{}` to `{}` and `{}`",
op.as_str(),
lhs.type_name(),
rhs.type_name()
),
));
diagnostic.add_message(Message::note(format!(
"expected `{lhs_expected}` and `{rhs_expected}`"
)));
diagnostic.add_message(Message::help(
"type checking should have ensured operand types match the operator",
));
diagnostic
}
fn unary_type_mismatch<A: Allocator>(
span: SpanId,
UnaryTypeMismatch {
op,
expected,
value,
}: UnaryTypeMismatch<A>,
) -> InterpretDiagnostic {
let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug)
.primary(Label::new(
span,
format!("cannot apply `{}` to `{}`", op.as_str(), value.type_name()),
));
diagnostic.add_message(Message::note(format!("expected `{expected}`")));
diagnostic.add_message(Message::help(
"type checking should have ensured operand type matches the operator",
));
diagnostic
}
fn apply_non_pointer(span: SpanId, r#type: &TypeName) -> InterpretDiagnostic {
let mut diagnostic =
Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary(
Label::new(span, format!("cannot call `{type}` as a function")),
);
diagnostic.add_message(Message::help(
"type checking should have ensured only function pointers are called",
));
diagnostic
}
// =============================================================================
// ICE: Structural Invariant
// =============================================================================
fn struct_field_length_mismatch(span: SpanId, values: usize, fields: usize) -> InterpretDiagnostic {
let mut diagnostic = Diagnostic::new(
InterpretDiagnosticCategory::StructuralInvariant,
Severity::Bug,
)
.primary(Label::new(
span,
format!("struct aggregate has {values} values but {fields} fields"),
));
diagnostic.add_message(Message::help(
"MIR construction should ensure aggregate value counts match field counts",
));
diagnostic
}
// =============================================================================
// ICE: Control Flow
// =============================================================================
fn invalid_discriminant(span: SpanId, value: Int) -> InterpretDiagnostic {
let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Severity::Bug)
.primary(Label::new(
span,
format!("switch discriminant `{value}` has no matching branch"),
));
diagnostic.add_message(Message::help(
"MIR construction should ensure all discriminant values have corresponding branches",
));
diagnostic
}
fn unreachable_reached(span: SpanId) -> InterpretDiagnostic {
let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Severity::Bug)
.primary(Label::new(span, "reached unreachable code"));
diagnostic.add_message(Message::help(
"control flow analysis should have ensured this code is never reached",
));
diagnostic
}
#[coverage(off)]
fn callstack_empty(span: SpanId) -> InterpretDiagnostic {
let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Severity::Bug)
.primary(Label::new(span, "attempted to step with empty callstack"));
diagnostic.add_message(Message::help(
"interpreter logic error: callstack should never be empty during execution",
));
diagnostic
}
// =============================================================================
// Error: Bounds Check (ICE in the future)
// =============================================================================
fn out_of_range(span: SpanId, length: usize, index: Int) -> InterpretDiagnostic {
let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::BoundsCheck, Severity::Error)
.primary(Label::new(
span,
format!("index `{index}` is out of bounds for length {length}"),
));
diagnostic.add_message(Message::note(format!("valid indices are 0..{length}")));
diagnostic
}
// =============================================================================
// Error: Input Resolution (ICE in the future)
// =============================================================================
fn input_not_found(span: SpanId, name: Symbol) -> InterpretDiagnostic {
let mut diagnostic = Diagnostic::new(
InterpretDiagnosticCategory::InputResolution,
Severity::Error,
)
.primary(Label::new(span, format!("input `{name}` not found")));
diagnostic.add_message(Message::help("ensure the input is provided to the runtime"));
diagnostic
}
// =============================================================================
// Error: Runtime Limit
// =============================================================================
fn recursion_limit_exceeded(span: SpanId, limit: usize) -> InterpretDiagnostic {
let mut diagnostic =
Diagnostic::new(InterpretDiagnosticCategory::RuntimeLimit, Severity::Error).primary(
Label::new(span, format!("recursion limit of {limit} exceeded")),
);
diagnostic.add_message(Message::help(
"consider refactoring to reduce recursion depth or increasing the limit",
));
diagnostic
}