Skip to content

Commit d145e88

Browse files
authored
BE-585, BE-591: HashQL: Add MIR interpreter test suite and refactor diagnostic categories (#8837)
1 parent c866a4f commit d145e88

29 files changed

Lines changed: 1960 additions & 178 deletions
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use hashql_core::r#type::environment::Environment;
2+
use hashql_diagnostics::Diagnostic;
3+
use hashql_mir::{
4+
intern::Interner,
5+
interpret::{CallStack, Inputs, Runtime, RuntimeConfig},
6+
};
7+
8+
use super::{
9+
RunContext, Suite, SuiteDiagnostic,
10+
mir_pass_transform_post_inline::mir_pass_transform_post_inline,
11+
mir_pass_transform_pre_inline::TextRenderer,
12+
};
13+
14+
pub(crate) struct MirInterpret;
15+
16+
impl Suite for MirInterpret {
17+
fn name(&self) -> &'static str {
18+
"mir/interpret"
19+
}
20+
21+
fn description(&self) -> &'static str {
22+
"Run the interpreter on the MIR"
23+
}
24+
25+
fn secondary_file_extensions(&self) -> &[&str] {
26+
&["mir"]
27+
}
28+
29+
fn run<'heap>(
30+
&self,
31+
RunContext {
32+
heap,
33+
diagnostics,
34+
secondary_outputs,
35+
..
36+
}: RunContext<'_, 'heap>,
37+
expr: hashql_ast::node::expr::Expr<'heap>,
38+
) -> Result<String, SuiteDiagnostic> {
39+
let mut environment = Environment::new(heap);
40+
let interner = Interner::new(heap);
41+
42+
let mut buffer = Vec::new();
43+
44+
let (root, bodies, _) = mir_pass_transform_post_inline(
45+
heap,
46+
expr,
47+
&interner,
48+
TextRenderer::new(&mut buffer),
49+
&mut environment,
50+
diagnostics,
51+
)?;
52+
53+
secondary_outputs.insert("mir", String::from_utf8_lossy_owned(buffer));
54+
55+
let inputs = Inputs::new();
56+
let mut runtime = Runtime::new(RuntimeConfig::default(), &bodies, &inputs);
57+
let callstack = CallStack::new(&runtime, root, []);
58+
59+
let output = runtime
60+
.run(callstack, |_| unimplemented!())
61+
.map_err(Diagnostic::generalize)
62+
.map_err(Diagnostic::boxed)?;
63+
64+
Ok(format!("{output:#?}"))
65+
}
66+
}

libs/@local/hashql/compiletest/src/suite/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ mod hir_lower_normalization;
2121
mod hir_lower_specialization;
2222
mod hir_lower_thunking;
2323
mod hir_reify;
24+
mod mir_interpret;
2425
mod mir_pass_analysis_data_dependency;
2526
mod mir_pass_transform_administrative_reduction;
2627
mod mir_pass_transform_cfg_simplify;
@@ -59,7 +60,7 @@ use self::{
5960
hir_lower_normalization::HirLowerNormalizationSuite,
6061
hir_lower_specialization::HirLowerSpecializationSuite,
6162
hir_lower_thunking::HirLowerThunkingSuite, hir_reify::HirReifySuite,
62-
mir_pass_analysis_data_dependency::MirPassAnalysisDataDependency,
63+
mir_interpret::MirInterpret, mir_pass_analysis_data_dependency::MirPassAnalysisDataDependency,
6364
mir_pass_transform_administrative_reduction::MirPassTransformAdministrativeReduction,
6465
mir_pass_transform_cfg_simplify::MirPassTransformCfgSimplify,
6566
mir_pass_transform_dse::MirPassTransformDse,
@@ -163,6 +164,7 @@ const SUITES: &[&dyn Suite] = &[
163164
&HirLowerTypeInferenceIntrinsicsSuite,
164165
&HirLowerTypeInferenceSuite,
165166
&HirReifySuite,
167+
&MirInterpret,
166168
&MirPassAnalysisDataDependency,
167169
&MirPassTransformAdministrativeReduction,
168170
&MirPassTransformCfgSimplify,

libs/@local/hashql/eval/src/orchestrator/error.rs

Lines changed: 112 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1-
//! Errors that occur while fulfilling [`GraphRead`] suspensions.
1+
//! Error types for the orchestration layer.
22
//!
3-
//! These are internal runtime errors: failures in compiled query execution,
4-
//! row decoding, or parameter encoding. The user wrote HashQL, not SQL; if
5-
//! the bridge fails, it indicates a bug in the compiler or runtime.
3+
//! The orchestrator sits between the MIR interpreter and external data sources
4+
//! (PostgreSQL). Errors fall into two families:
5+
//!
6+
//! - **Interpreter errors**: failures in the MIR interpreter itself (type invariant violations,
7+
//! control flow errors, etc.). These are produced by the interpreter and forwarded through the
8+
//! orchestrator.
9+
//! - **Bridge errors**: failures while fulfilling [`GraphRead`] suspensions (query execution, row
10+
//! decoding, parameter encoding). The user wrote HashQL, not SQL; if the bridge fails, it
11+
//! indicates a bug in the compiler or runtime.
12+
//!
13+
//! [`OrchestratorDiagnosticCategory`] unifies both families under a single
14+
//! category hierarchy so that downstream consumers (the eval crate) see one
15+
//! coherent diagnostic type from the orchestration layer.
616
//!
717
//! [`GraphRead`]: hashql_mir::body::terminator::GraphRead
818
9-
use alloc::string::String;
19+
use alloc::{borrow::Cow, string::String};
1020

1121
use hashql_core::{
1222
pretty::{Formatter, RenderOptions},
@@ -15,15 +25,15 @@ use hashql_core::{
1525
r#type::{TypeFormatter, TypeFormatterOptions, TypeId, environment::Environment},
1626
};
1727
use hashql_diagnostics::{
18-
Diagnostic, Label, category::TerminalDiagnosticCategory, diagnostic::Message,
19-
severity::Severity,
28+
Diagnostic, Label,
29+
category::{DiagnosticCategory, TerminalDiagnosticCategory},
30+
diagnostic::Message,
31+
severity::Critical,
2032
};
2133
use hashql_mir::{
2234
body::{basic_block::BasicBlockId, local::Local},
2335
def::DefId,
24-
interpret::error::{
25-
InterpretDiagnostic, InterpretDiagnosticCategory, SuspensionDiagnosticCategory,
26-
},
36+
interpret::error::InterpretDiagnosticCategory,
2737
};
2838

2939
use super::{Indexed, codec::JsonValueKind};
@@ -89,8 +99,64 @@ const VALUE_SERIALIZATION: TerminalDiagnosticCategory = TerminalDiagnosticCatego
8999
name: "Value Serialization",
90100
};
91101

92-
const fn category(terminal: &'static TerminalDiagnosticCategory) -> InterpretDiagnosticCategory {
93-
InterpretDiagnosticCategory::Suspension(SuspensionDiagnosticCategory(terminal))
102+
/// Type alias for orchestrator diagnostics.
103+
///
104+
/// The default severity kind is [`Critical`].
105+
pub type OrchestratorDiagnostic<K = Critical> =
106+
Diagnostic<OrchestratorDiagnosticCategory, SpanId, K>;
107+
108+
/// Diagnostic subcategory for errors that occur while fulfilling a suspension.
109+
///
110+
/// Wraps a [`TerminalDiagnosticCategory`] that identifies the specific bridge
111+
/// failure (query execution, row hydration, parameter encoding, etc.).
112+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
113+
pub struct BridgeDiagnosticCategory(pub &'static TerminalDiagnosticCategory);
114+
115+
impl DiagnosticCategory for BridgeDiagnosticCategory {
116+
fn id(&self) -> Cow<'_, str> {
117+
Cow::Borrowed("bridge")
118+
}
119+
120+
fn name(&self) -> Cow<'_, str> {
121+
Cow::Borrowed("Bridge")
122+
}
123+
124+
fn subcategory(&self) -> Option<&dyn DiagnosticCategory> {
125+
Some(self.0)
126+
}
127+
}
128+
129+
/// Top-level diagnostic category for the orchestration layer.
130+
///
131+
/// Unifies interpreter errors (forwarded from the MIR interpreter) and bridge
132+
/// errors (failures while fulfilling suspensions) under a single hierarchy.
133+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
134+
pub enum OrchestratorDiagnosticCategory {
135+
/// An error produced by the MIR interpreter itself.
136+
Interpret(InterpretDiagnosticCategory),
137+
/// An error from the bridge while fulfilling a suspension.
138+
Bridge(BridgeDiagnosticCategory),
139+
}
140+
141+
impl DiagnosticCategory for OrchestratorDiagnosticCategory {
142+
fn id(&self) -> Cow<'_, str> {
143+
Cow::Borrowed("orchestrator")
144+
}
145+
146+
fn name(&self) -> Cow<'_, str> {
147+
Cow::Borrowed("Orchestrator")
148+
}
149+
150+
fn subcategory(&self) -> Option<&dyn DiagnosticCategory> {
151+
match self {
152+
Self::Interpret(cat) => Some(cat),
153+
Self::Bridge(cat) => Some(cat),
154+
}
155+
}
156+
}
157+
158+
const fn category(terminal: &'static TerminalDiagnosticCategory) -> OrchestratorDiagnosticCategory {
159+
OrchestratorDiagnosticCategory::Bridge(BridgeDiagnosticCategory(terminal))
94160
}
95161

96162
/// Errors that occur while decoding a JSON value into a typed [`Value`].
@@ -356,7 +422,7 @@ pub enum BridgeError<'heap> {
356422
}
357423

358424
impl<'heap> BridgeError<'heap> {
359-
pub fn into_diagnostic(self, span: SpanId, env: &Environment<'heap>) -> InterpretDiagnostic {
425+
pub fn into_diagnostic(self, span: SpanId, env: &Environment<'heap>) -> OrchestratorDiagnostic {
360426
match self {
361427
Self::QueryExecution { sql, source } => query_execution(span, &sql, &source),
362428
Self::RowHydration { column, source } => row_hydration(span, column, &source),
@@ -388,8 +454,12 @@ impl<'heap> BridgeError<'heap> {
388454
}
389455
}
390456

391-
fn query_execution(span: SpanId, sql: &str, error: &tokio_postgres::Error) -> InterpretDiagnostic {
392-
let mut diagnostic = Diagnostic::new(category(&QUERY_EXECUTION), Severity::Bug).primary(
457+
fn query_execution(
458+
span: SpanId,
459+
sql: &str,
460+
error: &tokio_postgres::Error,
461+
) -> OrchestratorDiagnostic {
462+
let mut diagnostic = Diagnostic::new(category(&QUERY_EXECUTION), Critical::BUG).primary(
393463
Label::new(span, "compiled query was rejected by the database"),
394464
);
395465

@@ -411,9 +481,9 @@ fn row_hydration(
411481
value: column,
412482
}: Indexed<ColumnDescriptor>,
413483
source: &tokio_postgres::Error,
414-
) -> InterpretDiagnostic {
484+
) -> OrchestratorDiagnostic {
415485
let mut diagnostic =
416-
Diagnostic::new(category(&ROW_HYDRATION), Severity::Bug).primary(Label::new(
486+
Diagnostic::new(category(&ROW_HYDRATION), Critical::BUG).primary(Label::new(
417487
span,
418488
format!("cannot decode result column {index} ({column})"),
419489
));
@@ -429,7 +499,7 @@ fn row_hydration(
429499

430500
/// Adds notes describing a [`DecodeError`] to a diagnostic.
431501
fn add_decode_error_notes(
432-
diagnostic: &mut InterpretDiagnostic,
502+
diagnostic: &mut OrchestratorDiagnostic,
433503
source: &DecodeError<'_>,
434504
env: &Environment<'_>,
435505
) {
@@ -543,9 +613,9 @@ fn value_deserialization(
543613
}: Indexed<ColumnDescriptor>,
544614
source: &DecodeError<'_>,
545615
env: &Environment<'_>,
546-
) -> InterpretDiagnostic {
616+
) -> OrchestratorDiagnostic {
547617
let mut diagnostic =
548-
Diagnostic::new(category(&VALUE_DESERIALIZATION), Severity::Bug).primary(Label::new(
618+
Diagnostic::new(category(&VALUE_DESERIALIZATION), Critical::BUG).primary(Label::new(
549619
span,
550620
format!("cannot deserialize result column {index} ({column})"),
551621
));
@@ -565,8 +635,8 @@ fn continuation_deserialization(
565635
local: Local,
566636
source: &DecodeError<'_>,
567637
env: &Environment<'_>,
568-
) -> InterpretDiagnostic {
569-
let mut diagnostic = Diagnostic::new(category(&CONTINUATION_DESERIALIZATION), Severity::Bug)
638+
) -> OrchestratorDiagnostic {
639+
let mut diagnostic = Diagnostic::new(category(&CONTINUATION_DESERIALIZATION), Critical::BUG)
570640
.primary(Label::new(
571641
span,
572642
format!("cannot deserialize continuation local {local} in definition {body}"),
@@ -582,9 +652,13 @@ fn continuation_deserialization(
582652
diagnostic
583653
}
584654

585-
fn invalid_continuation_block_id(span: SpanId, body: DefId, block_id: i32) -> InterpretDiagnostic {
655+
fn invalid_continuation_block_id(
656+
span: SpanId,
657+
body: DefId,
658+
block_id: i32,
659+
) -> OrchestratorDiagnostic {
586660
let mut diagnostic =
587-
Diagnostic::new(category(&INVALID_CONTINUATION_BLOCK_ID), Severity::Bug).primary(
661+
Diagnostic::new(category(&INVALID_CONTINUATION_BLOCK_ID), Critical::BUG).primary(
588662
Label::new(span, "continuation returned an invalid block ID"),
589663
);
590664

@@ -599,8 +673,8 @@ fn invalid_continuation_block_id(span: SpanId, body: DefId, block_id: i32) -> In
599673
diagnostic
600674
}
601675

602-
fn invalid_continuation_local(span: SpanId, body: DefId, local: i32) -> InterpretDiagnostic {
603-
let mut diagnostic = Diagnostic::new(category(&INVALID_CONTINUATION_LOCAL), Severity::Bug)
676+
fn invalid_continuation_local(span: SpanId, body: DefId, local: i32) -> OrchestratorDiagnostic {
677+
let mut diagnostic = Diagnostic::new(category(&INVALID_CONTINUATION_LOCAL), Critical::BUG)
604678
.primary(Label::new(span, "continuation returned an invalid local"));
605679

606680
diagnostic.add_message(Message::note(format!(
@@ -618,9 +692,9 @@ fn parameter_encoding(
618692
span: SpanId,
619693
parameter: usize,
620694
error: &(dyn core::error::Error + Send + Sync),
621-
) -> InterpretDiagnostic {
695+
) -> OrchestratorDiagnostic {
622696
let mut diagnostic =
623-
Diagnostic::new(category(&PARAMETER_ENCODING), Severity::Bug).primary(Label::new(
697+
Diagnostic::new(category(&PARAMETER_ENCODING), Critical::BUG).primary(Label::new(
624698
span,
625699
format!(
626700
"cannot encode parameter ${} for the database",
@@ -637,8 +711,8 @@ fn parameter_encoding(
637711
diagnostic
638712
}
639713

640-
fn query_lookup(span: SpanId, body: DefId, block: BasicBlockId) -> InterpretDiagnostic {
641-
let mut diagnostic = Diagnostic::new(category(&QUERY_LOOKUP), Severity::Bug).primary(
714+
fn query_lookup(span: SpanId, body: DefId, block: BasicBlockId) -> OrchestratorDiagnostic {
715+
let mut diagnostic = Diagnostic::new(category(&QUERY_LOOKUP), Critical::BUG).primary(
642716
Label::new(span, "no compiled query found for this data access"),
643717
);
644718

@@ -653,8 +727,8 @@ fn query_lookup(span: SpanId, body: DefId, block: BasicBlockId) -> InterpretDiag
653727
diagnostic
654728
}
655729

656-
fn incomplete_continuation(span: SpanId, body: DefId, field: &str) -> InterpretDiagnostic {
657-
let mut diagnostic = Diagnostic::new(category(&INCOMPLETE_CONTINUATION), Severity::Bug)
730+
fn incomplete_continuation(span: SpanId, body: DefId, field: &str) -> OrchestratorDiagnostic {
731+
let mut diagnostic = Diagnostic::new(category(&INCOMPLETE_CONTINUATION), Critical::BUG)
658732
.primary(Label::new(
659733
span,
660734
"continuation state is missing required columns",
@@ -671,8 +745,8 @@ fn incomplete_continuation(span: SpanId, body: DefId, field: &str) -> InterpretD
671745
diagnostic
672746
}
673747

674-
fn missing_execution_residual(span: SpanId, body: DefId) -> InterpretDiagnostic {
675-
let mut diagnostic = Diagnostic::new(category(&MISSING_EXECUTION_RESIDUAL), Severity::Bug)
748+
fn missing_execution_residual(span: SpanId, body: DefId) -> OrchestratorDiagnostic {
749+
let mut diagnostic = Diagnostic::new(category(&MISSING_EXECUTION_RESIDUAL), Critical::BUG)
676750
.primary(Label::new(
677751
span,
678752
"no execution residual found for this definition",
@@ -689,8 +763,8 @@ fn missing_execution_residual(span: SpanId, body: DefId) -> InterpretDiagnostic
689763
diagnostic
690764
}
691765

692-
fn invalid_filter_return(span: SpanId, body: DefId) -> InterpretDiagnostic {
693-
let mut diagnostic = Diagnostic::new(category(&INVALID_FILTER_RETURN), Severity::Bug)
766+
fn invalid_filter_return(span: SpanId, body: DefId) -> OrchestratorDiagnostic {
767+
let mut diagnostic = Diagnostic::new(category(&INVALID_FILTER_RETURN), Critical::BUG)
694768
.primary(Label::new(span, "filter body returned a non-boolean value"));
695769

696770
diagnostic.add_message(Message::note(format!(
@@ -704,8 +778,8 @@ fn invalid_filter_return(span: SpanId, body: DefId) -> InterpretDiagnostic {
704778
diagnostic
705779
}
706780

707-
fn value_serialization(span: SpanId, error: &serde_json::Error) -> InterpretDiagnostic {
708-
let mut diagnostic = Diagnostic::new(category(&VALUE_SERIALIZATION), Severity::Bug)
781+
fn value_serialization(span: SpanId, error: &serde_json::Error) -> OrchestratorDiagnostic {
782+
let mut diagnostic = Diagnostic::new(category(&VALUE_SERIALIZATION), Critical::BUG)
709783
.primary(Label::new(span, "cannot serialize runtime value to JSON"));
710784

711785
diagnostic.add_message(Message::note(format!("serialization failed: {error}")));

0 commit comments

Comments
 (0)